From 2455720480778d5c8872209f7b5ee9e80587bc0b Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 8 Sep 2026 22:23:23 +0800 Subject: [PATCH 001/127] Add autonomous WebArena-Verified browser evaluation --- .env.example | 4 +- .gitignore | 5 +- Makefile | 14 +- README.md | 39 +- .../webarena-verified-pilot.json | 47 + examples/browser_agent/README.md | 23 + examples/browser_agent/run.sh | 15 + .../{browser_use => browser_kernel}/README.md | 12 +- .../agent_smoke.json | 0 .../build_public_suite.py | 0 .../docker/chromium.Dockerfile | 2 +- .../docker/moli.Dockerfile | 11 +- .../docker/start-chromium.sh | 3 + .../fixtures/app.html | 0 .../fixtures/catalog.html | 0 .../fixtures/core.html | 0 .../fixtures/dynamic.html | 0 .../fixtures/forms.html | 0 .../fixtures/interaction-space.html | 0 .../fixtures/page2.html | 0 .../fixtures/page3.html | 0 .../ground_truth_validation.json | 0 .../rebuild_report.py | 16 +- .../{browser_use => browser_kernel}/run.sh | 42 +- .../tasks.v2.json | 0 .../tasks.v3.json | 0 .../tasks.v4.json | 0 .../verify_metrics.py | 2 +- examples/web_fetch/README.md | 2 +- harness/browser_agent/__init__.py | 5 + harness/browser_agent/model.py | 189 ++++ harness/browser_agent/report.py | 227 +++++ harness/browser_agent/runner.py | 589 ++++++++++++ harness/browser_agent/selection.py | 105 +++ .../__init__.py | 0 .../{browser_use => browser_kernel}/cli.py | 2 +- .../diagnostics.py | 0 .../executor.py | 0 .../metrics.py | 0 .../{browser_use => browser_kernel}/report.py | 16 +- .../{browser_use => browser_kernel}/runner.py | 2 +- .../{browser_use => browser_kernel}/suite.py | 2 +- harness/web_fetch/README.md | 4 +- pyproject.toml | 16 +- .../browser-agent-benchmark-adoption.json | 104 +++ scripts/build_browser_agent_report.py | 6 + scripts/check_browser_agent_report.py | 111 +++ scripts/package_browser_agent_artifact_hub.py | 38 + scripts/publish_browser_agent_artifact_hub.py | 90 ++ tests/browser_agent/test_model.py | 143 +++ tests/browser_agent/test_report.py | 67 ++ tests/browser_agent/test_runner.py | 27 + tests/browser_agent/test_selection.py | 13 + .../test_diagnostics.py | 4 +- .../test_executor.py | 2 +- .../test_metrics.py | 2 +- .../test_real_smoke.py | 2 +- .../test_report.py | 40 +- .../test_runner.py | 8 +- .../test_suite.py | 16 +- .../test_verify_metrics.py | 4 +- uv.lock | 875 +++++++++++++++++- 62 files changed, 2819 insertions(+), 127 deletions(-) create mode 100644 evaluation/browser-agent/webarena-verified-pilot.json create mode 100644 examples/browser_agent/README.md create mode 100755 examples/browser_agent/run.sh rename examples/{browser_use => browser_kernel}/README.md (96%) rename examples/{browser_use => browser_kernel}/agent_smoke.json (100%) rename examples/{browser_use => browser_kernel}/build_public_suite.py (100%) rename examples/{browser_use => browser_kernel}/docker/chromium.Dockerfile (85%) rename examples/{browser_use => browser_kernel}/docker/moli.Dockerfile (76%) rename examples/{browser_use => browser_kernel}/docker/start-chromium.sh (88%) rename examples/{browser_use => browser_kernel}/fixtures/app.html (100%) rename examples/{browser_use => browser_kernel}/fixtures/catalog.html (100%) rename examples/{browser_use => browser_kernel}/fixtures/core.html (100%) rename examples/{browser_use => browser_kernel}/fixtures/dynamic.html (100%) rename examples/{browser_use => browser_kernel}/fixtures/forms.html (100%) rename examples/{browser_use => browser_kernel}/fixtures/interaction-space.html (100%) rename examples/{browser_use => browser_kernel}/fixtures/page2.html (100%) rename examples/{browser_use => browser_kernel}/fixtures/page3.html (100%) rename examples/{browser_use => browser_kernel}/ground_truth_validation.json (100%) rename examples/{browser_use => browser_kernel}/rebuild_report.py (84%) rename examples/{browser_use => browser_kernel}/run.sh (52%) rename examples/{browser_use => browser_kernel}/tasks.v2.json (100%) rename examples/{browser_use => browser_kernel}/tasks.v3.json (100%) rename examples/{browser_use => browser_kernel}/tasks.v4.json (100%) rename examples/{browser_use => browser_kernel}/verify_metrics.py (99%) create mode 100644 harness/browser_agent/__init__.py create mode 100644 harness/browser_agent/model.py create mode 100644 harness/browser_agent/report.py create mode 100644 harness/browser_agent/runner.py create mode 100644 harness/browser_agent/selection.py rename harness/{browser_use => browser_kernel}/__init__.py (100%) rename harness/{browser_use => browser_kernel}/cli.py (95%) rename harness/{browser_use => browser_kernel}/diagnostics.py (100%) rename harness/{browser_use => browser_kernel}/executor.py (100%) rename harness/{browser_use => browser_kernel}/metrics.py (100%) rename harness/{browser_use => browser_kernel}/report.py (96%) rename harness/{browser_use => browser_kernel}/runner.py (99%) rename harness/{browser_use => browser_kernel}/suite.py (99%) create mode 100644 research/browser-agent-benchmark-adoption.json create mode 100644 scripts/build_browser_agent_report.py create mode 100644 scripts/check_browser_agent_report.py create mode 100644 scripts/package_browser_agent_artifact_hub.py create mode 100644 scripts/publish_browser_agent_artifact_hub.py create mode 100644 tests/browser_agent/test_model.py create mode 100644 tests/browser_agent/test_report.py create mode 100644 tests/browser_agent/test_runner.py create mode 100644 tests/browser_agent/test_selection.py rename tests/{browser_use => browser_kernel}/test_diagnostics.py (97%) rename tests/{browser_use => browser_kernel}/test_executor.py (98%) rename tests/{browser_use => browser_kernel}/test_metrics.py (99%) rename tests/{browser_use => browser_kernel}/test_real_smoke.py (98%) rename tests/{browser_use => browser_kernel}/test_report.py (93%) rename tests/{browser_use => browser_kernel}/test_runner.py (96%) rename tests/{browser_use => browser_kernel}/test_suite.py (91%) rename tests/{browser_use => browser_kernel}/test_verify_metrics.py (95%) diff --git a/.env.example b/.env.example index 282413c38..fb23d7989 100644 --- a/.env.example +++ b/.env.example @@ -16,9 +16,9 @@ OBSCURA_BINARY=obscura CHROMIUM_BINARY=/Applications/Google Chrome.app/Contents/MacOS/Google Chrome KITESURF_CDP_ENDPOINT=wss://kitesurf.cloudflare.app/devtools/browser -# Browser Use optional model-based diagnostics only (not Web Fetch scoring) +# Browser Kernel optional model-based diagnostics and autonomous WebArena-Verified evaluation OPENAI_API_KEY= -OPENAI_BASE_URL= +OPENAI_BASE_URL=https://api.openai.com/v1 # Optional publishing override; default: ~/.config/artifact-hub/token ARTIFACT_HUB_TOKEN= diff --git a/.gitignore b/.gitignore index 48c8dfc48..28287a01f 100644 --- a/.gitignore +++ b/.gitignore @@ -29,6 +29,5 @@ run.log *.egg-info/ .pytest_cache/ .ruff_cache/ -.browser-use-eval-venv/ -artifacts/browser_use/ -/docs/reports/browser-use-evaluation-* +artifacts/browser_kernel/ +/docs/reports/browser-kernel-evaluation-* diff --git a/Makefile b/Makefile index c702547b6..e305103a8 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ UV ?= uv PY := $(UV) run --frozen python -.PHONY: setup runtime-check test test-python test-ui fixture-smoke ci report report-check +.PHONY: setup runtime-check test test-python test-ui fixture-smoke ci report report-check browser-agent-report browser-agent-report-check setup: $(UV) sync --frozen --all-extras @@ -12,7 +12,8 @@ runtime-check: test-python: runtime-check $(PY) -m unittest discover -s tests -v - $(PY) -m pytest tests/browser_use -q + $(PY) -m pytest tests/browser_agent -q + $(PY) -m pytest tests/browser_kernel -q test-ui: node tests/test_report_ui.mjs @@ -34,3 +35,12 @@ report-check: runtime-check $(PY) scripts/check_report.py reports/webfetch $(PY) scripts/check_report_links.py reports/webfetch $(PY) scripts/check_report_revision.py reports/webfetch + +browser-agent-report: runtime-check + @test -n "$(EVIDENCE)" || (echo 'EVIDENCE=/absolute/path is required' >&2; exit 2) + $(PY) scripts/build_browser_agent_report.py --evidence "$(EVIDENCE)" --output reports/browser-agent + +browser-agent-report-check: runtime-check + @test -n "$(EVIDENCE)" || (echo 'EVIDENCE=/absolute/path is required' >&2; exit 2) + $(PY) scripts/check_browser_agent_report.py reports/browser-agent --evidence "$(EVIDENCE)" + $(PY) scripts/check_report_links.py reports/browser-agent diff --git a/README.md b/README.md index f86a4adaa..a0931f22f 100644 --- a/README.md +++ b/README.md @@ -17,17 +17,17 @@ files are local build outputs and are not committed to this repository. ## Run the browser evaluation -Requirements: Docker Engine (cgroup v2), Python 3.11+ with `venv`, at least 4 -CPU cores, 6 GiB available memory, and Internet access. +Requirements: Docker Engine (cgroup v2), `uv`, Python 3.11, at least 4 CPU +cores, 6 GiB available memory, and Internet access. ```bash -./examples/browser_use/run.sh +./examples/browser_kernel/run.sh ``` -The command builds pinned browser images, installs the controller in a local -virtual environment, runs the versioned 30-task deterministic suite, and writes: +The command builds pinned browser images, resolves the locked project +environment, runs the versioned 30-task deterministic suite, and writes: -- raw reproducibility artifacts to `artifacts/browser_use/latest/` (ignored by Git) +- raw reproducibility artifacts to `artifacts/browser_kernel/latest/` (ignored by Git) - latest-only Markdown, HTML, and audit data to `docs/reports/` (ignored by Git) All statistics and HTML are generated by repository scripts. To rebuild the @@ -35,20 +35,39 @@ summary and report from an existing `attempts.jsonl` without rerunning the browsers, use: ```bash -uv run python examples/browser_use/rebuild_report.py +uv run --frozen python examples/browser_kernel/rebuild_report.py ``` -See [examples/browser_use/README.md](examples/browser_use/README.md) for the +See [examples/browser_kernel/README.md](examples/browser_kernel/README.md) for the measurement protocol, complexity tiers, overrides, and CubeSandbox decision. ## Harness layout -- `harness/browser_use`: interactive browser-use execution, metrics, and reports +- `harness/browser_kernel`: fixed-action browser-kernel execution, metrics, and reports +- `harness/browser_agent`: autonomous CowAgent execution with official WebArena-Verified scoring - `harness/web_fetch`: Web Fetch providers, execution, scoring, and reports -Browser Use and Web Fetch intentionally keep separate suites and metrics. +The browser-kernel microbenchmark, autonomous browser-agent evaluation, and +Web Fetch intentionally keep separate suites and metrics. Web Fetch has no legacy top-level package or compatibility imports. +## Autonomous browser-agent pilot + +The second browser track runs the real CowAgent loop with GPT-6 Astra +(`reasoning.effort=low`) against 30 pre-registered WebArena-Verified tasks. Both +engines receive the same task, model, agent, site snapshot, limits, and official +evaluator. Each engine gets one primary attempt; only a failed primary receives +one retry, reported separately from pass@1. + +```bash +./examples/browser_agent/run.sh +make browser-agent-report EVIDENCE="$HOME/.browser-eval/browser-agent/current" +make browser-agent-report-check EVIDENCE="$HOME/.browser-eval/browser-agent/current" +``` + +See [examples/browser_agent/README.md](examples/browser_agent/README.md) for the +local-site and credential boundary. + --- ## Web Fetch 评测 diff --git a/evaluation/browser-agent/webarena-verified-pilot.json b/evaluation/browser-agent/webarena-verified-pilot.json new file mode 100644 index 000000000..b3f14dd93 --- /dev/null +++ b/evaluation/browser-agent/webarena-verified-pilot.json @@ -0,0 +1,47 @@ +{ + "schema": "lex_webarena_verified_pilot/1", + "metadata": { + "benchmark": "WebArena-Verified", + "benchmark_version": "1.2.3", + "difficulty_definition": "estimated minimum visible UI operations after authentication: easy 1-4, medium 5-7, hard 8+", + "selection_policy": "pre-registered before model execution; four self-hosted sites only; covers retrieve, navigate, and mutate", + "excluded_sites": { + "wikipedia": "requires a large external ZIM data mount", + "map": "requires multiple external map datasets and persistent volumes" + } + }, + "tasks": [ + {"task_id": 44, "tier": "easy", "estimated_min_steps": 2, "official_hard": true, "rationale": "打开一个需要登录的 GitLab 目标页"}, + {"task_id": 41, "tier": "easy", "estimated_min_steps": 3, "official_hard": false, "rationale": "打开一份报表并读取第一个条目"}, + {"task_id": 77, "tier": "easy", "estimated_min_steps": 3, "official_hard": false, "rationale": "打开评论列表并读取一种状态的数量"}, + {"task_id": 94, "tier": "easy", "estimated_min_steps": 4, "official_hard": false, "rationale": "搜索一张发票并读取总金额"}, + {"task_id": 404, "tier": "easy", "estimated_min_steps": 4, "official_hard": false, "rationale": "进入一个论坛、选择最新帖子并点赞"}, + + {"task_id": 45, "tier": "medium", "estimated_min_steps": 5, "official_hard": false, "rationale": "进入项目议题列表并应用排序和状态筛选"}, + {"task_id": 118, "tier": "medium", "estimated_min_steps": 5, "official_hard": false, "rationale": "把需求转成商品查询并打开匹配详情页"}, + {"task_id": 117, "tier": "medium", "estimated_min_steps": 6, "official_hard": false, "rationale": "进入订单历史,经翻页或排序后读取最早日期"}, + {"task_id": 132, "tier": "medium", "estimated_min_steps": 6, "official_hard": false, "rationale": "找到项目贡献者活动页并限定一个日期"}, + {"task_id": 168, "tier": "medium", "estimated_min_steps": 5, "official_hard": false, "rationale": "打开个人项目并检查星标与项目标识"}, + {"task_id": 173, "tier": "medium", "estimated_min_steps": 6, "official_hard": false, "rationale": "按标题和更新时间筛选个人议题,再读取状态"}, + {"task_id": 183, "tier": "medium", "estimated_min_steps": 6, "official_hard": false, "rationale": "按库存量筛选后台表格并收集 SKU"}, + {"task_id": 188, "tier": "medium", "estimated_min_steps": 6, "official_hard": false, "rationale": "按状态筛选订单,打开最新一单并读取总额"}, + {"task_id": 198, "tier": "medium", "estimated_min_steps": 6, "official_hard": false, "rationale": "筛选并排序订单,再打开最新的匹配客户"}, + {"task_id": 399, "tier": "medium", "estimated_min_steps": 6, "official_hard": true, "rationale": "打开个人资料设置,修改简介并保存"}, + + {"task_id": 28, "tier": "hard", "estimated_min_steps": 10, "official_hard": true, "rationale": "找到最新论坛帖子,并按作者和票数条件检查多条评论"}, + {"task_id": 50, "tier": "hard", "estimated_min_steps": 9, "official_hard": true, "rationale": "检查一整年的分页订单并汇总数量和支出"}, + {"task_id": 66, "tier": "hard", "estimated_min_steps": 12, "official_hard": true, "rationale": "检查十个排序帖子,并根据描述归类推荐内容"}, + {"task_id": 68, "tier": "hard", "estimated_min_steps": 12, "official_hard": true, "rationale": "检查十个排序帖子,并从合格描述中提取两个字段"}, + {"task_id": 108, "tier": "hard", "estimated_min_steps": 10, "official_hard": true, "rationale": "应用日期和状态条件,并汇总五个月度数量"}, + {"task_id": 110, "tier": "hard", "estimated_min_steps": 12, "official_hard": true, "rationale": "应用日期和状态条件,并汇总十一个月度数量"}, + {"task_id": 124, "tier": "hard", "estimated_min_steps": 9, "official_hard": true, "rationale": "搜索商品、检查结果页并计算价格区间"}, + {"task_id": 127, "tier": "hard", "estimated_min_steps": 9, "official_hard": true, "rationale": "结合搜索词排名与独立的商品可用性检查"}, + {"task_id": 142, "tier": "hard", "estimated_min_steps": 10, "official_hard": true, "rationale": "检查日期区间内的订单、归类商品并汇总行项目金额"}, + {"task_id": 184, "tier": "hard", "estimated_min_steps": 9, "official_hard": true, "rationale": "筛选库存、检查变体并返回成对商品字段"}, + {"task_id": 193, "tier": "hard", "estimated_min_steps": 8, "official_hard": true, "rationale": "排序已完成订单、检查两份详情并合计付款"}, + {"task_id": 196, "tier": "hard", "estimated_min_steps": 12, "official_hard": true, "rationale": "检查两组各四笔订单,并计算付款差额"}, + {"task_id": 204, "tier": "hard", "estimated_min_steps": 9, "official_hard": true, "rationale": "找到最新完成订单,并规范化、排序多个行项目"}, + {"task_id": 506, "tier": "hard", "estimated_min_steps": 12, "official_hard": false, "rationale": "清空购物车,按预算和评分筛选品类,选择商品并结账"}, + {"task_id": 441, "tier": "hard", "estimated_min_steps": 11, "official_hard": true, "rationale": "定位项目文件、修改标题、提交到默认分支并验证"} + ] +} diff --git a/examples/browser_agent/README.md b/examples/browser_agent/README.md new file mode 100644 index 000000000..db9982e5f --- /dev/null +++ b/examples/browser_agent/README.md @@ -0,0 +1,23 @@ +# Autonomous Browser-Agent Evaluation + +This track runs the real CowAgent decision loop against the official +WebArena-Verified evaluator. It is independent from `harness/browser_kernel`, +which replays a fixed list of browser-tool calls to isolate browser-engine +behavior and does not measure autonomous planning. + +The pilot selection is pre-registered in +`evaluation/browser-agent/webarena-verified-pilot.json`: 5 easy, 10 medium, and +15 hard cases. These are project-defined operational tiers based on an audited +minimum number of visible UI operations, not WebArena's official hard-subset +label. The pilot uses the self-hosted shopping, shopping_admin, reddit, and +gitlab sites. Wikipedia and Map are deferred because their official local setup +requires large external data bundles. + +Put `OPENAI_API_KEY`, optional `OPENAI_BASE_URL`, and `COWAGENT_REPO` in this +repository's `.env`, then run: + + ./examples/browser_agent/run.sh + +Every task runs once per engine. Only a failed official evaluation receives one +retry. The official benchmark score is calculated from primary attempts only; +retry recovery is reported separately and never replaces the primary score. diff --git a/examples/browser_agent/run.sh b/examples/browser_agent/run.sh new file mode 100755 index 000000000..868b3872f --- /dev/null +++ b/examples/browser_agent/run.sh @@ -0,0 +1,15 @@ +#!/usr/bin/env bash +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +DATA_HOME="${BROWSER_EVAL_DATA_HOME:-${HOME}/.browser-eval}" +OUTPUT="${BROWSER_AGENT_OUTPUT_DIR:-${DATA_HOME}/browser-agent/current}" + +cd "${ROOT}" +uv run --frozen --extra browser-use-eval python -m harness.browser_agent.runner \ + --repo-root "${ROOT}" \ + --selection "${ROOT}/evaluation/browser-agent/webarena-verified-pilot.json" \ + --output "${OUTPUT}" \ + "$@" + +echo "Evidence: ${OUTPUT}" diff --git a/examples/browser_use/README.md b/examples/browser_kernel/README.md similarity index 96% rename from examples/browser_use/README.md rename to examples/browser_kernel/README.md index 0e19f5d9a..fd6878836 100644 --- a/examples/browser_use/README.md +++ b/examples/browser_kernel/README.md @@ -1,4 +1,4 @@ -# Browser Use Evaluation Harness +# Browser Kernel Microbenchmark This harness compares local Chromium and Moli without measuring host-wide resource noise. It starts one isolated browser container per engine. A normal @@ -16,7 +16,7 @@ Requirements: Run: - ./examples/browser_use/run.sh + ./examples/browser_kernel/run.sh Run the small real-container smoke test before a release (requires the two evaluation images, which the command above builds): @@ -26,7 +26,7 @@ evaluation images, which the command above builds): Smoke is intentionally minimal: one real public-page case, once per engine. It reuses existing evaluation images and is not a performance benchmark. -The first run builds two images, downloads pinned Moli v1.0.6, and creates a +The first run builds two images, downloads pinned Moli v1.1.3, and creates a dedicated Python virtual environment. No Codex, coding agent, LLM API key, browser account, website login, privileged container, or host filesystem mount is required. @@ -38,7 +38,7 @@ content remain external variables rather than being silently treated as fixed. Outputs: -- raw attempts, manifest, and summary under artifacts/browser_use/latest/ +- raw attempts, manifest, and summary under artifacts/browser_kernel/latest/ - latest-only Markdown, HTML, and audit data under docs/reports/ (ignored by Git) The complete run and report are code-driven: `run.sh` writes raw @@ -47,11 +47,11 @@ The complete run and report are code-driven: `run.sh` writes raw To recalculate all metrics and HTML from existing raw attempts without running the browsers again: - uv run python examples/browser_use/rebuild_report.py + uv run python examples/browser_kernel/rebuild_report.py Independent raw-data audit (does not import the aggregation implementation): - uv run python examples/browser_use/verify_metrics.py --artifacts artifacts/browser_use/latest + uv run python examples/browser_kernel/verify_metrics.py --artifacts artifacts/browser_kernel/latest `run.sh` runs this audit automatically. It checks correctness counts, the successful-task intersection, 81 aggregate/ratio metric groups, and every diff --git a/examples/browser_use/agent_smoke.json b/examples/browser_kernel/agent_smoke.json similarity index 100% rename from examples/browser_use/agent_smoke.json rename to examples/browser_kernel/agent_smoke.json diff --git a/examples/browser_use/build_public_suite.py b/examples/browser_kernel/build_public_suite.py similarity index 100% rename from examples/browser_use/build_public_suite.py rename to examples/browser_kernel/build_public_suite.py diff --git a/examples/browser_use/docker/chromium.Dockerfile b/examples/browser_kernel/docker/chromium.Dockerfile similarity index 85% rename from examples/browser_use/docker/chromium.Dockerfile rename to examples/browser_kernel/docker/chromium.Dockerfile index 3c1cec15c..20b02eafd 100644 --- a/examples/browser_use/docker/chromium.Dockerfile +++ b/examples/browser_kernel/docker/chromium.Dockerfile @@ -6,7 +6,7 @@ RUN apt-get update \ && rm -rf /var/lib/apt/lists/* \ && useradd --create-home --uid 10001 browser -COPY examples/browser_use/docker/start-chromium.sh /usr/local/bin/start-browser +COPY examples/browser_kernel/docker/start-chromium.sh /usr/local/bin/start-browser RUN chmod 0755 /usr/local/bin/start-browser USER browser diff --git a/examples/browser_use/docker/moli.Dockerfile b/examples/browser_kernel/docker/moli.Dockerfile similarity index 76% rename from examples/browser_use/docker/moli.Dockerfile rename to examples/browser_kernel/docker/moli.Dockerfile index 2c7c1bca6..1970441de 100644 --- a/examples/browser_use/docker/moli.Dockerfile +++ b/examples/browser_kernel/docker/moli.Dockerfile @@ -1,18 +1,19 @@ FROM debian:bookworm-slim@sha256:88200866dfff7ea7f5cbcb6ec7c8a701889efe6fe859fe64d6990e4b07ea4171 ARG TARGETARCH -ARG MOLI_VERSION=v1.0.6 -ARG MOLI_AMD64_SHA256=8c265be9ae49d77b6f86d38fadebe8da6e6d4dd1e5f7894c1819dba737721b91 -ARG MOLI_ARM64_SHA256=c64e7cfdb7ee314b0bf9eeaafcec128020a64a4cea592f576dcb8f9c12b3c2f5 +ARG MOLI_VERSION=v1.1.3 +ARG MOLI_AMD64_SHA256=e1f923f7fb06a684721e24a6c65c20567eaf81388a2225d1a9a175fc6c305df3 +ARG MOLI_ARM64_SHA256=f65ce90c38fdfeaa73b7e1d491823ec6d19371e5b74dbe35b43c8d61f0a8f6b7 RUN apt-get update \ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \ ca-certificates curl fontconfig fonts-liberation fonts-noto-cjk \ libcurl4 libgcc-s1 libstdc++6 tar \ - && case "$TARGETARCH" in \ + && resolved_arch="${TARGETARCH:-$(dpkg --print-architecture)}" \ + && case "$resolved_arch" in \ amd64) asset="moli-x86_64-unknown-linux-gnu.tar.gz"; sha="$MOLI_AMD64_SHA256" ;; \ arm64) asset="moli-aarch64-unknown-linux-gnu.tar.gz"; sha="$MOLI_ARM64_SHA256" ;; \ - *) echo "unsupported TARGETARCH=$TARGETARCH" >&2; exit 2 ;; \ + *) echo "unsupported architecture=$resolved_arch" >&2; exit 2 ;; \ esac \ && curl --proto '=https' --tlsv1.2 -fsSL \ "https://github.com/lexmount/moli/releases/download/${MOLI_VERSION}/${asset}" \ diff --git a/examples/browser_use/docker/start-chromium.sh b/examples/browser_kernel/docker/start-chromium.sh similarity index 88% rename from examples/browser_use/docker/start-chromium.sh rename to examples/browser_kernel/docker/start-chromium.sh index a472dbab2..6b02f8a74 100755 --- a/examples/browser_use/docker/start-chromium.sh +++ b/examples/browser_kernel/docker/start-chromium.sh @@ -13,6 +13,9 @@ chromium \ --no-default-browser-check \ --disable-background-networking \ --disable-component-update \ + --disable-breakpad \ + --disable-crash-reporter \ + --disable-crashpad \ --remote-debugging-address=0.0.0.0 \ --remote-debugging-port=9222 \ --user-data-dir=/tmp/chromium-profile \ diff --git a/examples/browser_use/fixtures/app.html b/examples/browser_kernel/fixtures/app.html similarity index 100% rename from examples/browser_use/fixtures/app.html rename to examples/browser_kernel/fixtures/app.html diff --git a/examples/browser_use/fixtures/catalog.html b/examples/browser_kernel/fixtures/catalog.html similarity index 100% rename from examples/browser_use/fixtures/catalog.html rename to examples/browser_kernel/fixtures/catalog.html diff --git a/examples/browser_use/fixtures/core.html b/examples/browser_kernel/fixtures/core.html similarity index 100% rename from examples/browser_use/fixtures/core.html rename to examples/browser_kernel/fixtures/core.html diff --git a/examples/browser_use/fixtures/dynamic.html b/examples/browser_kernel/fixtures/dynamic.html similarity index 100% rename from examples/browser_use/fixtures/dynamic.html rename to examples/browser_kernel/fixtures/dynamic.html diff --git a/examples/browser_use/fixtures/forms.html b/examples/browser_kernel/fixtures/forms.html similarity index 100% rename from examples/browser_use/fixtures/forms.html rename to examples/browser_kernel/fixtures/forms.html diff --git a/examples/browser_use/fixtures/interaction-space.html b/examples/browser_kernel/fixtures/interaction-space.html similarity index 100% rename from examples/browser_use/fixtures/interaction-space.html rename to examples/browser_kernel/fixtures/interaction-space.html diff --git a/examples/browser_use/fixtures/page2.html b/examples/browser_kernel/fixtures/page2.html similarity index 100% rename from examples/browser_use/fixtures/page2.html rename to examples/browser_kernel/fixtures/page2.html diff --git a/examples/browser_use/fixtures/page3.html b/examples/browser_kernel/fixtures/page3.html similarity index 100% rename from examples/browser_use/fixtures/page3.html rename to examples/browser_kernel/fixtures/page3.html diff --git a/examples/browser_use/ground_truth_validation.json b/examples/browser_kernel/ground_truth_validation.json similarity index 100% rename from examples/browser_use/ground_truth_validation.json rename to examples/browser_kernel/ground_truth_validation.json diff --git a/examples/browser_use/rebuild_report.py b/examples/browser_kernel/rebuild_report.py similarity index 84% rename from examples/browser_use/rebuild_report.py rename to examples/browser_kernel/rebuild_report.py index eb36344a3..23f82678d 100644 --- a/examples/browser_use/rebuild_report.py +++ b/examples/browser_kernel/rebuild_report.py @@ -7,20 +7,20 @@ import json from pathlib import Path -from harness.browser_use.diagnostics import diagnose_summary -from harness.browser_use.metrics import summarize_attempts -from harness.browser_use.report import write_latest_report -from harness.browser_use.suite import load_suite, validate_ground_truth_evidence +from harness.browser_kernel.diagnostics import diagnose_summary +from harness.browser_kernel.metrics import summarize_attempts +from harness.browser_kernel.report import write_latest_report +from harness.browser_kernel.suite import load_suite, validate_ground_truth_evidence def main() -> None: parser = argparse.ArgumentParser() - parser.add_argument("--artifacts", default="artifacts/browser_use/latest") + parser.add_argument("--artifacts", default="artifacts/browser_kernel/latest") parser.add_argument("--reports", default="docs/reports") - parser.add_argument("--suite", default="examples/browser_use/tasks.v4.json") + parser.add_argument("--suite", default="examples/browser_kernel/tasks.v4.json") parser.add_argument( "--ground-truth-evidence", - default="examples/browser_use/ground_truth_validation.json", + default="examples/browser_kernel/ground_truth_validation.json", ) args = parser.parse_args() @@ -75,7 +75,7 @@ def main() -> None: json.dumps(manifest, ensure_ascii=False, indent=2) + "\n", encoding="utf-8" ) write_latest_report(args.reports, manifest=manifest, summary=summary) - report_path = Path(args.reports) / "browser-use-evaluation-latest.html" + report_path = Path(args.reports) / "browser-kernel-evaluation-latest.html" print(f"Recalculated {len(rows)} attempts; report: {report_path}") diff --git a/examples/browser_use/run.sh b/examples/browser_kernel/run.sh similarity index 52% rename from examples/browser_use/run.sh rename to examples/browser_kernel/run.sh index 2d92ae7cc..fb8250c56 100755 --- a/examples/browser_use/run.sh +++ b/examples/browser_kernel/run.sh @@ -2,8 +2,6 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -PYTHON="${BROWSER_USE_EVAL_PYTHON:-python3}" -VENV="${BROWSER_USE_EVAL_VENV:-${ROOT}/.browser-use-eval-venv}" FAILURE_RETRIES="${BROWSER_USE_EVAL_FAILURE_RETRIES:-1}" SAMPLE_MS="${BROWSER_USE_EVAL_SAMPLE_INTERVAL_MS:-100}" SETTLE_MS="${BROWSER_USE_EVAL_SETTLE_MS:-1500}" @@ -13,44 +11,24 @@ BUILD_IMAGES="${BROWSER_USE_EVAL_BUILD_IMAGES:-1}" LLM_DIAGNOSTICS="${BROWSER_USE_EVAL_LLM_DIAGNOSTICS:-0}" DIAGNOSIS_MODEL="${BROWSER_USE_EVAL_DIAGNOSIS_MODEL:-gpt-5.6-sol}" DIAGNOSIS_EFFORT="${BROWSER_USE_EVAL_DIAGNOSIS_REASONING_EFFORT:-low}" -OUTPUT="${BROWSER_USE_EVAL_OUTPUT_DIR:-${ROOT}/artifacts/browser_use/latest}" +OUTPUT="${BROWSER_USE_EVAL_OUTPUT_DIR:-${ROOT}/artifacts/browser_kernel/latest}" REPORTS="${ROOT}/docs/reports" command -v docker >/dev/null 2>&1 || { echo "docker is required" >&2; exit 2; } -command -v "${PYTHON}" >/dev/null 2>&1 || { echo "${PYTHON} is required" >&2; exit 2; } +command -v uv >/dev/null 2>&1 || { echo "uv is required" >&2; exit 2; } docker info >/dev/null -if ! "${PYTHON}" -m venv --help >/dev/null 2>&1; then - echo "Python venv support is required (Ubuntu/Debian: sudo apt-get install python3-venv)" >&2 - exit 2 -fi - mkdir -p "${OUTPUT}" "${REPORTS}" find "${OUTPUT}" -mindepth 1 -maxdepth 1 -type f -delete if [[ "${BUILD_IMAGES}" == "1" ]]; then - docker build -f "${ROOT}/examples/browser_use/docker/chromium.Dockerfile" \ + docker build -f "${ROOT}/examples/browser_kernel/docker/chromium.Dockerfile" \ -t lex-browser-use-eval-chromium:local "${ROOT}" - docker build -f "${ROOT}/examples/browser_use/docker/moli.Dockerfile" \ + docker build -f "${ROOT}/examples/browser_kernel/docker/moli.Dockerfile" \ -t lex-browser-use-eval-moli:local "${ROOT}" fi -if command -v uv >/dev/null 2>&1; then - RUNNER=(uv run --extra browser-use-eval python) -else - if [[ ! -x "${VENV}/bin/python" ]]; then - if ! "${PYTHON}" -m venv "${VENV}"; then - echo "Failed to create venv (Ubuntu/Debian: sudo apt-get install python3-venv)" >&2 - exit 2 - fi - fi - if ! "${VENV}/bin/python" -m pip --version >/dev/null 2>&1; then - "${PYTHON}" -m ensurepip --upgrade - fi - "${VENV}/bin/python" -m pip install --disable-pip-version-check -q \ - -e "${ROOT}[browser-use-eval]" - RUNNER=("${VENV}/bin/python") -fi +RUNNER=(uv run --frozen --extra browser-use-eval python) EXTRA_ARGS=(--diagnosis-model "${DIAGNOSIS_MODEL}" \ --diagnosis-reasoning-effort "${DIAGNOSIS_EFFORT}") @@ -58,11 +36,11 @@ if [[ "${LLM_DIAGNOSTICS}" == "1" ]]; then EXTRA_ARGS+=(--llm-diagnostics) fi -"${RUNNER[@]}" -m harness.browser_use.cli \ - --suite "${ROOT}/examples/browser_use/tasks.v4.json" \ +"${RUNNER[@]}" -m harness.browser_kernel.cli \ + --suite "${ROOT}/examples/browser_kernel/tasks.v4.json" \ --output "${OUTPUT}" \ --reports "${REPORTS}" \ - --ground-truth-evidence "${ROOT}/examples/browser_use/ground_truth_validation.json" \ + --ground-truth-evidence "${ROOT}/examples/browser_kernel/ground_truth_validation.json" \ --failure-retries "${FAILURE_RETRIES}" \ --sample-interval-ms "${SAMPLE_MS}" \ --settle-ms "${SETTLE_MS}" \ @@ -70,8 +48,8 @@ fi --memory "${MEMORY}" \ "${EXTRA_ARGS[@]}" -"${RUNNER[@]}" "${ROOT}/examples/browser_use/verify_metrics.py" --artifacts "${OUTPUT}" +"${RUNNER[@]}" "${ROOT}/examples/browser_kernel/verify_metrics.py" --artifacts "${OUTPUT}" echo "Raw artifacts: ${OUTPUT}" -echo "Latest report: ${REPORTS}/browser-use-evaluation-latest.html" +echo "Latest report: ${REPORTS}/browser-kernel-evaluation-latest.html" echo "All metrics and HTML above were generated by repository code; Codex is not part of scoring or calculation." diff --git a/examples/browser_use/tasks.v2.json b/examples/browser_kernel/tasks.v2.json similarity index 100% rename from examples/browser_use/tasks.v2.json rename to examples/browser_kernel/tasks.v2.json diff --git a/examples/browser_use/tasks.v3.json b/examples/browser_kernel/tasks.v3.json similarity index 100% rename from examples/browser_use/tasks.v3.json rename to examples/browser_kernel/tasks.v3.json diff --git a/examples/browser_use/tasks.v4.json b/examples/browser_kernel/tasks.v4.json similarity index 100% rename from examples/browser_use/tasks.v4.json rename to examples/browser_kernel/tasks.v4.json diff --git a/examples/browser_use/verify_metrics.py b/examples/browser_kernel/verify_metrics.py similarity index 99% rename from examples/browser_use/verify_metrics.py rename to examples/browser_kernel/verify_metrics.py index 30fd79847..201550b0f 100644 --- a/examples/browser_use/verify_metrics.py +++ b/examples/browser_kernel/verify_metrics.py @@ -137,5 +137,5 @@ def value(task, engine, metric): if __name__ == "__main__": parser = argparse.ArgumentParser() - parser.add_argument("--artifacts", type=Path, default=Path("artifacts/browser_use/latest")) + parser.add_argument("--artifacts", type=Path, default=Path("artifacts/browser_kernel/latest")) print(json.dumps(verify(parser.parse_args().artifacts))) diff --git a/examples/web_fetch/README.md b/examples/web_fetch/README.md index 63c817638..aa9afc279 100644 --- a/examples/web_fetch/README.md +++ b/examples/web_fetch/README.md @@ -1,5 +1,5 @@ # Web Fetch Examples Versioned Web Fetch suites and one-command runners belong here. This capability -is intentionally separate from `examples/browser_use` and is not implemented in +is intentionally separate from `examples/browser_kernel` and is not implemented in the first Browser Use evaluation delivery. diff --git a/harness/browser_agent/__init__.py b/harness/browser_agent/__init__.py new file mode 100644 index 000000000..9aaad1311 --- /dev/null +++ b/harness/browser_agent/__init__.py @@ -0,0 +1,5 @@ +"""Autonomous browser-agent evaluation built on WebArena-Verified.""" + +from .selection import PILOT_SCHEMA, PilotTask, load_pilot + +__all__ = ["PILOT_SCHEMA", "PilotTask", "load_pilot"] diff --git a/harness/browser_agent/model.py b/harness/browser_agent/model.py new file mode 100644 index 000000000..848a0e54a --- /dev/null +++ b/harness/browser_agent/model.py @@ -0,0 +1,189 @@ +"""OpenAI Responses transport adapter for CowAgent's model protocol.""" + +from __future__ import annotations + +import json +from collections.abc import Iterable +from typing import Any + +import requests + + +def _text_from_blocks(content: Any) -> str: + if isinstance(content, str): + return content + if not isinstance(content, list): + return "" + return "\n".join( + str(block.get("text") or "") + for block in content + if isinstance(block, dict) and block.get("type") == "text" + ) + + +def cow_messages_to_responses(messages: list[dict[str, Any]]) -> list[dict[str, Any]]: + """Translate CowAgent's Claude-shaped history to Responses input items.""" + items: list[dict[str, Any]] = [] + for message in messages: + role = str(message.get("role") or "user") + content = message.get("content") + text = _text_from_blocks(content) + if text: + items.append({"role": role, "content": text}) + if not isinstance(content, list): + continue + for block in content: + if not isinstance(block, dict): + continue + if block.get("type") == "tool_use": + items.append( + { + "type": "function_call", + "call_id": str(block.get("id") or ""), + "name": str(block.get("name") or ""), + "arguments": json.dumps( + block.get("input") or {}, ensure_ascii=False, separators=(",", ":") + ), + } + ) + elif block.get("type") == "tool_result": + output = block.get("content") + if not isinstance(output, str): + output = json.dumps(output, ensure_ascii=False) + items.append( + { + "type": "function_call_output", + "call_id": str(block.get("tool_use_id") or ""), + "output": output, + } + ) + return items + + +def cow_tools_to_responses(tools: list[dict[str, Any]] | None) -> list[dict[str, Any]]: + return [ + { + "type": "function", + "name": str(tool["name"]), + "description": str(tool.get("description") or ""), + "parameters": tool.get("input_schema") or {"type": "object", "properties": {}}, + "strict": False, + } + for tool in tools or [] + ] + + +def response_to_chat_chunk(response: dict[str, Any]) -> dict[str, Any]: + content: list[str] = [] + calls: list[dict[str, Any]] = [] + for item in response.get("output") or []: + if item.get("type") == "message": + for block in item.get("content") or []: + if block.get("type") == "output_text" and block.get("text"): + content.append(str(block["text"])) + elif item.get("type") == "function_call": + calls.append( + { + "index": len(calls), + "id": str(item.get("call_id") or item.get("id") or ""), + "type": "function", + "function": { + "name": str(item.get("name") or ""), + "arguments": str(item.get("arguments") or "{}"), + }, + } + ) + delta: dict[str, Any] = {"content": "".join(content)} + if calls: + delta["tool_calls"] = calls + return { + "choices": [ + { + "delta": delta, + "finish_reason": "tool_calls" if calls else "stop", + } + ], + "usage": response.get("usage"), + "response_id": response.get("id"), + } + + +class AstraResponsesModel: + """Minimal adapter: CowAgent loop and tools stay unchanged; only transport differs.""" + + def __init__( + self, + *, + api_key: str, + base_url: str = "https://api.openai.com/v1", + model: str = "gpt-6-astra", + reasoning_effort: str = "low", + timeout_seconds: float = 180, + session: requests.Session | None = None, + ) -> None: + if reasoning_effort != "low": + raise ValueError("pilot protocol fixes GPT-6 Astra reasoning effort to low") + self.model = model + self.api_key = api_key + self.base_url = base_url.rstrip("/") + self.reasoning_effort = reasoning_effort + self.timeout_seconds = timeout_seconds + self.session = session or requests.Session() + self.last_response: dict[str, Any] | None = None + self._previous_response_id: str | None = None + self._seen_message_count = 0 + self.usage: dict[str, int] = {} + self.request_count = 0 + + def reset(self) -> None: + """Start a fresh Responses conversation for the next benchmark attempt.""" + self.last_response = None + self._previous_response_id = None + self._seen_message_count = 0 + self.usage = {} + self.request_count = 0 + + def _normalized_reasoning_effort(self) -> str: + return self.reasoning_effort + + def call_stream(self, request: Any) -> Iterable[dict[str, Any]]: + messages = list(request.messages or []) + if self._previous_response_id: + new_messages = messages[self._seen_message_count :] + input_items = cow_messages_to_responses( + [message for message in new_messages if message.get("role") != "assistant"] + ) + else: + input_items = cow_messages_to_responses(messages) + payload: dict[str, Any] = { + "model": self.model, + "instructions": str(getattr(request, "system", "") or ""), + "input": input_items, + "reasoning": {"effort": self.reasoning_effort}, + "tools": cow_tools_to_responses(request.tools), + } + if self._previous_response_id: + payload["previous_response_id"] = self._previous_response_id + if getattr(request, "max_tokens", None): + payload["max_output_tokens"] = int(request.max_tokens) + response = self.session.post( + f"{self.base_url}/responses", + headers={"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"}, + json=payload, + timeout=self.timeout_seconds, + ) + response.raise_for_status() + self.last_response = response.json() + self.request_count += 1 + usage = self.last_response.get("usage") or {} + details = usage.get("output_tokens_details") or {} + for key, value in ( + ("input_tokens", usage.get("input_tokens")), + ("output_tokens", usage.get("output_tokens")), + ("reasoning_tokens", details.get("reasoning_tokens")), + ("total_tokens", usage.get("total_tokens")), + ): + self.usage[key] = self.usage.get(key, 0) + int(value or 0) + self._previous_response_id = str(self.last_response["id"]) + self._seen_message_count = len(messages) + yield response_to_chat_chunk(self.last_response) diff --git a/harness/browser_agent/report.py b/harness/browser_agent/report.py new file mode 100644 index 000000000..d621d5dd5 --- /dev/null +++ b/harness/browser_agent/report.py @@ -0,0 +1,227 @@ +"""Publication-ready Chinese report for the autonomous browser-agent pilot.""" + +from __future__ import annotations + +import argparse +import html +import json +import math +import statistics +from collections import defaultdict +from pathlib import Path +from typing import Any + +from .selection import load_pilot + + +def _e(value: Any) -> str: + return html.escape(str(value), quote=True) + + +def _read_attempts(evidence: Path) -> list[dict[str, Any]]: + path = evidence / "attempts.jsonl" + if not path.exists(): + return [] + return [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line] + + +def _primary_summary(rows: list[dict[str, Any]]) -> dict[str, dict[str, Any]]: + result: dict[str, dict[str, Any]] = {} + for engine in ("chromium", "moli"): + primary = [row for row in rows if row["engine"] == engine and row["attempt"] == 1] + retries = [row for row in rows if row["engine"] == engine and row["attempt"] == 2] + durations = [float(row["duration_ms"]) for row in primary] + tokens = [int((row.get("model_usage") or {}).get("total_tokens") or 0) for row in primary] + result[engine] = { + "completed": len(primary), + "passed": sum(float(row["score"]) == 1.0 for row in primary), + "score": sum(float(row["score"]) for row in primary) / len(primary) if primary else None, + "retry_count": len(retries), + "recovered": sum(float(row["score"]) == 1.0 for row in retries), + "median_seconds": statistics.median(durations) / 1000 if durations else None, + "median_tokens": statistics.median(tokens) if tokens else None, + } + return result + + +def _paired_summary(rows: list[dict[str, Any]]) -> dict[str, Any]: + primary = { + (str(row["engine"]), int(row["task_id"])): float(row["score"]) == 1.0 + for row in rows + if int(row["attempt"]) == 1 + } + paired_ids = sorted( + task_id + for task_id in {task_id for _, task_id in primary} + if ("chromium", task_id) in primary and ("moli", task_id) in primary + ) + counts = {"both_pass": 0, "chromium_only": 0, "moli_only": 0, "both_fail": 0} + for task_id in paired_ids: + chromium = primary[("chromium", task_id)] + moli = primary[("moli", task_id)] + key = ( + "both_pass" + if chromium and moli + else "chromium_only" + if chromium + else "moli_only" + if moli + else "both_fail" + ) + counts[key] += 1 + discordant = counts["chromium_only"] + counts["moli_only"] + if discordant: + smaller = min(counts["chromium_only"], counts["moli_only"]) + tail = sum(math.comb(discordant, k) for k in range(smaller + 1)) / 2**discordant + p_value = min(1.0, 2 * tail) + else: + p_value = 1.0 if paired_ids else None + return {"paired": len(paired_ids), **counts, "mcnemar_exact_p": p_value} + + +def _pct(value: float | None) -> str: + return "—" if value is None else f"{value * 100:.1f}%" + + +def _seconds(value: float | None) -> str: + return "—" if value is None else f"{value:.1f} s" + + +def _table(headers: list[str], rows: list[list[Any]]) -> str: + head = "".join(f"{_e(item)}" for item in headers) + body = "".join( + "" + "".join(f"{item if isinstance(item, Safe) else _e(item)}" for item in row) + "" + for row in rows + ) + return f'
{head}{body}
' + + +class Safe(str): + pass + + +def _result_section(attempts: list[dict[str, Any]]) -> str: + summary = _primary_summary(attempts) + paired = _paired_summary(attempts) + rows = [] + for engine, label in (("chromium", "Chromium"), ("moli", "Moli 1.1.3")): + item = summary[engine] + rows.append( + [ + label, + f"{item['completed']}/30", + f"{item['passed']}/{item['completed']}" if item["completed"] else "—", + _pct(item["score"]), + f"{item['recovered']}/{item['retry_count']}" if item["retry_count"] else "—", + _seconds(item["median_seconds"]), + f"{item['median_tokens']:,.0f}" if item["median_tokens"] is not None else "—", + ] + ) + complete = all(item["completed"] == 30 for item in summary.values()) + notice = ( + '

测量完整。主分只使用每个浏览器每题的第一次正式执行;失败后的唯一一次重试只进入恢复诊断。

' + if complete + else '

测量尚未完整。本页可以先审阅选型与方法,但在两种浏览器各完成 30 个首轮样本前,不发布比较结论。

' + ) + paired_text = ( + f"完整配对 {paired['paired']}/30 题:两者都通过 {paired['both_pass']},仅 Chromium 通过 {paired['chromium_only']},仅 Moli 通过 {paired['moli_only']},两者都失败 {paired['both_fail']};精确 McNemar 双侧 p={paired['mcnemar_exact_p']:.3f}。" + if paired["paired"] == 30 + else f"当前完整配对 {paired['paired']}/30 题;未完成前不做显著性解释。" + ) + return f"""

实测结果

同一 Agent、同一模型,只替换浏览器

{notice}{_table(['浏览器','首轮覆盖','首轮通过','官方主分','失败重试恢复','首轮耗时中位数','首轮 tokens 中位数'], rows)}

{paired_text}

官方分数来自未修改的 WebArena‑Verified 1.2.3 evaluator。它是结果验收,不要求 Agent 复刻某条参考点击路径。McNemar 检验只比较同一题上的不一致结果;30 题仍是 pilot,p 值不能替代效应大小或更大样本复验。执行轨迹、网络 trace、最终 JSON、模型 token 用量和 evaluator 明细保存在仓库外的持久证据目录;报告不公开登录 Cookie 或完整网络正文。

""" + + +def _tier_section(attempts: list[dict[str, Any]]) -> str: + grouped: dict[tuple[str, str], list[dict[str, Any]]] = defaultdict(list) + for row in attempts: + if row["attempt"] == 1: + grouped[(row["engine"], row["tier"])].append(row) + rows: list[list[Any]] = [] + for tier, label in (("easy", "简单:1–4 步"), ("medium", "中等:5–7 步"), ("hard", "困难:8 步以上")): + line: list[Any] = [label] + for engine in ("chromium", "moli"): + items = grouped[(engine, tier)] + line.append(f"{sum(float(x['score']) == 1 for x in items)}/{len(items)}" if items else "—") + rows.append(line) + return f"""

难度不是官方标签,是预注册的操作长度分层

WebArena‑Verified 只发布一个 258 题 hard subset,并没有 easy/medium/hard 三档。为了让首轮同时覆盖简单、中等和困难任务,本项目在模型运行前按“完成目标所需的最少可见 UI 操作数”预注册三档:1–4、5–7、8+。这衡量的是操作链长度,不等于推理难度;涉及跨页聚合、条件筛选或提交修改的任务即使点击少,也可能更难。

{_table(['本项目分层','Chromium 首轮','Moli 首轮'], rows)}

30 题配额为 5/10/15;其中个别本项目“简单”题也属于官方 hard subset,两种分类回答的是不同问题,报告保留两者而不互相覆盖。

""" + + +def _task_table(selection_path: Path, attempts: list[dict[str, Any]]) -> str: + selection = load_pilot(selection_path) + score = {(row["engine"], row["task_id"]): row for row in attempts if row["attempt"] == 1} + rows = [] + for task in selection.tasks: + rows.append( + [ + task.task_id, + {"easy": "简单", "medium": "中等", "hard": "困难"}[task.tier], + task.estimated_min_steps, + "是" if task.official_hard else "否", + task.rationale, + _pct(float(score[("chromium", task.task_id)]["score"])) if ("chromium", task.task_id) in score else "—", + _pct(float(score[("moli", task.task_id)]["score"])) if ("moli", task.task_id) in score else "—", + ] + ) + return f"""

预注册的 30 题

选择文件在任何模型调用前固定,失败后不换题。样本含 23 个 RETRIEVE、4 个 MUTATE 和 3 个 NAVIGATE;按任务涉及站点计,Shopping Admin 12 题、Shopping 7 题、GitLab 6 题、Reddit 5 题。站点限定为这四个官方自托管环境;Wikipedia 与 Map 首轮排除,不是因为任务“太难”,而是它们分别要求大型 ZIM 和多套地图数据卷,会把环境下载与存储问题混进浏览器对比。

{_table(['ID','本项目层级','估计最少步','官方 hard','入选理由','Chromium','Moli'], rows)}
""" + + +def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: + attempts = _read_attempts(evidence) + output.mkdir(parents=True, exist_ok=True) + css = """ +:root{--ink:#17202a;--muted:#667085;--paper:#fbfaf7;--card:#fff;--line:#dedbd2;--accent:#155eef;--soft:#eef4ff;--good:#eaf8ef;--warn:#fff6df}*{box-sizing:border-box}html,body{overflow-x:hidden}body{margin:0;background:var(--paper);color:var(--ink);font:16px/1.72 ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}main{width:100%;max-width:1100px;margin:auto;padding:64px 28px 100px}header{border-bottom:1px solid var(--line);padding-bottom:42px;margin-bottom:42px}h1{font:700 clamp(38px,6vw,72px)/1.06 ui-serif,Georgia,serif;letter-spacing:-.035em;max-width:920px;margin:10px 0 22px}h2{font:700 30px/1.25 ui-serif,Georgia,serif;margin:0 0 16px}h1,h2,h3,p,li{overflow-wrap:anywhere}h3{margin:28px 0 8px}section{background:var(--card);border:1px solid var(--line);border-radius:18px;padding:30px;margin:22px 0;box-shadow:0 8px 30px rgba(31,41,55,.035)}p{max-width:88ch}.lede{font-size:20px;color:#344054}.eyebrow{color:var(--accent);font-weight:750;letter-spacing:.08em;text-transform:uppercase;font-size:13px}.meta,.fine{color:var(--muted);font-size:14px}.callout{border-left:4px solid var(--accent);padding:13px 16px;background:var(--soft);border-radius:6px}.callout.good{background:var(--good)}.callout.warn{background:var(--warn);border-color:#d68a00}.table-wrap{max-width:100%;overflow:auto;margin:20px 0}table{border-collapse:collapse;width:100%;font-size:14px}th,td{text-align:left;vertical-align:top;border-bottom:1px solid var(--line);padding:11px 12px}th{background:#f5f3ee;white-space:nowrap}a{color:var(--accent)}code{background:#f1f2f4;padding:2px 5px;border-radius:5px}.rank{display:grid;grid-template-columns:48px minmax(0,1fr);gap:14px;padding:18px 0;border-top:1px solid var(--line)}.rank>div{min-width:0}.rank b{font:700 30px/1 Georgia,serif;color:var(--accent)}nav{display:flex;gap:16px;flex-wrap:wrap;margin-top:24px}nav a{text-decoration:none;font-size:14px}@media(max-width:650px){main{padding:36px 14px}section{padding:21px}h1{font-size:34px}.lede{font-size:18px}h2{font-size:25px}}@media print{body{background:#fff}main{max-width:none;padding:0}section{break-inside:avoid;box-shadow:none}nav{display:none}a{color:inherit;text-decoration:none}} +""" + methodology = _table( + ["Benchmark", "评什么", "验收对象", "判分方式", "主要问题"], + [ + ["WebArena‑Verified", "自托管站点上的真实交互", "最终 JSON;需要时加网络事件", "确定性程序 evaluator;无 LLM judge;不比对固定动作序列", "部署重,站点状态必须逐次重置;仍可能有浏览器协议兼容问题"], + ["Online‑Mind2Web", "136 个真实网站的在线任务", "事实动作历史与最终完成情况", "官方建议 o4-mini WebJudge,并提供人工标签;属于 LLM-as-a-judge", "网页漂移、CAPTCHA、地区与账号差异;复现成本高"], + ["WebVoyager", "少量主流实时网站上的视觉交互", "最终回答与执行证据", "原始工作流使用多模态 LLM 自动评审并辅以人工核验", "judge 偏差和实时网站漂移,精确复现较弱"], + ["Mind2Web(2023)", "离线网页快照上的动作预测与泛化", "每一步目标元素与操作", "程序化 element accuracy、operation F1、step success、整任务 success", "主要评过程模仿,不证明今天能在真实网站完成任务"], + ["Mind2Web 2", "长链、多站点、时变的 agentic search", "带引用的研究答案", "树状 rubric 的 Agent-as-a-Judge;Partial Completion、Success、Pass@3", "更接近研究/搜索,不是浏览器 UI 内核对比的首选"], + ["BU Bench V1", "100 个混合来源的浏览器任务", "最终结果与运行轨迹", "官方仓库要求 Google judge LLM;二元分", "单一维护方、自定义题占比、judge 与云浏览器条件会影响可比性"], + ["Odysseys", "200 个真实互联网长链、多站任务", "最终产物、轨迹与平均 6.1 个任务 rubric", "按细粒度 rubric 做模型评审;报告 partial、perfect 与每步效率", "实时网页漂移,运行与评审成本高;更适合第二阶段长链能力"], + ], + ) + html_text = f"""Browser Use 公开评测选型与 WebArena‑Verified 首轮对比

Browser Agent Evaluation · 2026-09-08

Browser Use 公开评测怎么选,以及为什么首轮用 WebArena‑Verified

结论先说:原有 30 题应保留,但它测的是固定动作下浏览器内核能否正确、快速执行,不是 CowAgent 的自主规划能力。新增轨道用真实 CowAgent + GPT‑6 Astra(reasoning=low),在同一组 WebArena‑Verified 任务上只替换 Chromium/Moli,以官方程序化 evaluator 判最终结果。

+

采用统计与决策

公开采用次数 Top 3:Mind2Web、WebArena、WebVoyager

统计单位是公开一手材料中采用过该家族的机构数,而不是论文引用量;同一机构重复发布只计一次。Mind2Web 家族为 5 家(OpenAI、Anthropic、Google DeepMind、THUDM / ChatGLM、Browser Use),WebArena 家族为 3 家(OpenAI、Anthropic、THUDM / ChatGLM),WebVoyager 为 2 家(OpenAI、Google DeepMind)。不同变体的成绩不能合并,这里的聚合只回答“有多少机构采用过这个家族”。

1

Mind2Web 家族 · 5 家

采用最广,但家族内部差异最大:原版是离线动作预测,Online 版是真实网站 + WebJudge,Mind2Web 2 是长链研究任务 + Agent-as-a-Judge。适合作为第二阶段真实互联网泛化评测,不能把三者成绩合并。

2

WebArena 家族 · 3 家

覆盖导航、检索和改写状态;自托管让 Chromium/Moli 面对同一份站点数据。首轮仍选它的 Verified 版本,因为它重新审计任务、答案和 evaluator,并使用确定性程序判分。这里是“实验适配性”胜过“采用数量”。

3

WebVoyager · 2 家

经典视觉浏览器基准,优势是直观、接近用户网站;问题是实时网页和 LLM judge 同时引入波动,作为内核 A/B 首轮不如 WebArena‑Verified 稳定。

为什么不是照着 Astra 发布页抄 benchmark:GPT‑6 Astra 的公开 computer-use 主表是 Agents’ Last Exam、OSWorld 2.0 和 ScreenSpot‑Pro;页面只给出 Mind2Web 的 1.9× 演示耗时对比,没有公开 WebArena‑Verified 分数。这些基准能说明模型的通用电脑操作能力,但 OSWorld/ScreenSpot 并不是纯 browser use,也不适合直接比较两个浏览器内核。

+

Benchmark taxonomy

它们评最终结果,还是评过程?

{methodology}

WebArena‑Verified 的精确判分链

官方 1.2.3 数据共有 812 题,812 题都配置 AgentResponseEvaluator,其中 663 题还配置 NetworkEventEvaluator。前者解析 Agent 最终 JSON,核对任务类型、状态和带类型的返回字段;后者从 Playwright trace 或 HAR 中提取请求、响应、Cookie 等事件,核对导航或状态改变是否实际发生。字段先按官方 schema 做 URL、字符串、数字、布尔、数组和对象规范化,再做结构比较;需要有序时保留顺序,允许的备选答案由任务定义声明。没有模糊子串放行,也没有 LLM judge。

单题是二元分:该题配置的每一个 evaluator 都得到 1.0 才通过,任何一项失败或 evaluator 报错都记 0。因此它评的是最终结果与必要的可观察副作用,不是“按参考路径逐步打分”。Agent 可以走不同点击路径,只要完成的结果和必要事件符合任务契约即可。

+

Mind2Web 的 easy / medium / hard 要先纠正

原始 Mind2Web 的官方拆分不是难度档,而是 cross-task、cross-website、cross-domain:测试任务是否见过同站任务、是否换了网站、是否换了整个领域。Online‑Mind2Web 官方也没有稳定的 easy/medium/hard 定义;有些第三方按参考动作长度再分档,那是第三方分析口径。Mind2Web 2 论文把任务 horizon 定义为 short(少于 10 次动作)、medium(10–50)、long(超过 50),仍不等同于 easy/medium/hard。

所以本轮的 5/10/15 是我们自己的预注册操作长度层级。它只用于保证样本覆盖,不改写官方字段,也不把它当成跨论文可比较的难度标签。

+

Browser Use 官方现在更偏向什么

要区分三种信号。Browser Use 产品首页强调开放的 100 题 BU Bench V1,同时展示 Odysseys 的 200 题长链排行榜成绩;其 benchmark 仓库当前把 71 题 Stealth Bench V1 放在最前。Stealth Bench 测的是反机器人检测与浏览器基础设施,不是 CowAgent 能否自主完成业务任务,因此不能代替本报告的 agent 评测。

BU Bench 由 Custom、WebBench、Mind2Web 2、GAIA、BrowseComp 各 20 题组成,另有独立的 300 题 Online‑Mind2Web 运行仓库。最值得借鉴的是:运行前确认任务可完成、保存逐题 trace、支持断点续跑、把“不可完成”与 Agent 真失败分开。Odysseys 还把长任务拆成平均 6.1 个可检查 rubric,并同时报告完整成功率、部分进度和每步效率。

首轮不直接照搬的原因也很具体:这些工作流都包含模型评审,BU Bench 还混入搜索与问答任务,Odysseys 又是实时多站长链任务。它们适合第二阶段测真实互联网泛化和长链能力,但会把网页漂移、judge 波动和浏览器内核差异同时混进结果。

+

WebArena‑Verified 本地部署要不要账号付费

四个首轮站点使用官方 Docker 镜像和内置测试账号,本地运行不需要向 GitLab、Reddit 或电商站点申请真实账号,也没有按题平台费用。成本来自本机 CPU、内存、磁盘和 GPT‑6 Astra API。

Astra 通过 Responses API 调用,模型 ID 是 gpt-6-astra,本轮固定 reasoning.effort=low。OpenAI 官方模型页把 Free API tier 标为不支持,因此需要一个已开通 API 计费且有该模型权限的 OpenAI 账号和项目密钥;Codex 不能代替团队注册账号、开通计费或承担费用。

“本地可部署”也不等于轻量。本轮核对的官方 GitLab 镜像 manifest 含一个约 22.0 GB 的压缩主层,官方文档建议至少 4 GB 内存;四站点与两浏览器因此放在独立的 100 GB Docker profile 中。Wikipedia 与 Map 还需要额外数据卷,所以首轮不纳入。自托管能消除网站内容漂移、封号和真实支付风险,但不会自动消除镜像下载、环境初始化、登录兼容、浏览器 CDP 实现差异或 evaluator 缺陷。

+

本轮怎样保证是可比较的自主 Agent 实验

{_table(['项目','预注册规则'], [['样本','固定 30 题:简单 5、中等 10、困难 15;运行后不换题,不重复抽样'],['Agent 与模型','CowAgent 真实自主观察、规划和调用 BrowserTool;GPT‑6 Astra,reasoning=low;每个试次清空模型会话'],['唯一变量','Agent、提示、模型、任务、站点数据和资源限额相同,只替换 Chromium / Moli'],['环境','每个试次都从官方镜像重置该题站点;Shopping、Shopping Admin、Reddit 使用官方头登录,GitLab 使用官方 UI 登录'],['顺序','逐题交替先跑 Chromium 或 Moli,减轻固定先后顺序偏差'],['主分','每个“题 × 浏览器”只跑一次,官方二元 pass@1;两边构成逐题配对'],['重试','只对首轮失败项再试一次,最多一次;重试仅报告恢复率,不回填或抬高主分'],['留证','保存最终 JSON、CowAgent 事件、Playwright trace、官方 evaluator 明细、镜像 ID、代码 commit、token 与资源指标'],['排除','首轮不选 Wikipedia / Map;不选无法由官方确定性 evaluator 验收的外部实时站点任务']])}

“操作步数层级”只决定覆盖结构,不参与得分。环境准备失败应标成基础设施错误并修复后重跑同一预注册试次,不能计作 Agent 失败,也不能借机换题。

+{_result_section(attempts)}{_tier_section(attempts)}{_task_table(selection_path, attempts)} +

两条轨道必须分开读

{_table(['轨道','Agent 是否自主','控制变量','主指标','回答的问题'], [['浏览器内核微基准(原 30 题)','否;重放固定 BrowserTool 等价调用','同一动作序列,只替换浏览器','严格输出正确率、动作执行、资源与耗时','相同操作交给两个内核,谁更兼容/省资源?'],['WebArena‑Verified Agent 评测(本报告)','是;CowAgent 自主观察、规划和调用 BrowserTool','同一 Agent、模型、提示、站点快照与任务,只替换浏览器','官方首轮 pass@1;失败重试恢复率另列','浏览器差异会怎样影响完整自主任务成功率?']])}

固定动作轨道不是“有问题的假 Agent”,它是有价值的内核微基准;问题只是旧命名把它说成了自主 browser use。两条轨道互补,但分数不能相加,也不能用一条替代另一条。

+

公开证据与复现边界

采用统计只计公开一手材料。未找到 DeepSeek 或 Kimi 的官方浏览器交互 benchmark,不代表其内部没有评测;Kimi 公布的 BrowseComp 属于 agentic search,本报告不把它算成 UI browser use。

+
""" + report = output / "index.html" + report.write_text(html_text, encoding="utf-8") + audit = { + "schema": "lex_browser_agent_report/1", + "evidence": str(evidence), + "attempt_count": len(attempts), + "summary": _primary_summary(attempts), + "paired_summary": _paired_summary(attempts), + "selection_sha256": load_pilot(selection_path).sha256, + } + (output / "report-data.json").write_text( + json.dumps(audit, ensure_ascii=False, indent=2), encoding="utf-8" + ) + return report + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--evidence", required=True, type=Path) + parser.add_argument("--output", required=True, type=Path) + parser.add_argument( + "--selection", + type=Path, + default=Path("evaluation/browser-agent/webarena-verified-pilot.json"), + ) + args = parser.parse_args() + print(build_report(evidence=args.evidence, output=args.output, selection_path=args.selection)) + + +if __name__ == "__main__": + main() diff --git a/harness/browser_agent/runner.py b/harness/browser_agent/runner.py new file mode 100644 index 000000000..c5ce61bd6 --- /dev/null +++ b/harness/browser_agent/runner.py @@ -0,0 +1,589 @@ +"""Run CowAgent autonomously against official self-hosted WebArena-Verified sites.""" + +from __future__ import annotations + +import argparse +import importlib.metadata +import json +import platform +import subprocess +import sys +import time +import traceback +import uuid +from pathlib import Path +from typing import Any + +import docker +import requests +from dotenv import dotenv_values +from json_repair import repair_json +from webarena_verified import WebArenaVerified +from webarena_verified.environments.container.manager import ContainerManager +from webarena_verified.environments.env_ctrl_client import EnvCtrlDockerClient +from webarena_verified.types.config import EnvironmentConfig, WebArenaVerifiedConfig +from webarena_verified.types.task import WebArenaSite + +from harness.browser_kernel.metrics import ( + DockerStatsResources, + ResourceSampler, + delta_counters, +) +from harness.browser_kernel.runner import ( + ENGINE_IMAGES, + ENGINE_PORTS, + _published_endpoint, + _start_browser_container, + _wait_cdp, +) + +from .model import AstraResponsesModel +from .selection import PilotTask, load_pilot, validate_against_official + +SCHEMA = "lex_browser_agent_attempt/1" +SITE_PORTS = { + WebArenaSite.SHOPPING: (7770, 7771), + WebArenaSite.SHOPPING_ADMIN: (7780, 7781), + WebArenaSite.REDDIT: (9999, 9998), + WebArenaSite.GITLAB: (8023, 8024), +} +SITE_CREDENTIALS = { + WebArenaSite.SHOPPING: {"username": "emma.lopez@gmail.com", "password": "Password.123"}, + WebArenaSite.SHOPPING_ADMIN: {"username": "admin", "password": "admin1234"}, + WebArenaSite.REDDIT: {"username": "MarvelsGrantMan136", "password": "test1234"}, + WebArenaSite.GITLAB: {"username": "byteblaze", "password": "hello1234"}, +} +SITE_URL_SUFFIX = {WebArenaSite.SHOPPING_ADMIN: "/admin"} +SITE_AUTH_HEADERS = { + WebArenaSite.SHOPPING: "X-M2-Customer-Auto-Login", + WebArenaSite.SHOPPING_ADMIN: "X-M2-Admin-Auto-Login", + WebArenaSite.REDDIT: "X-Postmill-Auto-Login", +} + + +def _load_repo_env(repo_root: Path) -> dict[str, str]: + """Credentials are intentionally read only from this repository's .env.""" + env_file = repo_root / ".env" + if not env_file.exists(): + return {} + return {str(key): str(value) for key, value in dotenv_values(env_file).items() if value} + + +def _git_head(path: Path) -> str: + return subprocess.run( + ["git", "-C", str(path), "rev-parse", "HEAD"], + check=True, + capture_output=True, + text=True, + ).stdout.strip() + + +def _cdp_metadata(http_endpoint: str) -> dict[str, str]: + response = requests.get(http_endpoint.rstrip("/") + "/json/version", timeout=15) + response.raise_for_status() + payload = response.json() + return { + key: str(payload[key]) + for key in ("Browser", "Protocol-Version", "User-Agent", "V8-Version") + if payload.get(key) + } + + +def _configure_benchmark(hostname: str = "host.docker.internal") -> WebArenaVerified: + environments = { + site: EnvironmentConfig( + urls=[ + f"http://{hostname}:{SITE_PORTS[site][0]}" + f"{SITE_URL_SUFFIX.get(site, '')}" + ], + credentials=SITE_CREDENTIALS[site], + use_header_login=site == WebArenaSite.SHOPPING_ADMIN, + ) + for site in SITE_PORTS + } + return WebArenaVerified(config=WebArenaVerifiedConfig(environments=environments)) + + +def _ensure_site(site: WebArenaSite, *, reset: bool) -> None: + port, ctrl_port = SITE_PORTS[site] + manager = ContainerManager(site=site, hostname="host.docker.internal") + if reset or not manager.is_running(): + manager.start(port=port, env_ctrl_port=ctrl_port, wait=False) + ready = EnvCtrlDockerClient.create(manager.container_name, timeout=180).wait_until_ready( + timeout=180, interval=2 + ) + if not ready.success: + raise RuntimeError(f"{site.value} did not become ready: {ready.message}") + + +def _prepare_sites(sites: tuple[WebArenaSite, ...]) -> None: + """Keep only task-required sites running, then reset those sites from official images.""" + for site in SITE_PORTS: + manager = ContainerManager(site=site, hostname="host.docker.internal") + if site not in sites and manager.is_running(): + manager.stop() + for site in sites: + _ensure_site(site, reset=True) + + +def _login_context( + service: Any, sites: tuple[WebArenaSite, ...], benchmark: WebArenaVerified +) -> None: + def login() -> None: + context = service._context + assert context is not None + context.clear_cookies() + context.set_extra_http_headers( + { + SITE_AUTH_HEADERS[site]: ( + f"{SITE_CREDENTIALS[site]['username']}:" + f"{SITE_CREDENTIALS[site]['password']}" + ) + for site in sites + if site in SITE_AUTH_HEADERS + } + ) + for site in sites: + base = benchmark.config.get_environment(site).active_url + creds = SITE_CREDENTIALS[site] + page = context.new_page() + try: + page.set_default_timeout(60_000) + page.set_default_navigation_timeout(90_000) + if site == WebArenaSite.SHOPPING: + page.goto(f"{base}/customer/account", wait_until="commit") + page.evaluate("localStorage.clear(); sessionStorage.clear()") + page.reload(wait_until="domcontentloaded") + page.locator(".block-dashboard-info, .box-information").first.wait_for() + elif site == WebArenaSite.SHOPPING_ADMIN: + page.goto(base, wait_until="commit") + page.evaluate("localStorage.clear(); sessionStorage.clear()") + page.reload(wait_until="domcontentloaded") + page.locator(".page-title").wait_for() + elif site == WebArenaSite.REDDIT: + page.goto(base, wait_until="commit") + page.evaluate("localStorage.clear(); sessionStorage.clear()") + page.reload(wait_until="domcontentloaded") + page.get_by_role("button", name=creds["username"], exact=True).wait_for() + elif site == WebArenaSite.GITLAB: + page.goto(f"{base}/users/sign_in", wait_until="commit") + page.evaluate("localStorage.clear(); sessionStorage.clear()") + page.reload(wait_until="domcontentloaded") + page.get_by_test_id("username-field").fill(creds["username"]) + page.get_by_test_id("password-field").fill(creds["password"]) + page.get_by_test_id("sign-in-button").click() + page.wait_for_url("**/", timeout=90_000) + page.wait_for_timeout(1_000) + finally: + page.close() + + service._submit(login) + + +def _system_prompt() -> str: + return """You are the autonomous browser agent under evaluation. Use only the browser tool. +Complete the objective from the supplied start URLs. Do not ask for help, download files, or +change state unless the objective requires it. When finished, return only one JSON object with +task_type (RETRIEVE, MUTATE, or NAVIGATE), status (SUCCESS, NOT_FOUND_ERROR, +ACTION_NOT_ALLOWED_ERROR, PERMISSION_DENIED_ERROR, DATA_VALIDATION_ERROR, or UNKNOWN_ERROR), +retrieved_data, and error_details. For RETRIEVE, retrieved_data is always a list. For other task +types it is null.""" + + +def _user_prompt(task: Any, start_urls: list[str]) -> str: + response_eval = next( + item for item in task.eval if item.evaluator == "AgentResponseEvaluator" + ) + return ( + "Task objective:\n" + + task.intent + + "\n\nStart URLs:\n" + + "\n".join(f"- {url}" for url in start_urls) + + "\n\nOfficial response contract (schema only; this does not reveal the answer):\n" + + f"- task_type: {str(response_eval.expected.task_type).upper()}\n" + + "- retrieved_data JSON Schema: " + + json.dumps( + response_eval.model_dump(mode="json")["results_schema"], + ensure_ascii=False, + separators=(",", ":"), + ) + ) + + +def _parse_agent_response(text: str) -> dict[str, Any] | str: + try: + repaired = repair_json(text, return_objects=True) + except Exception: + return text + return repaired if isinstance(repaired, dict) else text + + +def _resource_summary( + resources: DockerStatsResources, + sampler: ResourceSampler, + before: Any, + after: Any, + duration_ms: float, +) -> dict[str, Any]: + result = {**delta_counters(before, after), **sampler.summary()} + if duration_ms > 0: + result["cpu_average_utilization_pct"] = result["cpu_ms"] / duration_ms * 100 + return result + + +def _run_attempt( # noqa: PLR0913 + *, + benchmark: WebArenaVerified, + selected: PilotTask, + engine: str, + attempt: int, + endpoint: str, + container: Any, + cowagent_repo: Path, + output_dir: Path, + model: AstraResponsesModel, + sample_interval_ms: int, +) -> dict[str, Any]: + task = benchmark.get_task(selected.task_id) + sites = tuple(task.sites) + _prepare_sites(sites) + + if str(cowagent_repo) not in sys.path: + sys.path.insert(0, str(cowagent_repo)) + from agent.protocol.agent import Agent # noqa: PLC0415 + from agent.tools.browser.browser_tool import BrowserTool # noqa: PLC0415 + + attempt_dir = output_dir / engine / str(selected.task_id) / f"attempt-{attempt}" + attempt_dir.mkdir(parents=True, exist_ok=True) + trace_path = attempt_dir / "network.zip" + response_path = attempt_dir / "agent_response.json" + eval_path = attempt_dir / "eval_result.json" + events_path = attempt_dir / "agent_events.json" + tool = BrowserTool( + config={ + "cdp_endpoint": endpoint, + "headless": True, + "viewport": {"width": 1280, "height": 900}, + "startup_timeout": 90, + } + ) + events: list[dict[str, Any]] = [] + resources = DockerStatsResources(container) + before = resources.counters() + sampler = ResourceSampler(resources, interval_ms=sample_interval_ms) + raw_response = "" + error = "" + prepared = False + sampler_started = False + started = time.perf_counter() + try: + model.reset() + service = tool._get_service() + service._submit(lambda: None) + _login_context(service, sites, benchmark) + service._submit(lambda: service._context.tracing.start(screenshots=True, snapshots=True)) + prepared = True + sampler.start() + sampler_started = True + agent = Agent( + system_prompt="", + description="WebArena-Verified CowAgent", + model=model, + tools=[tool], + output_mode="logger", + max_steps=30, + enable_skills=False, + skip_context_files=True, + ) + # CowAgent rebuilds its base prompt at run time; the suffix is the supported + # way to retain benchmark-specific completion and output constraints. + agent.extra_system_suffix = _system_prompt() + start_urls = list(benchmark.config.render_url(list(task.start_urls), list(task.sites))) + raw_response = agent.run_stream( + _user_prompt(task, start_urls), on_event=events.append, clear_history=True + ) + service._submit(lambda: service._context.tracing.stop(path=str(trace_path))) + except Exception: + error = traceback.format_exc() + try: + service = tool._get_service() + service._submit(lambda: service._context.tracing.stop(path=str(trace_path))) + except Exception: + pass + finally: + if sampler_started: + sampler.stop() + duration_ms = (time.perf_counter() - started) * 1000 + after = resources.counters() + tool.close() + + if not prepared: + raise RuntimeError( + f"{engine} task {selected.task_id}: environment precondition failed\n{error}" + ) + + parsed_response = _parse_agent_response(raw_response) + response_path.write_text( + json.dumps(parsed_response, ensure_ascii=False, indent=2), encoding="utf-8" + ) + events_path.write_text(json.dumps(events, ensure_ascii=False, indent=2), encoding="utf-8") + if trace_path.exists(): + evaluation = benchmark.evaluate_task( + task_id=selected.task_id, + agent_response=parsed_response, + network_trace=trace_path, + ) + eval_data = evaluation.model_dump(mode="json") + else: + eval_data = {"status": "error", "score": 0.0, "error_msg": error or "trace missing"} + eval_path.write_text(json.dumps(eval_data, ensure_ascii=False, indent=2), encoding="utf-8") + resource = ( + _resource_summary(resources, sampler, before, after, duration_ms) + if sampler.samples + else {} + ) + return { + "schema": SCHEMA, + "engine": engine, + "task_id": selected.task_id, + "tier": selected.tier, + "estimated_min_steps": selected.estimated_min_steps, + "attempt": attempt, + "is_retry": attempt > 1, + "status": eval_data.get("status", "error"), + "score": float(eval_data.get("score") or 0.0), + "duration_ms": round(duration_ms, 3), + "agent_response": parsed_response, + "error": error, + "resource": resource, + "model_usage": {**model.usage, "request_count": model.request_count}, + "paths": { + "trace": str(trace_path), + "response": str(response_path), + "evaluation": str(eval_path), + "events": str(events_path), + }, + } + + +def _load_completed_attempts(path: Path) -> dict[tuple[str, int, int], dict[str, Any]]: + """Load durable attempt rows so an interrupted multi-hour pilot can resume safely.""" + if not path.exists(): + return {} + completed: dict[tuple[str, int, int], dict[str, Any]] = {} + for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + if not line.strip(): + continue + row = json.loads(line) + key = (str(row["engine"]), int(row["task_id"]), int(row["attempt"])) + if key in completed: + raise ValueError(f"duplicate attempt at {path}:{line_number}: {key}") + completed[key] = row + return completed + + +def _write_or_validate_run_config(path: Path, planned: dict[str, Any]) -> None: + if path.exists(): + existing = json.loads(path.read_text(encoding="utf-8")) + if existing != planned: + raise ValueError("resume configuration differs from the existing evidence cohort") + return + path.write_text(json.dumps(planned, ensure_ascii=False, indent=2), encoding="utf-8") + + +def run(args: argparse.Namespace) -> Path: # noqa: PLR0915 + repo_root = Path(args.repo_root).resolve() + env = _load_repo_env(repo_root) + api_key = env.get("OPENAI_API_KEY") + if not api_key: + raise RuntimeError(f"OPENAI_API_KEY is required in {repo_root / '.env'}") + cowagent_repo = Path(args.cowagent_repo or env.get("COWAGENT_REPO", "")).resolve() + if not (cowagent_repo / "agent/protocol/agent.py").exists(): + raise RuntimeError("--cowagent-repo must point to a CowAgent checkout") + + output_dir = Path(args.output).expanduser().resolve() + output_dir.mkdir(parents=True, exist_ok=True) + selection = load_pilot(args.selection) + benchmark = _configure_benchmark() + resolved = validate_against_official(selection, benchmark) + model = AstraResponsesModel( + api_key=api_key, + base_url=env.get("OPENAI_BASE_URL", "https://api.openai.com/v1"), + model=args.model, + reasoning_effort="low", + ) + + client = docker.from_env() + run_id = uuid.uuid4().hex[:12] + containers: dict[str, Any] = {} + networks: dict[str, Any] = {} + endpoints: dict[str, str] = {} + attempts_file = output_dir / "attempts.jsonl" + run_config_file = output_dir / "run-config.json" + completed = _load_completed_attempts(attempts_file) + image_metadata: dict[str, dict[str, Any]] = {} + browser_metadata: dict[str, dict[str, str]] = {} + docker_info: dict[str, Any] = {} + try: + for engine in ENGINE_IMAGES: + network = client.networks.create(f"lex-browser-agent-{run_id}-{engine}") + networks[engine] = network + container = _start_browser_container( + client, + engine=engine, + run_id=f"agent-{run_id}", + cpus=args.cpus, + memory=args.memory, + network=network.name, + ) + containers[engine] = container + container.reload() + image_metadata[engine] = { + "id": container.image.id, + "tags": container.image.tags, + } + http_endpoint = _published_endpoint( + container, engine=engine, cdp_port=ENGINE_PORTS[engine] + ) + endpoints[engine] = _wait_cdp(http_endpoint, container=container) + browser_metadata[engine] = _cdp_metadata(http_endpoint) + + _write_or_validate_run_config( + run_config_file, + { + "schema": "lex_browser_agent_run_config/1", + "selection_sha256": selection.sha256, + "cowagent_commit": _git_head(cowagent_repo), + "model": args.model, + "reasoning_effort": "low", + "browser_images": image_metadata, + "browser_versions": browser_metadata, + "docker_context": subprocess.run( + ["docker", "context", "show"], + check=True, + capture_output=True, + text=True, + ).stdout.strip(), + }, + ) + + with attempts_file.open("a", encoding="utf-8") as stream: + for task_index, selected in enumerate(selection.tasks): + order = ("chromium", "moli") if task_index % 2 == 0 else ("moli", "chromium") + for engine in order: + for attempt in (1, 2): + key = (engine, selected.task_id, attempt) + if key in completed: + row = completed[key] + print( + json.dumps( + { + "engine": engine, + "task_id": selected.task_id, + "attempt": attempt, + "status": "resume-skip", + "score": row["score"], + } + ), + flush=True, + ) + if float(row["score"]) == 1.0: + break + continue + primary = completed.get((engine, selected.task_id, 1)) + if attempt == 2 and primary is not None and float(primary["score"]) == 1.0: + break + row = _run_attempt( + benchmark=benchmark, + selected=selected, + engine=engine, + attempt=attempt, + endpoint=endpoints[engine], + container=containers[engine], + cowagent_repo=cowagent_repo, + output_dir=output_dir, + model=model, + sample_interval_ms=args.sample_interval_ms, + ) + row["run_id"] = run_id + stream.write(json.dumps(row, ensure_ascii=False) + "\n") + stream.flush() + completed[key] = row + visible = { + key: row[key] + for key in ("engine", "task_id", "attempt", "status", "score") + } + print(json.dumps(visible), flush=True) + if row["score"] == 1.0: + break + docker_info = client.info() + finally: + for container in containers.values(): + try: + container.remove(force=True) + except Exception: + pass + for network in networks.values(): + try: + network.remove() + except Exception: + pass + + manifest = { + "schema": "lex_browser_agent_manifest/1", + "run_id": run_id, + "run_ids": sorted({str(row.get("run_id")) for row in completed.values()}), + "created_at": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + "selection_sha256": selection.sha256, + "selection": resolved, + "protocol": { + "agent": "CowAgent", + "cowagent_commit": _git_head(cowagent_repo), + "model": args.model, + "reasoning_effort": "low", + "primary_attempts_per_engine_task": 1, + "failure_retries": 1, + "official_score_uses": "primary attempt only", + "retry_score_use": "reported separately as recovery diagnostic", + "evaluator": "official webarena-verified package, unmodified", + "environment_reset": "official site container reset before every attempt", + "engine_order": "alternating by task", + "sites": [site.value for site in SITE_PORTS], + }, + "environment": { + "platform": platform.platform(), + "docker_server_version": client.version().get("Version"), + "docker_architecture": docker_info.get("Architecture"), + "docker_name": docker_info.get("Name"), + "webarena_verified_version": importlib.metadata.version("webarena-verified"), + "browser_images": image_metadata, + "browser_versions": browser_metadata, + }, + } + (output_dir / "manifest.json").write_text( + json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8" + ) + return output_dir + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--repo-root", default=".") + parser.add_argument( + "--selection", default="evaluation/browser-agent/webarena-verified-pilot.json" + ) + parser.add_argument("--output", required=True) + parser.add_argument("--cowagent-repo") + parser.add_argument("--model", default="gpt-6-astra") + parser.add_argument("--cpus", type=float, default=2.0) + parser.add_argument("--memory", default="2g") + parser.add_argument("--sample-interval-ms", type=int, default=100) + return parser.parse_args() + + +def main() -> None: + run(parse_args()) + + +if __name__ == "__main__": + main() diff --git a/harness/browser_agent/selection.py b/harness/browser_agent/selection.py new file mode 100644 index 000000000..0f8da4426 --- /dev/null +++ b/harness/browser_agent/selection.py @@ -0,0 +1,105 @@ +"""Pre-registered WebArena-Verified pilot selection and validation.""" + +from __future__ import annotations + +import hashlib +import json +from dataclasses import dataclass +from importlib.resources import files +from pathlib import Path +from typing import Any + +PILOT_SCHEMA = "lex_webarena_verified_pilot/1" +ALLOWED_SITES = frozenset({"shopping", "shopping_admin", "reddit", "gitlab"}) +TIER_COUNTS = {"easy": 5, "medium": 10, "hard": 15} +TIER_BOUNDS = {"easy": (1, 4), "medium": (5, 7), "hard": (8, None)} + + +@dataclass(frozen=True) +class PilotTask: + task_id: int + tier: str + estimated_min_steps: int + rationale: str + official_hard: bool + + +@dataclass(frozen=True) +class PilotSelection: + tasks: tuple[PilotTask, ...] + source_path: Path + sha256: str + metadata: dict[str, Any] + + +def _sha256(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def load_pilot(path: str | Path) -> PilotSelection: + source = Path(path) + payload = json.loads(source.read_text(encoding="utf-8")) + if payload.get("schema") != PILOT_SCHEMA: + raise ValueError(f"unsupported pilot schema: {payload.get('schema')!r}") + tasks = tuple( + PilotTask( + task_id=int(item["task_id"]), + tier=str(item["tier"]), + estimated_min_steps=int(item["estimated_min_steps"]), + rationale=str(item["rationale"]), + official_hard=bool(item["official_hard"]), + ) + for item in payload.get("tasks", []) + ) + ids = [task.task_id for task in tasks] + if len(ids) != len(set(ids)): + raise ValueError("pilot task IDs must be unique") + counts = {tier: sum(task.tier == tier for task in tasks) for tier in TIER_COUNTS} + if counts != TIER_COUNTS: + raise ValueError(f"pilot tier counts must be {TIER_COUNTS}, got {counts}") + for task in tasks: + low, high = TIER_BOUNDS[task.tier] + if task.estimated_min_steps < low or ( + high is not None and task.estimated_min_steps > high + ): + raise ValueError(f"task {task.task_id}: step estimate is outside {task.tier}") + return PilotSelection(tasks, source, _sha256(source), dict(payload.get("metadata") or {})) + + +def validate_against_official(selection: PilotSelection, benchmark: Any) -> list[dict[str, Any]]: + """Resolve task metadata through the official package; never copy evaluator rules.""" + hard_payload = json.loads( + files("webarena_verified") + .joinpath("assets/dataset/subsets/webarena-verified-hard.json") + .read_text(encoding="utf-8") + ) + official_hard_ids = {int(task_id) for task_id in hard_payload["task_ids"]} + resolved: list[dict[str, Any]] = [] + for selected in selection.tasks: + task = benchmark.get_task(selected.task_id) + sites = [getattr(site, "value", str(site)) for site in task.sites] + if not set(sites).issubset(ALLOWED_SITES): + raise ValueError(f"task {selected.task_id}: unsupported pilot sites {sites}") + if len(sites) != 1: + raise ValueError( + f"task {selected.task_id}: pilot requires one resettable site, got {sites}" + ) + if selected.official_hard != (selected.task_id in official_hard_ids): + raise ValueError( + f"task {selected.task_id}: official_hard does not match package subset" + ) + resolved.append( + { + "task_id": selected.task_id, + "tier": selected.tier, + "estimated_min_steps": selected.estimated_min_steps, + "rationale": selected.rationale, + "official_hard": selected.official_hard, + "sites": sites, + "intent": task.intent, + "intent_template_id": task.intent_template_id, + "revision": task.revision, + "start_urls": list(task.start_urls), + } + ) + return resolved diff --git a/harness/browser_use/__init__.py b/harness/browser_kernel/__init__.py similarity index 100% rename from harness/browser_use/__init__.py rename to harness/browser_kernel/__init__.py diff --git a/harness/browser_use/cli.py b/harness/browser_kernel/cli.py similarity index 95% rename from harness/browser_use/cli.py rename to harness/browser_kernel/cli.py index 22e12ec6d..083d0822d 100644 --- a/harness/browser_use/cli.py +++ b/harness/browser_kernel/cli.py @@ -1,4 +1,4 @@ -"""Command-line entrypoint for isolated browser use evaluation.""" +"""Command-line entrypoint for isolated browser kernel microbenchmark.""" from __future__ import annotations diff --git a/harness/browser_use/diagnostics.py b/harness/browser_kernel/diagnostics.py similarity index 100% rename from harness/browser_use/diagnostics.py rename to harness/browser_kernel/diagnostics.py diff --git a/harness/browser_use/executor.py b/harness/browser_kernel/executor.py similarity index 100% rename from harness/browser_use/executor.py rename to harness/browser_kernel/executor.py diff --git a/harness/browser_use/metrics.py b/harness/browser_kernel/metrics.py similarity index 100% rename from harness/browser_use/metrics.py rename to harness/browser_kernel/metrics.py diff --git a/harness/browser_use/report.py b/harness/browser_kernel/report.py similarity index 96% rename from harness/browser_use/report.py rename to harness/browser_kernel/report.py index a05fb4944..bebeb5fd3 100644 --- a/harness/browser_use/report.py +++ b/harness/browser_kernel/report.py @@ -11,7 +11,7 @@ from typing import Any from urllib.parse import urlparse -REPORT_STEM = "browser-use-evaluation-latest" +REPORT_STEM = "browser-kernel-evaluation-latest" PAGES = tuple( (slug, label, f"{REPORT_STEM}{suffix}.html") for slug, label, suffix in ( @@ -412,7 +412,7 @@ def _summary_page(manifest: dict[str, Any], summary: dict[str, Any]) -> str:

关键技术结论

+
  • 根因边界:布局与 actionability 证据只能定位现象,尚不能确认 screencast、渲染器或适配层中的具体根因。查看逐任务证据与验证方案。
  • 双方成功任务的速度与资源

    统计范围:双方都成功的 {paired} 个同任务,不含失败与重试样本。绝对值优胜项为绿色粗体;配对比值低于 1 为绿色、高于 1 为红色,显示值相同保持中性。

    {_metric_tables(summary)}{_duration_ratio_audit(summary)}
    {smoke_html}""" @@ -640,7 +640,7 @@ def _method_page(manifest: dict[str, Any]) -> str: ], ["采样", f"每 {p.get('sample_interval_ms', '?')} ms 采样内存、PID、CPU、网络计数"], ] - return f"""

    步骤的严格定义

    一步 = 一次 CowAgent BrowserTool 等价调用,例如 navigate、click、get_text、fill、select、scroll、back 或 forward。等待页面就绪和目标值出现属于该工具调用内部,不单列、不计步。每次调用的参数、耗时、输出和状态写入执行轨迹。

    成功判定:两个独立程序指标

    1. 结果正确(主指标):每个标注输出与 expected_outputs 做严格字符串相等;不使用模糊匹配或模型评分;Codex 只参与评测前的标注核对。
    2. 结果与参考过程均正确(辅指标):在结果正确之外,本轮被重放的参考动作序列还必须与 expected_actions 一致,且全部 BrowserTool 调用成功。

    边界:expected_actions 是工具层微基准的固定参考路径,不代表任务只有一种解法。若评测自由规划 CowAgent,应把过程判定改成必要里程碑、禁止动作和步数预算,不能要求整条序列逐步相等。

    环境

    {_table(["项目", "记录值"], rows, "本轮环境")}

    执行流程

    1. 分别启动并预热两个浏览器,浏览器进程和上下文在整轮评测中保持运行。
    2. 任务前清 Cookie、权限、HTTP 缓存、Cache Storage、Service Worker、IndexedDB、来源存储和历史,并创建新页面。
    3. 计时外静置 {p.get("post_reset_settle_ms", "?")} ms,按任务交替两个 Provider 的执行顺序。
    4. 执行完整任务并采样;失败复核归入同一任务详情。
    5. 资源基线只比较双方都成功的同任务,失败与复核样本不混入正常路径。

    统计口径

    """ + return f"""

    步骤的严格定义

    一步 = 一次 CowAgent BrowserTool 等价调用,例如 navigate、click、get_text、fill、select、scroll、back 或 forward。等待页面就绪和目标值出现属于该工具调用内部,不单列、不计步。每次调用的参数、耗时、输出和状态写入执行轨迹。

    成功判定:两个独立程序指标

    1. 结果正确(主指标):每个标注输出与 expected_outputs 做严格字符串相等;不使用模糊匹配或模型评分;Codex 只参与评测前的标注核对。
    2. 结果与参考过程均正确(辅指标):在结果正确之外,本轮被重放的参考动作序列还必须与 expected_actions 一致,且全部 BrowserTool 调用成功。

    边界:expected_actions 是工具层微基准的固定参考路径,不代表任务只有一种解法。若评测自由规划 CowAgent,应把过程判定改成必要里程碑、禁止动作和步数预算,不能要求整条序列逐步相等。

    环境

    {_table(["项目", "记录值"], rows, "本轮环境")}

    执行流程

    1. 分别启动并预热两个浏览器,浏览器进程和上下文在整轮评测中保持运行。
    2. 任务前清 Cookie、权限、HTTP 缓存、Cache Storage、Service Worker、IndexedDB、来源存储和历史,并创建新页面。
    3. 计时外静置 {p.get("post_reset_settle_ms", "?")} ms,按任务交替两个 Provider 的执行顺序。
    4. 执行完整任务并采样;失败复核归入同一任务详情。
    5. 资源基线只比较双方都成功的同任务,失败与复核样本不混入正常路径。

    统计口径

    """ METRICS = ( @@ -1223,7 +1223,7 @@ def _appendix_page(manifest: dict[str, Any]) -> str: ["attempts.jsonl SHA-256", str(provenance.get("attempts_sha256") or "未记录")], ["summary.json SHA-256", str(provenance.get("summary_sha256") or "未记录")], ] - return f"""

    复现步骤

    1. 准备 Docker Engine、Python 3.11+、至少 4 CPU/6 GiB 内存。
    2. 运行 ./examples/browser_use/run.sh;脚本执行完整基准,并依次生成原始记录、汇总数据和分页 HTML。
    3. 如需为失败任务补充一次同任务证据,设置 BROWSER_USE_EVAL_FAILURE_RETRIES=1;该记录不会另计为新任务。
    4. 如只需从已有 attempts.jsonl 重算全部指标和报告,运行 uv run python examples/browser_use/rebuild_report.py。
    5. 检查 attempts.jsonl、manifest.json、summary.json 与分页 HTML;所有比值、分位数和均值均由仓库脚本计算,Codex 不参与统计。

    下载本轮机器可读审计摘要

    运行与原始数据绑定

    {_table(["项目", "值"], provenance_rows, "可复核来源")}

    数据字段

    {_table(["对象", "关键字段", "用途"], fields, "数据结构")}

    复现限制

    原始环境 JSON

    展开
    {_e(json.dumps(manifest.get("environment") or {}, ensure_ascii=False, indent=2, sort_keys=True))}
    """ + return f"""

    复现步骤

    1. 准备 Docker Engine、Python 3.11+、至少 4 CPU/6 GiB 内存。
    2. 运行 ./examples/browser_kernel/run.sh;脚本执行完整基准,并依次生成原始记录、汇总数据和分页 HTML。
    3. 如需为失败任务补充一次同任务证据,设置 BROWSER_USE_EVAL_FAILURE_RETRIES=1;该记录不会另计为新任务。
    4. 如只需从已有 attempts.jsonl 重算全部指标和报告,运行 uv run python examples/browser_kernel/rebuild_report.py。
    5. 检查 attempts.jsonl、manifest.json、summary.json 与分页 HTML;所有比值、分位数和均值均由仓库脚本计算,Codex 不参与统计。

    下载本轮机器可读审计摘要

    运行与原始数据绑定

    {_table(["项目", "值"], provenance_rows, "可复核来源")}

    数据字段

    {_table(["对象", "关键字段", "用途"], fields, "数据结构")}

    复现限制

    原始环境 JSON

    展开
    {_e(json.dumps(manifest.get("environment") or {}, ensure_ascii=False, indent=2, sort_keys=True))}
    """ def _markdown(manifest: dict[str, Any], summary: dict[str, Any]) -> str: @@ -1260,7 +1260,7 @@ def paired_duration(engine: str, stat: str) -> str: [ "", "```bash", - "BROWSER_USE_EVAL_FAILURE_RETRIES=1 ./examples/browser_use/run.sh", + "BROWSER_USE_EVAL_FAILURE_RETRIES=1 ./examples/browser_kernel/run.sh", "```", "", ] @@ -1278,9 +1278,9 @@ def write_latest_report( analysis = _analysis_page(manifest, summary, detail_pages) keep = {f"{REPORT_STEM}.md", audit_name, *(filename for _, _, filename in PAGES), *detail_pages} for pattern in ( - "browser-use-evaluation-*.md", - "browser-use-evaluation-*.html", - "browser-use-evaluation-*.json", + "browser-kernel-evaluation-*.md", + "browser-kernel-evaluation-*.html", + "browser-kernel-evaluation-*.json", ): for old in root.glob(pattern): if old.name not in keep: diff --git a/harness/browser_use/runner.py b/harness/browser_kernel/runner.py similarity index 99% rename from harness/browser_use/runner.py rename to harness/browser_kernel/runner.py index dba83e470..923a1ddfa 100644 --- a/harness/browser_use/runner.py +++ b/harness/browser_kernel/runner.py @@ -1,4 +1,4 @@ -"""Docker-isolated browser use evaluation runner.""" +"""Docker-isolated browser kernel microbenchmark runner.""" from __future__ import annotations diff --git a/harness/browser_use/suite.py b/harness/browser_kernel/suite.py similarity index 99% rename from harness/browser_use/suite.py rename to harness/browser_kernel/suite.py index f61fe6fb4..9b1050a9b 100644 --- a/harness/browser_use/suite.py +++ b/harness/browser_kernel/suite.py @@ -1,4 +1,4 @@ -"""Task-suite schema and validation for browser use evaluation.""" +"""Task-suite schema and validation for browser kernel microbenchmark.""" from __future__ import annotations diff --git a/harness/web_fetch/README.md b/harness/web_fetch/README.md index 766e37979..49f5c7ecf 100644 --- a/harness/web_fetch/README.md +++ b/harness/web_fetch/README.md @@ -135,7 +135,9 @@ evaluation records, task decisions and `word_*` metrics are unchanged. Full-body metrics require a separately scoped, comparable complete reference. Web Fetch providers, execution, reference checks, scoring and report generation -live in this package. Browser Use remains independent in `harness/browser_use`. +live in this package. The fixed-action kernel benchmark remains independent in +`harness/browser_kernel`; autonomous browser-agent evaluation lives in +`harness/browser_agent`. From the repository root: diff --git a/pyproject.toml b/pyproject.toml index 8c78f98e7..f983fdfba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,8 +20,15 @@ reference-audit = [ ] browser-use-eval = [ "docker>=7,<8", + "json-repair>=0.54,<1", + "pillow>=11,<13", "playwright>=1.52,<2", + "python-dotenv>=1,<2", + "pyyaml>=6,<7", + "regex>=2025.7,<2027", + "requests>=2.32,<3", "tiktoken>=0.7,<1", + "webarena-verified==1.2.3", ] test = [ "pytest>=8,<10", @@ -38,8 +45,10 @@ markers = [ [tool.ruff] line-length = 100 include = [ - "harness/browser_use/**/*.py", - "tests/browser_use/**/*.py", + "harness/browser_agent/**/*.py", + "harness/browser_kernel/**/*.py", + "tests/browser_agent/**/*.py", + "tests/browser_kernel/**/*.py", ] target-version = "py311" @@ -47,7 +56,8 @@ target-version = "py311" select = ["B", "E", "F", "I", "UP"] [tool.ruff.lint.per-file-ignores] -"harness/browser_use/report.py" = ["E501"] +"harness/browser_kernel/report.py" = ["E501"] +"harness/browser_agent/report.py" = ["E501"] [build-system] requires = ["setuptools>=64"] diff --git a/research/browser-agent-benchmark-adoption.json b/research/browser-agent-benchmark-adoption.json new file mode 100644 index 000000000..e3e866b4a --- /dev/null +++ b/research/browser-agent-benchmark-adoption.json @@ -0,0 +1,104 @@ +{ + "schema_version": 1, + "verified_at": "2026-09-08", + "scope": "Public first-party model, system-card, paper, and official benchmark evidence. Search/research benchmarks are separated from browser-interaction benchmarks.", + "organizations": [ + { + "organization": "OpenAI", + "browser_interaction": ["WebArena", "WebVoyager", "Mind2Web"], + "computer_use": ["OSWorld 2.0", "ScreenSpot-Pro", "Agents' Last Exam"], + "agentic_search": ["BrowseComp"], + "notes": "GPT-6 Astra publishes computer-use scores and a Mind2Web latency comparison, but no WebArena-Verified score on its launch page.", + "sources": [ + "https://openai.com/index/gpt-6-astra/", + "https://openai.com/index/computer-using-agent/" + ] + }, + { + "organization": "Anthropic", + "browser_interaction": ["WebArena", "WebArena-Verified", "Online-Mind2Web"], + "computer_use": ["OSWorld 2.0"], + "agentic_search": [], + "notes": "The Claude Sonnet 4.6 system card documents official WebArena-Verified prompt/grader use and Average@5 reporting.", + "sources": [ + "https://www-cdn.anthropic.com/bbd8ef16d70b7a1665f14f306ee88b53f686aa75/Claude%20Sonnet%204.6%20System%20Card.pdf" + ] + }, + { + "organization": "Google DeepMind", + "browser_interaction": ["Online-Mind2Web", "WebVoyager"], + "computer_use": ["AndroidWorld"], + "agentic_search": [], + "notes": "Gemini 2.5 Computer Use reports both official-leaderboard and Browserbase-harness Online-Mind2Web results, illustrating harness sensitivity.", + "sources": [ + "https://storage.googleapis.com/deepmind-media/Model-Cards/Gemini-2-5-Computer-Use-Model-Card.pdf" + ] + }, + { + "organization": "THUDM / ChatGLM", + "browser_interaction": ["Mind2Web", "WebArena", "MiniWob++", "AutoWebBench"], + "computer_use": [], + "agentic_search": [], + "notes": "AutoWebGLM publishes adapters/evaluation assets, but modifies WebArena for its interaction system, so numbers are not automatically comparable with an unmodified harness.", + "sources": ["https://github.com/THUDM/AutoWebGLM"] + }, + { + "organization": "Moonshot AI / Kimi", + "browser_interaction": [], + "computer_use": [], + "agentic_search": ["BrowseComp", "DeepSearchQA", "ResearchRubrics"], + "notes": "The inspected official Kimi releases emphasize search/research benchmarks; no first-party browser-interaction evaluation was found in this audit. Do not count BrowseComp as UI browser use.", + "sources": ["https://github.com/MoonshotAI/Kimi-K3"] + }, + { + "organization": "DeepSeek", + "browser_interaction": [], + "computer_use": [], + "agentic_search": [], + "notes": "No first-party browser-interaction benchmark disclosure was found in the inspected official model materials. Absence of public evidence is not evidence of no internal evaluation.", + "sources": ["https://github.com/deepseek-ai"] + }, + { + "organization": "Browser Use", + "browser_interaction": ["BU Bench V1", "Online-Mind2Web", "Odysseys"], + "browser_infrastructure": ["Stealth Bench V1"], + "computer_use": [], + "agentic_search": [], + "notes": "The main product README highlights the open 100-task BU Bench and an Odysseys leaderboard result. The benchmark repository currently puts Stealth Bench V1 first, but that measures anti-bot browser infrastructure rather than end-to-end agent task completion. BU Bench, Online-Mind2Web, and Odysseys all use model-based judging in their documented workflows.", + "sources": [ + "https://github.com/browser-use/benchmark", + "https://github.com/browser-use/online-mind2web", + "https://github.com/browser-use/browser-use", + "https://github.com/ljang0/Odysseys" + ] + } + ], + "benchmark_families": [ + { + "family": "WebArena", + "adopters": ["OpenAI", "Anthropic", "THUDM / ChatGLM"], + "interaction": true + }, + { + "family": "Mind2Web", + "adopters": ["OpenAI", "Anthropic", "Google DeepMind", "THUDM / ChatGLM", "Browser Use"], + "interaction": true, + "warning": "Original Mind2Web, Online-Mind2Web, and Mind2Web 2 have different tasks and graders and must not share a score table." + }, + { + "family": "WebVoyager", + "adopters": ["OpenAI", "Google DeepMind"], + "interaction": true + }, + { + "family": "BU Bench V1", + "adopters": ["Browser Use"], + "interaction": true + }, + { + "family": "Odysseys", + "adopters": ["Browser Use"], + "interaction": true + } + ] +} diff --git a/scripts/build_browser_agent_report.py b/scripts/build_browser_agent_report.py new file mode 100644 index 000000000..5ceadde0c --- /dev/null +++ b/scripts/build_browser_agent_report.py @@ -0,0 +1,6 @@ +"""Build the autonomous browser-agent report.""" + +from harness.browser_agent.report import main + +if __name__ == "__main__": + main() diff --git a/scripts/check_browser_agent_report.py b/scripts/check_browser_agent_report.py new file mode 100644 index 000000000..dd287df51 --- /dev/null +++ b/scripts/check_browser_agent_report.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python3 +"""Validate the autonomous browser-agent report and its measured evidence.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +from harness.browser_agent.selection import load_pilot + + +def check( + report_dir: Path, + evidence_dir: Path, + selection_path: Path, + *, + require_complete: bool, +) -> None: + index = report_dir / "index.html" + audit_path = report_dir / "report-data.json" + attempts_path = evidence_dir / "attempts.jsonl" + manifest_path = evidence_dir / "manifest.json" + run_config_path = evidence_dir / "run-config.json" + if not index.is_file() or not audit_path.is_file(): + raise ValueError("report must contain index.html and report-data.json") + selection = load_pilot(selection_path) + audit = json.loads(audit_path.read_text(encoding="utf-8")) + if audit.get("selection_sha256") != selection.sha256: + raise ValueError("report was not built from the current pre-registered selection") + rows = [] + if attempts_path.exists(): + rows = [ + json.loads(line) + for line in attempts_path.read_text(encoding="utf-8").splitlines() + if line.strip() + ] + selected_ids = {task.task_id for task in selection.tasks} + expected_primary = { + (engine, task_id, 1) + for engine in ("chromium", "moli") + for task_id in selected_ids + } + by_key: dict[tuple[str, int, int], dict] = {} + for row in rows: + key = (str(row["engine"]), int(row["task_id"]), int(row["attempt"])) + if key in by_key: + raise ValueError(f"duplicate measured attempt: {key}") + if key[0] not in {"chromium", "moli"} or key[1] not in selected_ids: + raise ValueError(f"attempt outside the registered cohort: {key}") + if key[2] not in {1, 2}: + raise ValueError(f"more than one retry is not allowed: {key}") + by_key[key] = row + for engine, task_id, attempt in by_key: + if attempt == 2: + primary = by_key.get((engine, task_id, 1)) + if primary is None or float(primary["score"]) == 1.0: + raise ValueError( + f"retry is allowed only after a failed primary: {(engine, task_id)}" + ) + html = index.read_text(encoding="utf-8") + if "expected_response" in html or "reference_answer" in html: + raise ValueError("private evaluator answers must not be published") + if require_complete: + if not manifest_path.is_file() or not run_config_path.is_file(): + raise ValueError("complete evidence requires manifest.json and run-config.json") + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + run_config = json.loads(run_config_path.read_text(encoding="utf-8")) + if manifest.get("selection_sha256") != selection.sha256: + raise ValueError("manifest selection hash differs from the registered cohort") + if run_config.get("selection_sha256") != selection.sha256: + raise ValueError("run configuration differs from the registered cohort") + if run_config.get("reasoning_effort") != "low": + raise ValueError("pilot reasoning effort must be low") + missing = expected_primary - set(by_key) + if missing: + raise ValueError(f"missing {len(missing)} primary attempts") + missing_retries = { + (engine, task_id, 2) + for engine, task_id, _ in expected_primary + if float(by_key[(engine, task_id, 1)]["score"]) != 1.0 + and (engine, task_id, 2) not in by_key + } + if missing_retries: + raise ValueError(f"missing {len(missing_retries)} permitted failure retries") + if "测量尚未完整" in html: + raise ValueError("complete publication cannot contain an incomplete-results notice") + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("report", type=Path) + parser.add_argument("--evidence", required=True, type=Path) + parser.add_argument( + "--selection", + type=Path, + default=Path("evaluation/browser-agent/webarena-verified-pilot.json"), + ) + parser.add_argument("--require-complete", action="store_true") + args = parser.parse_args() + check( + args.report, + args.evidence, + args.selection, + require_complete=args.require_complete, + ) + print("Browser-agent report integrity check passed") + + +if __name__ == "__main__": + main() diff --git a/scripts/package_browser_agent_artifact_hub.py b/scripts/package_browser_agent_artifact_hub.py new file mode 100644 index 000000000..a58562290 --- /dev/null +++ b/scripts/package_browser_agent_artifact_hub.py @@ -0,0 +1,38 @@ +#!/usr/bin/env python3 +"""Package the complete browser-agent report for Artifact Hub.""" + +from __future__ import annotations + +import argparse +import zipfile +from pathlib import Path + +from scripts.check_browser_agent_report import check + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--report", type=Path, default=Path("reports/browser-agent")) + parser.add_argument("--evidence", required=True, type=Path) + parser.add_argument( + "--output", type=Path, default=Path("publish/browser-agent-sites.zip") + ) + args = parser.parse_args() + check( + args.report, + args.evidence, + Path("evaluation/browser-agent/webarena-verified-pilot.json"), + require_complete=True, + ) + args.output.parent.mkdir(parents=True, exist_ok=True) + with zipfile.ZipFile(args.output, "w", zipfile.ZIP_DEFLATED) as archive: + for path in sorted(args.report.rglob("*")): + if path.is_file(): + if path.is_symlink(): + raise ValueError(f"publication cannot contain symlinks: {path}") + archive.write(path, path.relative_to(args.report)) + print(args.output) + + +if __name__ == "__main__": + main() diff --git a/scripts/publish_browser_agent_artifact_hub.py b/scripts/publish_browser_agent_artifact_hub.py new file mode 100644 index 000000000..23a3f3a05 --- /dev/null +++ b/scripts/publish_browser_agent_artifact_hub.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +"""Publish the validated browser-agent report as a new Artifact Hub site.""" + +from __future__ import annotations + +import json +import os +import urllib.request +import uuid +from pathlib import Path + +from harness.web_fetch.artifact_auth import load_token + +BASE = "https://artifact-hub.dev.lexmount.net" +ARCHIVE = Path("publish/browser-agent-sites.zip") +RECEIPT = Path("publish/browser-agent-artifact-hub-receipt.json") + + +def request(path: str, data: bytes | None = None, content_type: str | None = None): + headers = { + "Authorization": "Bearer " + load_token(), + "User-Agent": "Browser-Agent-Report-Publisher/1.0", + } + if content_type: + headers["Content-Type"] = content_type + req = urllib.request.Request(BASE + path, data=data, headers=headers) + with urllib.request.urlopen(req, timeout=240) as response: + return response.status, response.read() + + +def main() -> None: + if RECEIPT.exists(): + result = json.loads(RECEIPT.read_text(encoding="utf-8")) + else: + if not ARCHIVE.is_file(): + raise ValueError(f"missing validated package: {ARCHIVE}") + boundary = "----browseragent" + uuid.uuid4().hex + parts = [] + for key, value in ( + ("mode", "zip"), + ("title", "Browser Use 公开评测与 WebArena-Verified 对比"), + ): + parts.append( + ( + f'--{boundary}\r\nContent-Disposition: form-data; name="{key}"' + f"\r\n\r\n{value}\r\n" + ).encode() + ) + parts.append( + ( + f'--{boundary}\r\nContent-Disposition: form-data; name="file"; ' + 'filename="browser-agent.zip"\r\nContent-Type: application/zip\r\n\r\n' + ).encode() + + ARCHIVE.read_bytes() + + b"\r\n" + ) + parts.append(f"--{boundary}--\r\n".encode()) + status, raw = request( + "/api/sites", + b"".join(parts), + "multipart/form-data; boundary=" + boundary, + ) + result = json.loads(raw) + if status != 200 or not result.get("slug"): + raise RuntimeError(result) + RECEIPT.parent.mkdir(parents=True, exist_ok=True) + RECEIPT.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") + os.chmod(RECEIPT, 0o600) + if not result.get("share_url"): + status, raw = request( + f"/api/sites/{result['slug']}/shares", + json.dumps({"policy": "public"}).encode(), + "application/json", + ) + if status != 200: + raise RuntimeError(raw.decode(errors="replace")) + result["share_url"] = json.loads(raw)["url"] + RECEIPT.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") + os.chmod(RECEIPT, 0o600) + public = urllib.request.Request( + result["share_url"], headers={"User-Agent": "Browser-Agent-Report-Publisher/1.0"} + ) + with urllib.request.urlopen(public, timeout=60) as response: + if response.status != 200: + raise RuntimeError(f"public report returned {response.status}") + print(json.dumps({"slug": result["slug"], "url": result["share_url"]}, ensure_ascii=False)) + + +if __name__ == "__main__": + main() diff --git a/tests/browser_agent/test_model.py b/tests/browser_agent/test_model.py new file mode 100644 index 000000000..89f7f003c --- /dev/null +++ b/tests/browser_agent/test_model.py @@ -0,0 +1,143 @@ +from harness.browser_agent.model import ( + AstraResponsesModel, + cow_messages_to_responses, + cow_tools_to_responses, + response_to_chat_chunk, +) + + +class _Request: + system = "system" + tools = [] + max_tokens = 100 + + def __init__(self, messages): + self.messages = messages + + +class _Response: + def __init__(self, payload): + self._payload = payload + + def raise_for_status(self): + return None + + def json(self): + return self._payload + + +class _Session: + def __init__(self): + self.payloads = [] + + def post(self, _url, **kwargs): + self.payloads.append(kwargs["json"]) + call_id = f"call_{len(self.payloads)}" + return _Response( + { + "id": f"resp_{len(self.payloads)}", + "output": [ + { + "type": "function_call", + "call_id": call_id, + "name": "browser", + "arguments": "{}", + } + ], + } + ) + + +def test_translates_tool_round_trip() -> None: + messages = [ + {"role": "user", "content": [{"type": "text", "text": "open it"}]}, + { + "role": "assistant", + "content": [ + { + "type": "tool_use", + "id": "call_1", + "name": "browser", + "input": {"action": "navigate", "url": "https://example.com"}, + } + ], + }, + { + "role": "user", + "content": [ + {"type": "tool_result", "tool_use_id": "call_1", "content": "done"} + ], + }, + ] + assert cow_messages_to_responses(messages) == [ + {"role": "user", "content": "open it"}, + { + "type": "function_call", + "call_id": "call_1", + "name": "browser", + "arguments": '{"action":"navigate","url":"https://example.com"}', + }, + {"type": "function_call_output", "call_id": "call_1", "output": "done"}, + ] + + +def test_translates_tool_schema_and_response() -> None: + tools = cow_tools_to_responses( + [{"name": "browser", "description": "Browse", "input_schema": {"type": "object"}}] + ) + assert tools[0]["name"] == "browser" + chunk = response_to_chat_chunk( + { + "id": "resp_1", + "output": [ + { + "type": "function_call", + "call_id": "call_2", + "name": "browser", + "arguments": '{"action":"snapshot"}', + } + ], + } + ) + assert chunk["choices"][0]["finish_reason"] == "tool_calls" + assert chunk["choices"][0]["delta"]["tool_calls"][0]["id"] == "call_2" + + +def test_model_continues_responses_conversation_without_resending_assistant_call() -> None: + session = _Session() + model = AstraResponsesModel(api_key="test", session=session) + list(model.call_stream(_Request([{"role": "user", "content": "open it"}]))) + list( + model.call_stream( + _Request( + [ + {"role": "user", "content": "open it"}, + { + "role": "assistant", + "content": [ + { + "type": "tool_use", + "id": "call_1", + "name": "browser", + "input": {}, + } + ], + }, + { + "role": "user", + "content": [ + { + "type": "tool_result", + "tool_use_id": "call_1", + "content": "done", + } + ], + }, + ] + ) + ) + ) + assert session.payloads[1]["previous_response_id"] == "resp_1" + assert session.payloads[1]["input"] == [ + {"type": "function_call_output", "call_id": "call_1", "output": "done"} + ] diff --git a/tests/browser_agent/test_report.py b/tests/browser_agent/test_report.py new file mode 100644 index 000000000..45bf66339 --- /dev/null +++ b/tests/browser_agent/test_report.py @@ -0,0 +1,67 @@ +import json +from pathlib import Path + +from harness.browser_agent.report import _paired_summary, build_report + + +def test_paired_summary_uses_primary_attempts_only() -> None: + rows = [ + {"engine": "chromium", "task_id": 1, "attempt": 1, "score": 1}, + {"engine": "moli", "task_id": 1, "attempt": 1, "score": 0}, + {"engine": "moli", "task_id": 1, "attempt": 2, "score": 1}, + {"engine": "chromium", "task_id": 2, "attempt": 1, "score": 0}, + {"engine": "moli", "task_id": 2, "attempt": 1, "score": 1}, + ] + + summary = _paired_summary(rows) + + assert summary["paired"] == 2 + assert summary["chromium_only"] == 1 + assert summary["moli_only"] == 1 + assert summary["mcnemar_exact_p"] == 1.0 + + +def test_report_keeps_primary_score_and_retry_recovery_separate(tmp_path: Path) -> None: + evidence = tmp_path / "evidence" + output = tmp_path / "report" + evidence.mkdir() + rows = [ + { + "engine": "chromium", + "task_id": 44, + "tier": "easy", + "attempt": 1, + "score": 0, + "duration_ms": 1000, + }, + { + "engine": "chromium", + "task_id": 44, + "tier": "easy", + "attempt": 2, + "score": 1, + "duration_ms": 900, + }, + { + "engine": "moli", + "task_id": 44, + "tier": "easy", + "attempt": 1, + "score": 1, + "duration_ms": 800, + }, + ] + (evidence / "attempts.jsonl").write_text( + "\n".join(json.dumps(row) for row in rows) + "\n", encoding="utf-8" + ) + report = build_report( + evidence=evidence, + output=output, + selection_path=Path("evaluation/browser-agent/webarena-verified-pilot.json"), + ) + text = report.read_text(encoding="utf-8") + assert "0/1" in text + assert "1/1" in text + data = json.loads((output / "report-data.json").read_text(encoding="utf-8")) + assert data["summary"]["chromium"]["score"] == 0 + assert data["summary"]["chromium"]["recovered"] == 1 diff --git a/tests/browser_agent/test_runner.py b/tests/browser_agent/test_runner.py new file mode 100644 index 000000000..edd30f0a5 --- /dev/null +++ b/tests/browser_agent/test_runner.py @@ -0,0 +1,27 @@ +from webarena_verified.types.task import WebArenaSite + +from harness.browser_agent.runner import SITE_AUTH_HEADERS, _configure_benchmark, _user_prompt + + +def test_official_container_urls_and_header_auth() -> None: + benchmark = _configure_benchmark() + + assert ( + benchmark.config.get_environment(WebArenaSite.SHOPPING_ADMIN).active_url + == "http://host.docker.internal:7780/admin" + ) + assert SITE_AUTH_HEADERS == { + WebArenaSite.SHOPPING: "X-M2-Customer-Auto-Login", + WebArenaSite.SHOPPING_ADMIN: "X-M2-Admin-Auto-Login", + WebArenaSite.REDDIT: "X-Postmill-Auto-Login", + } + + +def test_user_prompt_exposes_schema_but_not_reference_answer() -> None: + benchmark = _configure_benchmark() + task = benchmark.get_task(41) + prompt = _user_prompt(task, ["http://host.docker.internal:7780/admin"]) + + assert "task_type: RETRIEVE" in prompt + assert '"type":"array"' in prompt + assert "hollister" not in prompt diff --git a/tests/browser_agent/test_selection.py b/tests/browser_agent/test_selection.py new file mode 100644 index 000000000..e6216d68e --- /dev/null +++ b/tests/browser_agent/test_selection.py @@ -0,0 +1,13 @@ +from pathlib import Path + +from harness.browser_agent.selection import TIER_COUNTS, load_pilot + + +def test_pilot_has_pre_registered_tier_counts() -> None: + selection = load_pilot(Path("evaluation/browser-agent/webarena-verified-pilot.json")) + assert len(selection.tasks) == 30 + assert { + tier: sum(task.tier == tier for task in selection.tasks) for tier in TIER_COUNTS + } == TIER_COUNTS + assert sum(task.official_hard for task in selection.tasks) == 16 + assert len(selection.sha256) == 64 diff --git a/tests/browser_use/test_diagnostics.py b/tests/browser_kernel/test_diagnostics.py similarity index 97% rename from tests/browser_use/test_diagnostics.py rename to tests/browser_kernel/test_diagnostics.py index b956a2e0b..d3139b7ae 100644 --- a/tests/browser_use/test_diagnostics.py +++ b/tests/browser_kernel/test_diagnostics.py @@ -5,8 +5,8 @@ import pytest -from harness.browser_use import diagnostics -from harness.browser_use.diagnostics import build_failure_groups, classify_failure +from harness.browser_kernel import diagnostics +from harness.browser_kernel.diagnostics import build_failure_groups, classify_failure @pytest.mark.parametrize( diff --git a/tests/browser_use/test_executor.py b/tests/browser_kernel/test_executor.py similarity index 98% rename from tests/browser_use/test_executor.py rename to tests/browser_kernel/test_executor.py index 886a4ee67..def0f1c05 100644 --- a/tests/browser_use/test_executor.py +++ b/tests/browser_kernel/test_executor.py @@ -4,7 +4,7 @@ import pytest -from harness.browser_use.executor import execute_steps, judge_outputs, validate_outputs +from harness.browser_kernel.executor import execute_steps, judge_outputs, validate_outputs class FakePage: diff --git a/tests/browser_use/test_metrics.py b/tests/browser_kernel/test_metrics.py similarity index 99% rename from tests/browser_use/test_metrics.py rename to tests/browser_kernel/test_metrics.py index 3fcce2b7d..f5b36408b 100644 --- a/tests/browser_use/test_metrics.py +++ b/tests/browser_kernel/test_metrics.py @@ -1,6 +1,6 @@ from __future__ import annotations -from harness.browser_use.metrics import ( +from harness.browser_kernel.metrics import ( CounterSnapshot, DockerStatsResources, LinuxContainerResources, diff --git a/tests/browser_use/test_real_smoke.py b/tests/browser_kernel/test_real_smoke.py similarity index 98% rename from tests/browser_use/test_real_smoke.py rename to tests/browser_kernel/test_real_smoke.py index edffbb147..7879b1f1b 100644 --- a/tests/browser_use/test_real_smoke.py +++ b/tests/browser_kernel/test_real_smoke.py @@ -9,7 +9,7 @@ import pytest from playwright.sync_api import sync_playwright -from harness.browser_use.runner import ( +from harness.browser_kernel.runner import ( ENGINE_IMAGES, ENGINE_PORTS, _cleanup_runtime, diff --git a/tests/browser_use/test_report.py b/tests/browser_kernel/test_report.py similarity index 93% rename from tests/browser_use/test_report.py rename to tests/browser_kernel/test_report.py index eacdab869..0a2b87c8a 100644 --- a/tests/browser_use/test_report.py +++ b/tests/browser_kernel/test_report.py @@ -4,7 +4,7 @@ import re from pathlib import Path -from harness.browser_use.report import ( +from harness.browser_kernel.report import ( METRICS, _absolute_metric_cell, _annotate_source_marks, @@ -88,7 +88,7 @@ def test_resource_tables_separate_absolute_values_from_paired_ratios(): def test_report_uses_fixed_latest_filename_and_removes_legacy_versions(tmp_path): reports = tmp_path / "reports" reports.mkdir() - (reports / "browser-use-evaluation-20260101.md").write_text("old", encoding="utf-8") + (reports / "browser-kernel-evaluation-20260101.md").write_text("old", encoding="utf-8") path = write_latest_report( reports, @@ -177,10 +177,10 @@ def test_report_uses_fixed_latest_filename_and_removes_legacy_versions(tmp_path) }, ) - assert path.name == "browser-use-evaluation-latest.md" + assert path.name == "browser-kernel-evaluation-latest.md" assert path.exists() - assert not (reports / "browser-use-evaluation-20260101.md").exists() - rendered = (reports / "browser-use-evaluation-latest.html").read_text(encoding="utf-8") + assert not (reports / "browser-kernel-evaluation-20260101.md").exists() + rendered = (reports / "browser-kernel-evaluation-latest.html").read_text(encoding="utf-8") assert "结论与产品建议" in rendered assert 'lang="zh-CN"' in rendered assert "报告分页" in rendered @@ -196,15 +196,15 @@ def test_report_uses_fixed_latest_filename_and_removes_legacy_versions(tmp_path) assert "边际分位数相除" in rendered assert "结果重试恢复" not in rendered assert "首次结果正确" not in rendered - assert (reports / "browser-use-evaluation-latest-suite.html").exists() - assert (reports / "browser-use-evaluation-latest-method.html").exists() - assert not (reports / "browser-use-evaluation-latest-results.html").exists() - assert (reports / "browser-use-evaluation-latest-analysis.html").exists() - assert (reports / "browser-use-evaluation-latest-appendix.html").exists() - assert (reports / "browser-use-evaluation-latest-data.json").exists() + assert (reports / "browser-kernel-evaluation-latest-suite.html").exists() + assert (reports / "browser-kernel-evaluation-latest-method.html").exists() + assert not (reports / "browser-kernel-evaluation-latest-results.html").exists() + assert (reports / "browser-kernel-evaluation-latest-analysis.html").exists() + assert (reports / "browser-kernel-evaluation-latest-appendix.html").exists() + assert (reports / "browser-kernel-evaluation-latest-data.json").exists() assert "总体结果" not in rendered assert "第 1/5 页" in rendered - suite = (reports / "browser-use-evaluation-latest-suite.html").read_text(encoding="utf-8") + suite = (reports / "browser-kernel-evaluation-latest-suite.html").read_text(encoding="utf-8") assert "1公网任务" in suite assert "公网测试页面" in suite assert 'href="https://example.com"' in suite @@ -220,14 +220,14 @@ def test_report_uses_fixed_latest_filename_and_removes_legacy_versions(tmp_path) assert 'data-search="任务01 任务 01 example' in suite for html_path in reports.glob("*.html"): assert "人工复核" not in html_path.read_text(encoding="utf-8") - audit = json.loads((reports / "browser-use-evaluation-latest-data.json").read_text()) + audit = json.loads((reports / "browser-kernel-evaluation-latest-data.json").read_text()) assert "human_review" not in audit["ground_truth_validation"] - method = (reports / "browser-use-evaluation-latest-method.html").read_text(encoding="utf-8") + method = (reports / "browser-kernel-evaluation-latest-method.html").read_text(encoding="utf-8") assert "RX = Receive" in method assert "P50 与中位数是同一个统计量" in method assert "Codex" in method assert "# Browser Use Evaluation" not in rendered + assert "
    # Browser Kernel Microbenchmark" not in rendered
     
     
     def test_report_links_to_full_failures_and_tool_trace_on_detail_pages(tmp_path):
    @@ -318,7 +318,9 @@ def test_report_links_to_full_failures_and_tool_trace_on_detail_pages(tmp_path):
                 },
             },
         )
    -    analysis = (reports / "browser-use-evaluation-latest-analysis.html").read_text(encoding="utf-8")
    +    analysis = (
    +        reports / "browser-kernel-evaluation-latest-analysis.html"
    +    ).read_text(encoding="utf-8")
         assert "逐任务分析与原始证据" in analysis
         assert "TRACE_ONLY_SEARCH_MARKER" in analysis
         assert "c.dataset.search" in analysis
    @@ -360,7 +362,7 @@ def test_report_links_to_full_failures_and_tool_trace_on_detail_pages(tmp_path):
         assert "LLM 根因假设" not in analysis
         assert "未启用 LLM" not in analysis
         assert "时序机制研究" in analysis
    -    homepage = (reports / "browser-use-evaluation-latest.html").read_text(encoding="utf-8")
    +    homepage = (reports / "browser-kernel-evaluation-latest.html").read_text(encoding="utf-8")
         assert 'class="failure-shortcut"' not in homepage
         assert f'href="{detail_files[0].name}"' not in homepage
         assert "#provider=moli&status=fail" in homepage
    @@ -469,7 +471,7 @@ def test_detail_preserves_success_and_divergent_actual_calls():
     
     
     def test_all_published_suite_tasks_link_to_public_web_targets(tmp_path):
    -    suite_file = Path(__file__).resolve().parents[2] / "examples/browser_use/tasks.v4.json"
    +    suite_file = Path(__file__).resolve().parents[2] / "examples/browser_kernel/tasks.v4.json"
         raw_tasks = json.loads(suite_file.read_text(encoding="utf-8"))["tasks"]
         suite = [
             {
    @@ -485,7 +487,7 @@ def test_all_published_suite_tasks_link_to_public_web_targets(tmp_path):
             manifest={"run_id": "suite-links", "suite": suite},
             summary={"functional": {}, "resource_comparison": {"by_engine": {}}},
         )
    -    rendered = (reports / "browser-use-evaluation-latest-suite.html").read_text(encoding="utf-8")
    +    rendered = (reports / "browser-kernel-evaluation-latest-suite.html").read_text(encoding="utf-8")
         links = re.findall(
             r'公网测试页面]+href="(https://[^\"]+)"',
             rendered,
    diff --git a/tests/browser_use/test_runner.py b/tests/browser_kernel/test_runner.py
    similarity index 96%
    rename from tests/browser_use/test_runner.py
    rename to tests/browser_kernel/test_runner.py
    index 3a8c51fd5..c2b9281b5 100644
    --- a/tests/browser_use/test_runner.py
    +++ b/tests/browser_kernel/test_runner.py
    @@ -9,8 +9,8 @@
     
     import pytest
     
    -from harness.browser_use import diagnostics, runner
    -from harness.browser_use.runner import (
    +from harness.browser_kernel import diagnostics, runner
    +from harness.browser_kernel.runner import (
         ENGINE_PORTS,
         _cleanup_runtime,
         _external_websocket_url,
    @@ -18,7 +18,7 @@
         _prepare_page,
         _published_endpoint,
     )
    -from harness.browser_use.suite import BrowserTask, TaskSuite
    +from harness.browser_kernel.suite import BrowserTask, TaskSuite
     
     
     def test_browser_engines_publish_their_external_cdp_ports():
    @@ -118,7 +118,7 @@ def diagnostic_failure(*a, **k):
     
     
     def test_moli_image_installs_its_verified_dynamic_font_dependency():
    -    dockerfile = Path("examples/browser_use/docker/moli.Dockerfile").read_text(encoding="utf-8")
    +    dockerfile = Path("examples/browser_kernel/docker/moli.Dockerfile").read_text(encoding="utf-8")
     
         assert "fontconfig" in dockerfile
     
    diff --git a/tests/browser_use/test_suite.py b/tests/browser_kernel/test_suite.py
    similarity index 91%
    rename from tests/browser_use/test_suite.py
    rename to tests/browser_kernel/test_suite.py
    index 18f21d8ef..8197988ab 100644
    --- a/tests/browser_use/test_suite.py
    +++ b/tests/browser_kernel/test_suite.py
    @@ -7,7 +7,7 @@
     
     import pytest
     
    -from harness.browser_use.suite import load_suite, validate_ground_truth_evidence
    +from harness.browser_kernel.suite import load_suite, validate_ground_truth_evidence
     
     
     def _task(task_id, calls, expected="Example"):
    @@ -102,7 +102,7 @@ def test_load_suite_rejects_duplicate_task_ids(tmp_path):
     
     
     def test_suite_has_thirty_ground_truth_tasks_with_requested_difficulty_mix():
    -    suite = load_suite("examples/browser_use/tasks.v4.json")
    +    suite = load_suite("examples/browser_kernel/tasks.v4.json")
     
         assert len(suite.tasks) == 30
         assert len({task.task_id for task in suite.tasks}) == 30
    @@ -130,7 +130,7 @@ def test_suite_has_thirty_ground_truth_tasks_with_requested_difficulty_mix():
     
     
     def test_version_language_path_targets_desktop_controls_and_waits_for_navigation():
    -    suite = load_suite("examples/browser_use/tasks.v4.json")
    +    suite = load_suite("examples/browser_kernel/tasks.v4.json")
         task = next(t for t in suite.tasks if t.task_id == "python-version-language")
         selects = [call for call in task.tool_calls if call["action"] == "select"]
         assert len(selects) == 3
    @@ -140,7 +140,7 @@ def test_version_language_path_targets_desktop_controls_and_waits_for_navigation
     
     
     def test_suite_uses_public_targets_and_repository_derived_capability_coverage():
    -    suite = load_suite("examples/browser_use/tasks.v4.json")
    +    suite = load_suite("examples/browser_kernel/tasks.v4.json")
         assert all(task.source.get("kind") == "public_web" for task in suite.tasks)
         assert all(str(task.source.get("url") or "").startswith("https://") for task in suite.tasks)
         basis_urls = [str(url) for task in suite.tasks for url in task.source.get("design_basis") or []]
    @@ -159,9 +159,9 @@ def test_suite_uses_public_targets_and_repository_derived_capability_coverage():
     
     
     def test_codex_browser_ground_truth_evidence_matches_dataset_exactly():
    -    dataset_path = Path("examples/browser_use/tasks.v4.json")
    +    dataset_path = Path("examples/browser_kernel/tasks.v4.json")
         evidence = json.loads(
    -        Path("examples/browser_use/ground_truth_validation.json").read_text(encoding="utf-8")
    +        Path("examples/browser_kernel/ground_truth_validation.json").read_text(encoding="utf-8")
         )
         assert evidence["dataset_sha256"] == hashlib.sha256(dataset_path.read_bytes()).hexdigest()
         assert evidence["validator"] == "Codex Desktop in-app Browser Tool"
    @@ -182,10 +182,10 @@ def test_codex_browser_ground_truth_evidence_matches_dataset_exactly():
     
     
     def test_ground_truth_validation_rejects_evidence_for_another_suite(tmp_path):
    -    dataset_path = Path("examples/browser_use/tasks.v4.json")
    +    dataset_path = Path("examples/browser_kernel/tasks.v4.json")
         suite = load_suite(dataset_path)
         evidence = json.loads(
    -        Path("examples/browser_use/ground_truth_validation.json").read_text(encoding="utf-8")
    +        Path("examples/browser_kernel/ground_truth_validation.json").read_text(encoding="utf-8")
         )
         evidence["dataset_sha256"] = "0" * 64
         evidence_path = tmp_path / "evidence.json"
    diff --git a/tests/browser_use/test_verify_metrics.py b/tests/browser_kernel/test_verify_metrics.py
    similarity index 95%
    rename from tests/browser_use/test_verify_metrics.py
    rename to tests/browser_kernel/test_verify_metrics.py
    index 5471c48fa..378b9b969 100644
    --- a/tests/browser_use/test_verify_metrics.py
    +++ b/tests/browser_kernel/test_verify_metrics.py
    @@ -6,11 +6,11 @@
     
     import pytest
     
    -from harness.browser_use.metrics import summarize_attempts
    +from harness.browser_kernel.metrics import summarize_attempts
     
     
     def test_independent_audit_detects_tampered_aggregate_and_paired_operands(tmp_path):
    -    script = Path(__file__).resolve().parents[2] / "examples/browser_use/verify_metrics.py"
    +    script = Path(__file__).resolve().parents[2] / "examples/browser_kernel/verify_metrics.py"
         spec = importlib.util.spec_from_file_location("verify_metrics", script)
         module = importlib.util.module_from_spec(spec)
         spec.loader.exec_module(module)
    diff --git a/uv.lock b/uv.lock
    index 2944f5d77..f56dd0541 100644
    --- a/uv.lock
    +++ b/uv.lock
    @@ -2,17 +2,35 @@ version = 1
     revision = 2
     requires-python = ">=3.11"
     
    +[[package]]
    +name = "annotated-types"
    +version = "0.8.0"
    +source = { registry = "https://pypi.org/simple" }
    +sdist = { url = "https://files.pythonhosted.org/packages/5f/56/a8120250d128bed162cd73c76d45f6ef9991f3e068f62a8ee060afa3104a/annotated_types-0.8.0.tar.gz", hash = "sha256:13b2beaad985e05e2d6407ee4c4f35590b11f8d693a258a561055cac8f64cab7", size = 15893, upload-time = "2026-07-23T20:16:13.995Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/99/91/8acff4f5e50511b911bbccb72b8628a49c68ce14148cd9f6431094859a90/annotated_types-0.8.0-py3-none-any.whl", hash = "sha256:f072f4d804ea359e4eaf198b1af7a8b0943881a87f31bb764f8bf219bb9419e0", size = 13427, upload-time = "2026-07-23T20:16:12.938Z" },
    +]
    +
    +[[package]]
    +name = "attrs"
    +version = "26.1.0"
    +source = { registry = "https://pypi.org/simple" }
    +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" },
    +]
    +
     [[package]]
     name = "beautifulsoup4"
    -version = "4.15.0"
    +version = "4.14.2"
     source = { registry = "https://pypi.org/simple" }
     dependencies = [
         { name = "soupsieve" },
         { name = "typing-extensions" },
     ]
    -sdist = { url = "https://files.pythonhosted.org/packages/43/65/318323f98dbee45d42dff61d8f047181bc6f2268a9068cfad035a46be5af/beautifulsoup4-4.15.0.tar.gz", hash = "sha256:288e3ca7d54b06f2ac191970bc275c1939cb46d450b255bf6718b04aa37ab4f7", size = 632571, upload-time = "2026-06-07T16:44:20.453Z" }
    +sdist = { url = "https://files.pythonhosted.org/packages/77/e9/df2358efd7659577435e2177bfa69cba6c33216681af51a707193dec162a/beautifulsoup4-4.14.2.tar.gz", hash = "sha256:2a98ab9f944a11acee9cc848508ec28d9228abfd522ef0fad6a02a72e0ded69e", size = 625822, upload-time = "2025-09-29T10:05:42.613Z" }
     wheels = [
    -    { url = "https://files.pythonhosted.org/packages/88/c6/92fcd42f1ba33e1184263f25bfabf3d27c383410470f169e4b8163bf9c17/beautifulsoup4-4.15.0-py3-none-any.whl", hash = "sha256:d6f88de62e1d4e38ecb1077eb9724cd0eff29d2a08ca16a401e9b9e93f117cf9", size = 109924, upload-time = "2026-06-07T16:44:21.566Z" },
    +    { url = "https://files.pythonhosted.org/packages/94/fe/3aed5d0be4d404d12d36ab97e2f1791424d9ca39c2f754a6285d59a3b01d/beautifulsoup4-4.14.2-py3-none-any.whl", hash = "sha256:5ef6fa3a8cbece8488d66985560f97ed091e22bbc4e9c2338508a9d5de6d4515", size = 106392, upload-time = "2025-09-29T10:05:43.771Z" },
     ]
     
     [[package]]
    @@ -23,8 +41,15 @@ source = { editable = "." }
     [package.optional-dependencies]
     browser-use-eval = [
         { name = "docker" },
    +    { name = "json-repair" },
    +    { name = "pillow" },
         { name = "playwright" },
    +    { name = "python-dotenv" },
    +    { name = "pyyaml" },
    +    { name = "regex" },
    +    { name = "requests" },
         { name = "tiktoken" },
    +    { name = "webarena-verified" },
     ]
     headless-eval = [
         { name = "playwright" },
    @@ -44,14 +69,21 @@ test = [
     requires-dist = [
         { name = "beautifulsoup4", marker = "extra == 'reference-audit'", specifier = ">=4.12,<5" },
         { name = "docker", marker = "extra == 'browser-use-eval'", specifier = ">=7,<8" },
    +    { name = "json-repair", marker = "extra == 'browser-use-eval'", specifier = ">=0.54,<1" },
    +    { name = "pillow", marker = "extra == 'browser-use-eval'", specifier = ">=11,<13" },
         { name = "playwright", marker = "extra == 'browser-use-eval'", specifier = ">=1.52,<2" },
         { name = "playwright", marker = "extra == 'headless-eval'", specifier = "==1.58.0" },
         { name = "pypdf", marker = "extra == 'headless-eval'", specifier = ">=5,<7" },
         { name = "pypdf", marker = "extra == 'reference-audit'", specifier = ">=5,<7" },
         { name = "pytest", marker = "extra == 'test'", specifier = ">=8,<10" },
    +    { name = "python-dotenv", marker = "extra == 'browser-use-eval'", specifier = ">=1,<2" },
    +    { name = "pyyaml", marker = "extra == 'browser-use-eval'", specifier = ">=6,<7" },
    +    { name = "regex", marker = "extra == 'browser-use-eval'", specifier = ">=2025.7,<2027" },
    +    { name = "requests", marker = "extra == 'browser-use-eval'", specifier = ">=2.32,<3" },
         { name = "requests", marker = "extra == 'reference-audit'", specifier = ">=2.32,<3" },
         { name = "ruff", marker = "extra == 'test'", specifier = ">=0.9,<1" },
         { name = "tiktoken", marker = "extra == 'browser-use-eval'", specifier = ">=0.7,<1" },
    +    { name = "webarena-verified", marker = "extra == 'browser-use-eval'", specifier = "==1.2.3" },
     ]
     provides-extras = ["headless-eval", "reference-audit", "browser-use-eval", "test"]
     
    @@ -211,6 +243,15 @@ wheels = [
         { url = "https://files.pythonhosted.org/packages/cc/61/d01fc49b8dea277640b55a9e15960dbca9fdc8c9fde18e572d39c59f4019/charset_normalizer-3.5.1-py3-none-any.whl", hash = "sha256:6df0ec430f9a831772c23ca5a224cba36517a58a84bb32c32bb59a9fa67c47f6", size = 68658, upload-time = "2026-08-15T08:20:43.306Z" },
     ]
     
    +[[package]]
    +name = "click"
    +version = "8.5.0"
    +source = { registry = "https://pypi.org/simple" }
    +sdist = { url = "https://files.pythonhosted.org/packages/c7/0e/7fa0ef50764b67090eca4114772a2abf8b6148198475e54c660b97caeee6/click-8.5.0.tar.gz", hash = "sha256:ba0d2089de75ea0310e2dde03160e6ca10009947fb95a182f9b54021bb272e34", size = 382235, upload-time = "2026-08-26T13:33:14.56Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/58/50/6c0d534c5f134586a8e1ba4e330569e32f057e33372ae556463212fb4cd3/click-8.5.0-py3-none-any.whl", hash = "sha256:255bc9599cf7748b4b1a446ccc735421bd08a2ae529a8b88597d3de5664ee360", size = 125251, upload-time = "2026-08-26T13:33:12.928Z" },
    +]
    +
     [[package]]
     name = "colorama"
     version = "0.4.6"
    @@ -220,6 +261,29 @@ wheels = [
         { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
     ]
     
    +[[package]]
    +name = "compact-json"
    +version = "1.8.1"
    +source = { registry = "https://pypi.org/simple" }
    +dependencies = [
    +    { name = "importlib-resources" },
    +    { name = "setuptools" },
    +    { name = "wcwidth" },
    +]
    +sdist = { url = "https://files.pythonhosted.org/packages/5e/33/6d56e085b4525d2eb863773d1c08710fac59ef349f94e35f4ce3ff7979f2/compact_json-1.8.1.tar.gz", hash = "sha256:1dfcd53a05e27725fd73d694028c6d9a4b5e7fe8586d0980d0f7d5b5672ee144", size = 17975, upload-time = "2024-05-16T07:30:19.001Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/62/3d/32da265b9d062ca624b60431b36ef97a2946e5252a79c08e4e14b0f9c3af/compact_json-1.8.1-py3-none-any.whl", hash = "sha256:1232321c1eb65d48a341265cd289996c02fe4ce213ffc4cfe948c3faf985ea0a", size = 16909, upload-time = "2024-05-16T07:30:17.306Z" },
    +]
    +
    +[[package]]
    +name = "decorator"
    +version = "5.3.1"
    +source = { registry = "https://pypi.org/simple" }
    +sdist = { url = "https://files.pythonhosted.org/packages/60/8b/32f9823da46cde7df2087faa08cd98d01b908f8dcab982cdba9c84e85355/decorator-5.3.1.tar.gz", hash = "sha256:4cbcdd55a6efadb9dbea26b858f4fb3264567b52d69ca0d25b721b553f60ea82", size = 58084, upload-time = "2026-05-18T06:03:28.057Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/05/7f/798705f5296a58ca505d600456748d1be48078eac8a7050d8a98bc9edb89/decorator-5.3.1-py3-none-any.whl", hash = "sha256:f47fe6fdbd2edd623ecfe36875d37aba411624e2670dd395dddae1358689bb3c", size = 10365, upload-time = "2026-05-18T06:03:26.517Z" },
    +]
    +
     [[package]]
     name = "docker"
     version = "7.2.0"
    @@ -234,6 +298,76 @@ wheels = [
         { url = "https://files.pythonhosted.org/packages/75/23/529140fe1aab80fc6992f93a706deec709140a6397439139a054e1515c45/docker-7.2.0-py3-none-any.whl", hash = "sha256:a3f45fdeb9165e2d25d9a1d02ddf3bc70fb572cf5ebbf9b58558c22caf29b71f", size = 148775, upload-time = "2026-07-09T14:53:45.224Z" },
     ]
     
    +[[package]]
    +name = "flexcache"
    +version = "0.3"
    +source = { registry = "https://pypi.org/simple" }
    +dependencies = [
    +    { name = "typing-extensions" },
    +]
    +sdist = { url = "https://files.pythonhosted.org/packages/55/b0/8a21e330561c65653d010ef112bf38f60890051d244ede197ddaa08e50c1/flexcache-0.3.tar.gz", hash = "sha256:18743bd5a0621bfe2cf8d519e4c3bfdf57a269c15d1ced3fb4b64e0ff4600656", size = 15816, upload-time = "2024-03-09T03:21:07.555Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/27/cd/c883e1a7c447479d6e13985565080e3fea88ab5a107c21684c813dba1875/flexcache-0.3-py3-none-any.whl", hash = "sha256:d43c9fea82336af6e0115e308d9d33a185390b8346a017564611f1466dcd2e32", size = 13263, upload-time = "2024-03-09T03:21:05.635Z" },
    +]
    +
    +[[package]]
    +name = "flexparser"
    +version = "0.4"
    +source = { registry = "https://pypi.org/simple" }
    +dependencies = [
    +    { name = "typing-extensions" },
    +]
    +sdist = { url = "https://files.pythonhosted.org/packages/82/99/b4de7e39e8eaf8207ba1a8fa2241dd98b2ba72ae6e16960d8351736d8702/flexparser-0.4.tar.gz", hash = "sha256:266d98905595be2ccc5da964fe0a2c3526fbbffdc45b65b3146d75db992ef6b2", size = 31799, upload-time = "2024-11-07T02:00:56.249Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/fe/5e/3be305568fe5f34448807976dc82fc151d76c3e0e03958f34770286278c1/flexparser-0.4-py3-none-any.whl", hash = "sha256:3738b456192dcb3e15620f324c447721023c0293f6af9955b481e91d00179846", size = 27625, upload-time = "2024-11-07T02:00:54.523Z" },
    +]
    +
    +[[package]]
    +name = "future"
    +version = "1.0.0"
    +source = { registry = "https://pypi.org/simple" }
    +sdist = { url = "https://files.pythonhosted.org/packages/a7/b2/4140c69c6a66432916b26158687e821ba631a4c9273c474343badf84d3ba/future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05", size = 1228490, upload-time = "2024-02-21T11:52:38.461Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/da/71/ae30dadffc90b9006d77af76b393cb9dfbfc9629f339fc1574a1c52e6806/future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216", size = 491326, upload-time = "2024-02-21T11:52:35.956Z" },
    +]
    +
    +[[package]]
    +name = "geocoder"
    +version = "1.38.1"
    +source = { registry = "https://pypi.org/simple" }
    +dependencies = [
    +    { name = "click" },
    +    { name = "future" },
    +    { name = "ratelim" },
    +    { name = "requests" },
    +    { name = "six" },
    +]
    +sdist = { url = "https://files.pythonhosted.org/packages/ea/0b/2ea440270c1efb7ac73450cb704344c8127f45dabff0bea48711dc9dd93a/geocoder-1.38.1.tar.gz", hash = "sha256:c9925374c961577d0aee403b09e6f8ea1971d913f011f00ca70c76beaf7a77e7", size = 64345, upload-time = "2018-04-04T12:34:47.649Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/4f/6b/13166c909ad2f2d76b929a4227c952630ebaf0d729f6317eb09cbceccbab/geocoder-1.38.1-py2.py3-none-any.whl", hash = "sha256:a733e1dfbce3f4e1a526cac03aadcedb8ed1239cf55bd7f3a23c60075121a834", size = 98590, upload-time = "2018-04-04T12:34:51.222Z" },
    +]
    +
    +[[package]]
    +name = "geographiclib"
    +version = "2.1"
    +source = { registry = "https://pypi.org/simple" }
    +sdist = { url = "https://files.pythonhosted.org/packages/df/78/4892343230a9d29faa1364564e525307a37e54ad776ea62c12129dbba704/geographiclib-2.1.tar.gz", hash = "sha256:6a6545e6262d0ed3522e13c515713718797e37ed8c672c31ad7b249f372ef108", size = 37004, upload-time = "2025-08-21T21:34:26Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/31/b3/802576f2ea5dcb48501bb162e4c7b7b3ca5654a42b2c968ef98a797a4c79/geographiclib-2.1-py3-none-any.whl", hash = "sha256:e2a873b9b9e7fc38721ad73d5f4e6c9ed140d428a339970f505c07056997d40b", size = 40740, upload-time = "2025-08-21T21:34:24.955Z" },
    +]
    +
    +[[package]]
    +name = "geopy"
    +version = "2.4.1"
    +source = { registry = "https://pypi.org/simple" }
    +dependencies = [
    +    { name = "geographiclib" },
    +]
    +sdist = { url = "https://files.pythonhosted.org/packages/0e/fd/ef6d53875ceab72c1fad22dbed5ec1ad04eb378c2251a6a8024bad890c3b/geopy-2.4.1.tar.gz", hash = "sha256:50283d8e7ad07d89be5cb027338c6365a32044df3ae2556ad3f52f4840b3d0d1", size = 117625, upload-time = "2023-11-23T21:49:32.734Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/e5/15/cf2a69ade4b194aa524ac75112d5caac37414b20a3a03e6865dfe0bd1539/geopy-2.4.1-py3-none-any.whl", hash = "sha256:ae8b4bc5c1131820f4d75fce9d4aaaca0c85189b3aa5d64c3dcaf5e3b7b882a7", size = 125437, upload-time = "2023-11-23T21:49:30.421Z" },
    +]
    +
     [[package]]
     name = "greenlet"
     version = "3.5.5"
    @@ -320,6 +454,15 @@ wheels = [
         { url = "https://files.pythonhosted.org/packages/57/b0/0e52c878c53f245edd3a11020f20979b3f490f245af532c7cae3027754b5/idna-3.19-py3-none-any.whl", hash = "sha256:815e7be7a7806d54abb586dc943addc79e8b2ee16915059658cbeff4b1b43bf4", size = 68550, upload-time = "2026-08-18T05:14:22.343Z" },
     ]
     
    +[[package]]
    +name = "importlib-resources"
    +version = "7.1.0"
    +source = { registry = "https://pypi.org/simple" }
    +sdist = { url = "https://files.pythonhosted.org/packages/e4/06/b56dfa750b44e86157093bc8fca0ab81dccbf5260510de4eaf1cb69b5b99/importlib_resources-7.1.0.tar.gz", hash = "sha256:0722d4c6212489c530f2a145a34c0a7a3b4721bc96a15fada5930e2a0b760708", size = 44985, upload-time = "2026-04-12T16:36:09.232Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/8a/db/55a262f3606bebcae07cc14095338471ad7c0bbcaa37707e6f0ee49725b7/importlib_resources-7.1.0-py3-none-any.whl", hash = "sha256:1bd7b48b4088eddb2cd16382150bb515af0bd2c70128194392725f82ad2c96a1", size = 37232, upload-time = "2026-04-12T16:36:08.219Z" },
    +]
    +
     [[package]]
     name = "iniconfig"
     version = "2.3.0"
    @@ -329,6 +472,83 @@ wheels = [
         { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" },
     ]
     
    +[[package]]
    +name = "jellyfish"
    +version = "1.2.1"
    +source = { registry = "https://pypi.org/simple" }
    +sdist = { url = "https://files.pythonhosted.org/packages/0b/14/fc5bdb637996df181e5c4fa3b15dcc27d33215e6c41753564ae453bdb40f/jellyfish-1.2.1.tar.gz", hash = "sha256:72d2fda61b23babe862018729be73c8b0dc12e3e6601f36f6e65d905e249f4db", size = 364417, upload-time = "2025-10-11T19:36:37.219Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/ee/67/5d5ec4004d92573cbccd33fc84d0ad61e523b29f7b17b062913b183961e1/jellyfish-1.2.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:9913789a98ccf49213fbb1dabc597847a0ec33d3b0e151689498f4b38ba9be0f", size = 325488, upload-time = "2025-10-11T19:35:06.87Z" },
    +    { url = "https://files.pythonhosted.org/packages/83/21/6cf3add349cd0002cc586178bd8f1fd006894e5c70f959a8db5507cfe075/jellyfish-1.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4e36d9000d4f7e1a35689a74ec7749d27a216dfa6c47cac2e5ad3de8a523bd69", size = 320226, upload-time = "2025-10-11T19:35:08.314Z" },
    +    { url = "https://files.pythonhosted.org/packages/1c/ed/b5458b09482913caece2e9f807599318e48490b01c3c3134b636ecd7af8c/jellyfish-1.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7853d2ed7d6929c029312ec849410f1ea7ae76ce72ad1140fb73f6e8a1e6aa4f", size = 353091, upload-time = "2025-10-11T19:35:09.395Z" },
    +    { url = "https://files.pythonhosted.org/packages/67/be/7e01fda506f3249d3548d35d1203e009a850734297ccfe4039ce76a927dc/jellyfish-1.2.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:68080af234256ef943f0add6fc79816b0c643d8df291c17a85c1b6e45bdfbb96", size = 362820, upload-time = "2025-10-11T19:35:11.28Z" },
    +    { url = "https://files.pythonhosted.org/packages/f5/95/39302d0df1e1b7c348c1fe6fda27cc6cd4c0bd0b27d79f34de3981a14e55/jellyfish-1.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4c5acb213aa75a61bcfc176566e20f2503069667e760d83d403b59e115fef0dd", size = 360560, upload-time = "2025-10-11T19:35:12.266Z" },
    +    { url = "https://files.pythonhosted.org/packages/ac/9d/b477787bc032e8b5b1ffd798e1c638ecbd54621967dc5577ccd10b5e9444/jellyfish-1.2.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4b28fcefc0c3534277ff0306e6c10672fb050f4784b5f3be7037e80801569fb5", size = 533823, upload-time = "2025-10-11T19:35:13.268Z" },
    +    { url = "https://files.pythonhosted.org/packages/73/7e/c6e389c4fccfc2838b1d3fe21736b5bf9ea1e739287d128a291eb84df158/jellyfish-1.2.1-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:f69aeb08659a6c81d559bbe319075e3417434ae5b3a5e4a758d1c4055a03497a", size = 554439, upload-time = "2025-10-11T19:35:14.595Z" },
    +    { url = "https://files.pythonhosted.org/packages/17/1e/3239b2dfdfb2f1d8795a8d35936c5eadb90475dbbeebacf45e083579d560/jellyfish-1.2.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:63770120cc3386dcc13bcc4df508ab281a6b14c3b2c0e33586439a6c40ee122f", size = 523762, upload-time = "2025-10-11T19:35:15.614Z" },
    +    { url = "https://files.pythonhosted.org/packages/2c/05/62f16bec1d2cd74e6944dfb18a8511bd9df9f2d58e041567f909da22ee26/jellyfish-1.2.1-cp311-cp311-win32.whl", hash = "sha256:ecf62d4aad0baa8832ab60f96e7baedbe6558bd292597503d927e9c5bce745d8", size = 208967, upload-time = "2025-10-11T19:35:16.616Z" },
    +    { url = "https://files.pythonhosted.org/packages/87/a4/69b65d9090d297407bc530f2e5b8707aa1caa9484e7281a04da6821f13be/jellyfish-1.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:bd186c041d9be86c4fa5e2490943ce5d7f05b472f45d7f49426f259f3dd20bc4", size = 213556, upload-time = "2025-10-11T19:35:17.528Z" },
    +    { url = "https://files.pythonhosted.org/packages/9c/52/4112537334f1b21ead968a663f0aeb8a5774f42f9c92ded69bad21db1c5e/jellyfish-1.2.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:32a85b752cb51463face13e2b1797cfa617cd7fb7073f15feaa4020a86a346ce", size = 323225, upload-time = "2025-10-11T19:35:18.555Z" },
    +    { url = "https://files.pythonhosted.org/packages/b2/0d/c54aa2476e5e63673910720b75f3b15e2484687fff9a457a84861f3fa898/jellyfish-1.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:675ab43840488944899ca87f02d4813c1e32107e56afaba7489705a70214e8aa", size = 317839, upload-time = "2025-10-11T19:35:19.648Z" },
    +    { url = "https://files.pythonhosted.org/packages/51/3f/a81347d705150a69e446cabcbe8f223ad990164dffd3e6f8178ed44cf198/jellyfish-1.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c888f624d03e55e501bc438906505c79fb307d8da37a6dda18dd1ac2e6d5ea9c", size = 353337, upload-time = "2025-10-11T19:35:20.651Z" },
    +    { url = "https://files.pythonhosted.org/packages/e7/3a/b655e72b852f6c304a2bc12091485f71e58e8c6374a15c8f21a1f0e1b9cd/jellyfish-1.2.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:d2b56a1fd2c5126c4a3362ec4470291cdd3c7daa22f583da67e75e30dc425ce6", size = 362632, upload-time = "2025-10-11T19:35:21.624Z" },
    +    { url = "https://files.pythonhosted.org/packages/4e/be/f9f9a0b7ba48c994e0573d718e39bde713572cfb11f967d97328420a7aef/jellyfish-1.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1a3ccff843822e7f3ad6f91662488a3630724c8587976bce114f3c7238e8ffa1", size = 360514, upload-time = "2025-10-11T19:35:22.886Z" },
    +    { url = "https://files.pythonhosted.org/packages/f0/b6/960e556e155f65438c1b70d50f745ceb2989de8255a769ccaad26bf94a3f/jellyfish-1.2.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:10da696747e2de0336180fd5ba77ef769a7c80f9743123545f7fc0251efbbcec", size = 533973, upload-time = "2025-10-11T19:35:24.077Z" },
    +    { url = "https://files.pythonhosted.org/packages/24/63/f5b5fb00c0df70387f699535c38190a97f30b79c2e7d4afb97794f838875/jellyfish-1.2.1-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:c3c18f13175a9c90f3abd8805720b0eb3e10eca1d5d4e0cf57722b2a62d62016", size = 553863, upload-time = "2025-10-11T19:35:25.64Z" },
    +    { url = "https://files.pythonhosted.org/packages/86/45/4de6626b6045884ed27995e170bacd09239b19549e25d95492cde10ea052/jellyfish-1.2.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:0368596e176bf548b3be2979ff33e274fb6d5e13b2cebe85137b8b698b002a85", size = 523629, upload-time = "2025-10-11T19:35:26.732Z" },
    +    { url = "https://files.pythonhosted.org/packages/73/d6/8593e08568438b207f91b2fba2f6c879abc85dc450c0ad599a4e81dd9f07/jellyfish-1.2.1-cp312-cp312-win32.whl", hash = "sha256:451ddf4094e108e33d3b86d7817a7e20a2c5e6812d08c34ee22f6a595f38dcca", size = 209179, upload-time = "2025-10-11T19:35:27.72Z" },
    +    { url = "https://files.pythonhosted.org/packages/0f/ff/ae991a96e8a370f41bbd91dbabdc94b404a164b0ab268388f43c2ab10d45/jellyfish-1.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:15318c13070fe6d9caeb7e10f9cdf89ff47c9d20f05a9a2c0d3b5cb8062a7033", size = 213630, upload-time = "2025-10-11T19:35:28.978Z" },
    +    { url = "https://files.pythonhosted.org/packages/5c/e6/75feeda1c3634525296aa56265db151f896005b139e177f8b1a285546a1f/jellyfish-1.2.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:4b3e3223aaad74e18aacc74775e01815e68af810258ceea6fa6a81b19f384312", size = 322958, upload-time = "2025-10-11T19:35:29.906Z" },
    +    { url = "https://files.pythonhosted.org/packages/0e/66/4b92bb55b545ebefbf085e45cbcda576d2a2a3dc48fd61dae469c27e73a6/jellyfish-1.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e967e67058b78189d2b20a9586c7720a05ec4a580d6a98c796cd5cd2b7b11303", size = 317859, upload-time = "2025-10-11T19:35:31.312Z" },
    +    { url = "https://files.pythonhosted.org/packages/fe/8e/9d0055f921c884605bf22a96e376b016993928126e8a4c7fd8698260fb4e/jellyfish-1.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32581c50b34a09889b2d96796170e53da313a1e7fde32be63c82e50e7e791e3c", size = 353222, upload-time = "2025-10-11T19:35:32.352Z" },
    +    { url = "https://files.pythonhosted.org/packages/4f/d2/deca58a62e57f7e2b2172ab39f522831279ee08ec0943fc0d0e33cd6e6f9/jellyfish-1.2.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:07b022412ebece96759006cb015d46b8218d7f896d8b327c6bbee784ddf38ed9", size = 362392, upload-time = "2025-10-11T19:35:33.305Z" },
    +    { url = "https://files.pythonhosted.org/packages/12/40/9a7f62d367f5a862950ce3598188fe0e22e11d1f5d6eaad6eda5adc354b0/jellyfish-1.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80a49eb817eaa6591f43a31e5c93d79904de62537f029907ef88c050d781a638", size = 360358, upload-time = "2025-10-11T19:35:34.585Z" },
    +    { url = "https://files.pythonhosted.org/packages/a5/e5/6b44a1058df3dfa3dd1174c9f86685c78f780d0b68851a057075aea14587/jellyfish-1.2.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:e1b990fb15985571616f7f40a12d6fa062897b19fb5359b6dec3cd811d802c24", size = 533945, upload-time = "2025-10-11T19:35:35.764Z" },
    +    { url = "https://files.pythonhosted.org/packages/50/4c/2397f43ad2692a1052299607838b41a4c2dd5707fde4ce459d686e763eb1/jellyfish-1.2.1-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:dd895cf63fac0a9f11b524fff810d9a6081dcf3c518b34172ac8684eb504dd43", size = 553707, upload-time = "2025-10-11T19:35:36.926Z" },
    +    { url = "https://files.pythonhosted.org/packages/de/aa/dc7cf053c8c40035791de1dc2f45b1f57772a14b0dc53318720e87073831/jellyfish-1.2.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:6d2bac5982d7a08759ea487bfa00149e6aa8a3be7cd43c4ed1be1e3505425c69", size = 523323, upload-time = "2025-10-11T19:35:37.981Z" },
    +    { url = "https://files.pythonhosted.org/packages/2b/1a/610c7f1f7777646322f489b5ed1e4631370c9fa4fb40a8246af71b496b6d/jellyfish-1.2.1-cp313-cp313-win32.whl", hash = "sha256:509355ebedec69a8bf0cc113a6bf9c01820d12fe2eea44f47dfa809faf2d5463", size = 209143, upload-time = "2025-10-11T19:35:39.276Z" },
    +    { url = "https://files.pythonhosted.org/packages/80/9a/6102b23b03a6df779fee76c979c0eb819b300c83b468900df78bb574b944/jellyfish-1.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:9c747ae5c0fb4bd519f6abbfe4bd704b2f1c63fd4dd3dbb8d8864478974e1571", size = 213466, upload-time = "2025-10-11T19:35:40.24Z" },
    +    { url = "https://files.pythonhosted.org/packages/89/c3/92190ff494881008ff127d67aba80245a5071ec7c3ff1181ceddc6c9d636/jellyfish-1.2.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:212aaf177236192a735bbbf5938717aa8518d14a25b08b015e47e783e70be060", size = 322379, upload-time = "2025-10-11T19:35:41.21Z" },
    +    { url = "https://files.pythonhosted.org/packages/d4/db/993c81f3e95e06e2a5cb71aaf9af063d8798a34c9715c8059707ddc12b86/jellyfish-1.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:b8986d9768daddd5e87abf513ae168ea0afe690a444d4c82d5b1b14b0d045820", size = 317270, upload-time = "2025-10-11T19:35:43.367Z" },
    +    { url = "https://files.pythonhosted.org/packages/fc/6a/0f521b098e136c43c7ae1e77db4a792f9e65167fe818820502996488b926/jellyfish-1.2.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5fa0ba0946f3c274f6a87aaa3c631dc70a363bd46cceea828ce777e8db653b6f", size = 352931, upload-time = "2025-10-11T19:35:44.402Z" },
    +    { url = "https://files.pythonhosted.org/packages/a0/c4/5d2242a650f890384b435610ef2962b1ac6091c070912a81a97020d2502a/jellyfish-1.2.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6e76b23431a667cd485fb562428d1ad29bae9fdd0fcdfb5a51cc8087bae0e88c", size = 362473, upload-time = "2025-10-11T19:35:45.427Z" },
    +    { url = "https://files.pythonhosted.org/packages/d5/fe/831fc45a4d3e497bccc4735809551320968360d14b89eb3d7cb892549316/jellyfish-1.2.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a058f4c6a591d5e5a47569f5648a26303ba19c76a960fef7e0beba2aa959e52e", size = 359772, upload-time = "2025-10-11T19:35:46.65Z" },
    +    { url = "https://files.pythonhosted.org/packages/b4/0f/d132265e299947e4462c1485f829a08a513c97c41bdfe758754e4a5c1dfe/jellyfish-1.2.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:6a49ce2a580edd3b16b69421137deef464e2f8907f9ef906d49950b1a52908c1", size = 533628, upload-time = "2025-10-11T19:35:47.691Z" },
    +    { url = "https://files.pythonhosted.org/packages/52/2a/d51dbf0aceb9b141dd8318ce6a41ab08a5deaae56be16a8bf3d8685ac817/jellyfish-1.2.1-cp314-cp314-musllinux_1_1_i686.whl", hash = "sha256:c85aa2bc76a36d92a3197f406f86636664d5b323727dfec4fa2842a8a24a06ae", size = 553614, upload-time = "2025-10-11T19:35:52.928Z" },
    +    { url = "https://files.pythonhosted.org/packages/f9/e1/fcc7c5919d871537942425f707b764af65b76c7b88377aa71083c5280e37/jellyfish-1.2.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:29cfa8bfb72aacf2d611a3313b358ed4d4140fa3d3efcffea750c8e7f8acb1aa", size = 523057, upload-time = "2025-10-11T19:35:54.423Z" },
    +    { url = "https://files.pythonhosted.org/packages/95/65/ee5289540b2015643493cc29b50350dbe63ca1977a902de5295a4df8c25a/jellyfish-1.2.1-cp314-cp314-win32.whl", hash = "sha256:f121218dc33fb318c34ddd889dc7362606ce1316af2bb63b73cc1df81523ca34", size = 209340, upload-time = "2025-10-11T19:35:55.69Z" },
    +    { url = "https://files.pythonhosted.org/packages/bc/e2/fa5de38380b0f5bd531b27a78acb0dc6118dab0b21f56d36008b829aa7de/jellyfish-1.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:9a73b5c6425a70ebd440579a677eb4f03b327b2f59090db34e6c937aeea5aabd", size = 213399, upload-time = "2025-10-11T19:35:56.776Z" },
    +    { url = "https://files.pythonhosted.org/packages/96/72/e4897449abd844d501412873d1d15bd846bcc919648c0b1043e225268b21/jellyfish-1.2.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:748dc45a0394fbe9120b8b3b9a39fab0967c7e2d6ecdd5304af018e774f80f96", size = 326967, upload-time = "2025-10-11T19:36:18.851Z" },
    +    { url = "https://files.pythonhosted.org/packages/60/3f/c7a550abd212ae40c2a555055a3f16ba39376e486ba0189e150fb25cf6b1/jellyfish-1.2.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:13f1ac9caba22af10bfe42f674822643c0266009f882e0fe652079706dc5d13a", size = 321759, upload-time = "2025-10-11T19:36:19.882Z" },
    +    { url = "https://files.pythonhosted.org/packages/19/58/a268365ba659f04d4db0c94325042aa9aee69c3a9a5823a5b2a2db308a5c/jellyfish-1.2.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1ffeeb6c78c45fbb6d2a22b0173fb8a6af849001d6c26fab49c525136dbd9734", size = 354847, upload-time = "2025-10-11T19:36:20.976Z" },
    +    { url = "https://files.pythonhosted.org/packages/5d/63/371351a5e0e19d642e33c1c8b4c3ef47538f36bbd8d76a06ee34000b38a2/jellyfish-1.2.1-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1354b558a0a16597b6032dd0af64bebd24994f7e7484cf14993320eb764b06cb", size = 364210, upload-time = "2025-10-11T19:36:22.071Z" },
    +    { url = "https://files.pythonhosted.org/packages/a8/ed/f43d79b9b6d846189b1235f8303d1246ca9cba79a61a26cac790b57c1789/jellyfish-1.2.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e5977810972c6f0b2e61252c4758fd5aee21abf663ff309881195a99d37daa94", size = 361876, upload-time = "2025-10-11T19:36:23.235Z" },
    +    { url = "https://files.pythonhosted.org/packages/74/5c/82455195b77cd1996c3618bd5aa8f25a9fc254d401a3c1425fb60cf97742/jellyfish-1.2.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:536c80d8d4ec7f39cbb10b85d926ff96cef3cde4a83ca0991c07cd9835d5dc13", size = 535488, upload-time = "2025-10-11T19:36:24.252Z" },
    +    { url = "https://files.pythonhosted.org/packages/74/3d/295468c5df5a8d03f522b0c21fc3e694d6be376602a6d755bf7815947522/jellyfish-1.2.1-pp311-pypy311_pp73-musllinux_1_1_i686.whl", hash = "sha256:21baa92d4a5112167721156f6d061c2ae105f2995b3a5e19cec6662928f0c439", size = 555818, upload-time = "2025-10-11T19:36:25.667Z" },
    +    { url = "https://files.pythonhosted.org/packages/41/6d/4029265138a5a0b18e4df381560e467339bec477f4efaa7614736dbc433e/jellyfish-1.2.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:68ea3ddd4dae1152a7f7155ef02a7bfad919611158d71b301f9aa167685819af", size = 525259, upload-time = "2025-10-11T19:36:26.726Z" },
    +]
    +
    +[[package]]
    +name = "json-repair"
    +version = "0.63.4"
    +source = { registry = "https://pypi.org/simple" }
    +sdist = { url = "https://files.pythonhosted.org/packages/62/72/4ec122befe7c2091ba7370d985cf0a3d4dbeccc2f832898ddb7921b4ab94/json_repair-0.63.4.tar.gz", hash = "sha256:77aa642193d62b02b889e8ce0df33898d3ea87282f0b9d8653f8ce8772c642b4", size = 52906, upload-time = "2026-08-25T07:39:02.725Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/a1/ea/1a7b47196b0d03f59a80f5218b2a34160587c84ca8b0bcb56cb4ee93834e/json_repair-0.63.4-py3-none-any.whl", hash = "sha256:0f374f3eee21454aef0a5d72c06b8689b660a1788f80ab392639e3f7d5c5d458", size = 51295, upload-time = "2026-08-25T07:39:01.259Z" },
    +]
    +
    +[[package]]
    +name = "jsonpath-ng"
    +version = "1.7.0"
    +source = { registry = "https://pypi.org/simple" }
    +dependencies = [
    +    { name = "ply" },
    +]
    +sdist = { url = "https://files.pythonhosted.org/packages/6d/86/08646239a313f895186ff0a4573452038eed8c86f54380b3ebac34d32fb2/jsonpath-ng-1.7.0.tar.gz", hash = "sha256:f6f5f7fd4e5ff79c785f1573b394043b39849fb2bb47bcead935d12b00beab3c", size = 37838, upload-time = "2024-10-11T15:41:42.404Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/35/5a/73ecb3d82f8615f32ccdadeb9356726d6cae3a4bbc840b437ceb95708063/jsonpath_ng-1.7.0-py3-none-any.whl", hash = "sha256:f3d7f9e848cba1b6da28c55b1c26ff915dc9e0b1ba7e752a53d6da8d5cbd00b6", size = 30105, upload-time = "2024-11-20T17:58:30.418Z" },
    +]
    +
     [[package]]
     name = "packaging"
     version = "26.3"
    @@ -338,6 +558,115 @@ wheels = [
         { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" },
     ]
     
    +[[package]]
    +name = "pillow"
    +version = "12.3.0"
    +source = { registry = "https://pypi.org/simple" }
    +sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/fb/c8/0a78b0e02d7ac54bc03e5321c9220da52f0c2ea83b21f7c40e7f3169c502/pillow-12.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:00808c5e14ef63ac5161091d242999076604ff74b883423a11e5d7bbb38bf756", size = 5392415, upload-time = "2026-07-01T11:53:47.162Z" },
    +    { url = "https://files.pythonhosted.org/packages/b2/5b/a02d30018abd97ced9f5a6c63d28597694a00d066516b9c1c6de45859fc9/pillow-12.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:37d6d0a00072fd2948eb22bce7e1475f34569d90c87c59f7a2ec59541b77f7a6", size = 4785266, upload-time = "2026-07-01T11:53:49.079Z" },
    +    { url = "https://files.pythonhosted.org/packages/c8/98/766667a4be768150a202836acd9fad19c06824ca86c4286d3cf6b274964e/pillow-12.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bcb46e2f9feff8d06323983bd83ed00c201fdcab3d74973e7072a889b3979fcd", size = 6263814, upload-time = "2026-07-01T11:53:51.32Z" },
    +    { url = "https://files.pythonhosted.org/packages/3b/2d/ede717bc1144f63886c21fd349bb95860b0d1a21149ff16f2bb362b612b6/pillow-12.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:23d27a3e0307ec2244cc51e7287b919aa68d097504ebe19df4e76a98a3eea5bd", size = 6934408, upload-time = "2026-07-01T11:53:53.487Z" },
    +    { url = "https://files.pythonhosted.org/packages/a3/48/9c58b685e69d49c31af6c8eb9012055fab7e665785165c84796e2c73ce72/pillow-12.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:4f883547d4b7f0495ebe7056b0cc2aea76094e7a4abc8e933540f3271df27d9c", size = 6337160, upload-time = "2026-07-01T11:53:55.457Z" },
    +    { url = "https://files.pythonhosted.org/packages/ff/fa/dc2a5c0ba6df93f67c31d34b808b7ce440b40cdbf96f0b81cde1d1e6fa93/pillow-12.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:236ff70b9312fb68943c703aa842ca6a758abfa45ac187a5e7c1452e96ef72b5", size = 7045172, upload-time = "2026-07-01T11:53:57.736Z" },
    +    { url = "https://files.pythonhosted.org/packages/86/a5/444817a4d4c4c2417df00513086ca196f388d8f9ef40c2e4ccd1ad1af54b/pillow-12.3.0-cp311-cp311-win32.whl", hash = "sha256:10e41f0fbf1eec8cfd234b8fe17a4caac7c9d0db4c204d3c173a8f9f6ef3232b", size = 6472232, upload-time = "2026-07-01T11:53:59.767Z" },
    +    { url = "https://files.pythonhosted.org/packages/63/c6/4bad1b18d132a50b27e1365e1ab163616f7a5bb56d330f66f9d1d9d4f9d4/pillow-12.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:8e95e1385e4998ae9694eeaa4730ba5457ff61185b3a55e2e7bea0880aef452a", size = 7233653, upload-time = "2026-07-01T11:54:02.066Z" },
    +    { url = "https://files.pythonhosted.org/packages/fd/16/00f91ab7760dc842f5aad55217e80fc4a7067a0604535249bc8a2d6d9870/pillow-12.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:ebaea975e03d3141d9d3a507df75c9b3ec90fa9d2ffd07567b3a978d9d790b26", size = 2568195, upload-time = "2026-07-01T11:54:04.622Z" },
    +    { url = "https://files.pythonhosted.org/packages/37/bf/fb3ebff8ddcb76aac5a01389251bbbb9519922a9b520d8247c1ca864a25d/pillow-12.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ba09209fbe443b4acccebe845d8a138b89a8f4fbaeedd44953490b5315d5e965", size = 5345969, upload-time = "2026-07-01T11:54:06.397Z" },
    +    { url = "https://files.pythonhosted.org/packages/d8/66/9a386a92561f402389a4fc70c18838bf6d35eb5eb5c6850b4b2dc64f5048/pillow-12.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffd0c5368496f41b0944be820fcb7a838aa6e623d250b01acf2643939c3f99d7", size = 4780323, upload-time = "2026-07-01T11:54:09.351Z" },
    +    { url = "https://files.pythonhosted.org/packages/25/27/ac8f99618ffd3dde21db0f4d4b1d2ab00c0880595bfd17df103f7f39fd0c/pillow-12.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9c7f76c0673154f044e9d78c8655fb4213f6ca31a836df48b40fe5d187717b9", size = 6266838, upload-time = "2026-07-01T11:54:11.71Z" },
    +    { url = "https://files.pythonhosted.org/packages/84/21/a35af28dcc61f37ed850a2d64c65c701321dfbf25085e469d5559360cbbf/pillow-12.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:78cb2c6865a35ab8ff8b75fd122f6033b92a62c82801110e48ddd6c936a45d91", size = 6940830, upload-time = "2026-07-01T11:54:13.732Z" },
    +    { url = "https://files.pythonhosted.org/packages/eb/51/8b08617af3ad95e33ce6d7dd2c99ed6c8298f7fb131636303956be022e25/pillow-12.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e491916b378fba47242221bb9ead245211b70d504f495d105d17b14a24b4907c", size = 6344383, upload-time = "2026-07-01T11:54:15.756Z" },
    +    { url = "https://files.pythonhosted.org/packages/1d/72/cf78ac9780bb93c28328f408973845a309d4d145041665f734572ced1b52/pillow-12.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0dd2064cbc55aaec028ef5fbb60fa47bb6c3e7918e07ff17935284b227a9d2df", size = 7052934, upload-time = "2026-07-01T11:54:17.721Z" },
    +    { url = "https://files.pythonhosted.org/packages/20/20/25e0f4dc178a6bc0696793720055519a0de89e7661dae886992decbd2f81/pillow-12.3.0-cp312-cp312-win32.whl", hash = "sha256:dbce0b29841537a2fa4a214c2bbf14de3587c9680caa9b4e217568472490b28f", size = 6472684, upload-time = "2026-07-01T11:54:19.839Z" },
    +    { url = "https://files.pythonhosted.org/packages/45/89/da2f7971a317f83d807fdd4065c0af40208e59e692cc43d315a71a0e96d1/pillow-12.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a2b55dd6b2a4c4b7d87ffa56bdb33fdc5fdb9a462173861a7bc097f17d91cb09", size = 7227137, upload-time = "2026-07-01T11:54:22.025Z" },
    +    { url = "https://files.pythonhosted.org/packages/de/47/4845a0a6c0dbf1db8456bd9fc791f13c5ced7ced20606d08a0aacfd25b49/pillow-12.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:331b624368d4f1d069149002f25f44bc61c8919ce8ddb3c45bdad8f6e2d89510", size = 2568267, upload-time = "2026-07-01T11:54:24.051Z" },
    +    { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" },
    +    { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" },
    +    { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" },
    +    { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" },
    +    { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" },
    +    { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" },
    +    { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" },
    +    { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" },
    +    { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" },
    +    { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" },
    +    { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" },
    +    { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" },
    +    { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" },
    +    { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" },
    +    { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" },
    +    { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" },
    +    { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" },
    +    { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" },
    +    { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" },
    +    { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" },
    +    { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" },
    +    { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" },
    +    { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" },
    +    { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" },
    +    { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" },
    +    { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" },
    +    { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" },
    +    { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" },
    +    { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" },
    +    { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" },
    +    { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" },
    +    { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" },
    +    { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" },
    +    { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" },
    +    { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" },
    +    { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" },
    +    { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" },
    +    { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" },
    +    { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" },
    +    { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" },
    +    { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" },
    +    { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" },
    +    { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" },
    +    { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" },
    +    { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" },
    +    { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" },
    +    { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" },
    +    { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" },
    +    { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" },
    +    { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" },
    +    { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" },
    +    { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" },
    +    { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" },
    +    { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" },
    +    { url = "https://files.pythonhosted.org/packages/75/18/2e8b40223153ccbc60df07f9e8928dc0c76202aa4e55ae9f53962b6510d6/pillow-12.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b3c777e849237620b022f7f297dd67705f9f5cf1685f09f02e46f93e92725468", size = 5302510, upload-time = "2026-07-01T11:56:25.736Z" },
    +    { url = "https://files.pythonhosted.org/packages/46/3e/51fabf59d5ab801ceab709453d3ab6b180083496579549de4c45ced6528a/pillow-12.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:b343699e8308bdc51978310e1c959c584e7869cc8c40780058c87da7781a1e94", size = 4736058, upload-time = "2026-07-01T11:56:28.041Z" },
    +    { url = "https://files.pythonhosted.org/packages/bf/20/22fe9384b7949e25fb1293bcfc84fb82590ff4ea6b37c95b24d26d793d86/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fbd139c8447d25dd750ab79ee274cc5e1fe80fc56340ab10b18a195e1b6eca3e", size = 5237776, upload-time = "2026-07-01T11:56:30.263Z" },
    +    { url = "https://files.pythonhosted.org/packages/08/14/f6ba68107680ffa74b39985f3f30884e41318fbc4250caa423c79b4788bb/pillow-12.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7e480451b9fa137494bccd3a7d69adbe8ac65a87d97be61e11f1b1050a5bac3", size = 5860358, upload-time = "2026-07-01T11:56:32.68Z" },
    +    { url = "https://files.pythonhosted.org/packages/36/54/0169bc772ec491108b62f644f8ecf1fe5d8ae5ebafde2ee2142210166903/pillow-12.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:04f01d28a6aaff387bf842a13be313df23ba0597a44f1a976c9feb3c6ff4711a", size = 7231786, upload-time = "2026-07-01T11:56:35.046Z" },
    +]
    +
    +[[package]]
    +name = "pint"
    +version = "0.25"
    +source = { registry = "https://pypi.org/simple" }
    +dependencies = [
    +    { name = "flexcache" },
    +    { name = "flexparser" },
    +    { name = "platformdirs" },
    +    { name = "typing-extensions" },
    +]
    +sdist = { url = "https://files.pythonhosted.org/packages/5a/cb/e6ffaf3d019e8501b1264dac529bf829ac2f1fe1d488cfcf67f1fccadacf/pint-0.25.tar.gz", hash = "sha256:22911a30d682ee0540d656571c19a7b1806ce00b2be88a16f67218108b7b8cc2", size = 253010, upload-time = "2025-08-15T19:49:12.72Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/76/cc/c528311d798e22ec884b816e8aa2989e0f1f28cdc8e5969e2be5f10bce85/pint-0.25-py3-none-any.whl", hash = "sha256:cc20ae3dff010b9bbea41fb80c2de008f683cc83512cea73633d55aead80aa1e", size = 305462, upload-time = "2025-08-15T19:49:11.083Z" },
    +]
    +
    +[[package]]
    +name = "platformdirs"
    +version = "4.11.7"
    +source = { registry = "https://pypi.org/simple" }
    +sdist = { url = "https://files.pythonhosted.org/packages/69/b7/802a56eca9f2fac455b8bab5375a2647b0f0e14a2cd63ef077de3c4a7658/platformdirs-4.11.7.tar.gz", hash = "sha256:4f41487eeeeeb07f3a6625e61d9bc0ae6809f92d3386dbd74392fbb76108104d", size = 35127, upload-time = "2026-09-01T13:35:10.502Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/27/6e/80993e10a0482f630cef528635789233224f36b1ffd11592aa15d13ff9ce/platformdirs-4.11.7-py3-none-any.whl", hash = "sha256:8a02cb259042c79d1cd0450facc2fe6dc9d303ae7901afbe33bf8ea0b188cef6", size = 23938, upload-time = "2026-09-01T13:35:09.02Z" },
    +]
    +
     [[package]]
     name = "playwright"
     version = "1.58.0"
    @@ -366,6 +695,140 @@ wheels = [
         { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" },
     ]
     
    +[[package]]
    +name = "ply"
    +version = "3.11"
    +source = { registry = "https://pypi.org/simple" }
    +sdist = { url = "https://files.pythonhosted.org/packages/e5/69/882ee5c9d017149285cab114ebeab373308ef0f874fcdac9beb90e0ac4da/ply-3.11.tar.gz", hash = "sha256:00c7c1aaa88358b9c765b6d3000c6eec0ba42abca5351b095321aef446081da3", size = 159130, upload-time = "2018-02-15T19:01:31.097Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/a3/58/35da89ee790598a0700ea49b2a66594140f44dec458c07e8e3d4979137fc/ply-3.11-py2.py3-none-any.whl", hash = "sha256:096f9b8350b65ebd2fd1346b12452efe5b9607f7482813ffca50c22722a807ce", size = 49567, upload-time = "2018-02-15T19:01:27.172Z" },
    +]
    +
    +[[package]]
    +name = "price-parser"
    +version = "0.5.0"
    +source = { registry = "https://pypi.org/simple" }
    +dependencies = [
    +    { name = "attrs" },
    +]
    +sdist = { url = "https://files.pythonhosted.org/packages/71/ad/1dbbe2f5baf98f272f2b557124d95e4c68481ba4e1593b90cb39c5836542/price_parser-0.5.0.tar.gz", hash = "sha256:5053af8726de8d412d066f69f721ed5e3e0d61a29f27f6fc3a348d60917ece30", size = 34312, upload-time = "2025-10-06T09:52:40.664Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/7b/73/09ebcc1650df02390a45ee252af3fbcdd7cb1fd14bd41941b42c273b046a/price_parser-0.5.0-py3-none-any.whl", hash = "sha256:7851375f92e1237ed909110d5a7394e880acb4971b1459dd773ce903ba83b6f0", size = 14771, upload-time = "2025-10-06T09:52:39.642Z" },
    +]
    +
    +[[package]]
    +name = "probableparsing"
    +version = "0.0.1"
    +source = { registry = "https://pypi.org/simple" }
    +sdist = { url = "https://files.pythonhosted.org/packages/8b/26/5a86ac418ee4cecb127a4a6f98d6b202216b5ac4f6b519328ae81d07ba9e/probableparsing-0.0.1.tar.gz", hash = "sha256:8114bbf889e1f9456fe35946454c96e42a6ee2673a90d4f1f9c46a406f543767", size = 1722, upload-time = "2016-03-28T23:39:54.972Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/e1/6b/91255cbf739a835df41af530a36798397d70342d152b773b5b0fe3001843/probableparsing-0.0.1-py2.py3-none-any.whl", hash = "sha256:509df25fdda4fd7c0b2a100f58cc971bd23daf26f3b3320aebf2616d2e10c69e", size = 3056, upload-time = "2016-12-19T15:04:32.102Z" },
    +]
    +
    +[[package]]
    +name = "pydantic"
    +version = "2.12.0"
    +source = { registry = "https://pypi.org/simple" }
    +dependencies = [
    +    { name = "annotated-types" },
    +    { name = "pydantic-core" },
    +    { name = "typing-extensions" },
    +    { name = "typing-inspection" },
    +]
    +sdist = { url = "https://files.pythonhosted.org/packages/c3/da/b8a7ee04378a53f6fefefc0c5e05570a3ebfdfa0523a878bcd3b475683ee/pydantic-2.12.0.tar.gz", hash = "sha256:c1a077e6270dbfb37bfd8b498b3981e2bb18f68103720e51fa6c306a5a9af563", size = 814760, upload-time = "2025-10-07T15:58:03.467Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/f4/9d/d5c855424e2e5b6b626fbc6ec514d8e655a600377ce283008b115abb7445/pydantic-2.12.0-py3-none-any.whl", hash = "sha256:f6a1da352d42790537e95e83a8bdfb91c7efbae63ffd0b86fa823899e807116f", size = 459730, upload-time = "2025-10-07T15:58:01.576Z" },
    +]
    +
    +[[package]]
    +name = "pydantic-core"
    +version = "2.41.1"
    +source = { registry = "https://pypi.org/simple" }
    +dependencies = [
    +    { name = "typing-extensions" },
    +]
    +sdist = { url = "https://files.pythonhosted.org/packages/7d/14/12b4a0d2b0b10d8e1d9a24ad94e7bbb43335eaf29c0c4e57860e8a30734a/pydantic_core-2.41.1.tar.gz", hash = "sha256:1ad375859a6d8c356b7704ec0f547a58e82ee80bb41baa811ad710e124bc8f2f", size = 454870, upload-time = "2025-10-07T10:50:45.974Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/f6/a9/ec440f02e57beabdfd804725ef1e38ac1ba00c49854d298447562e119513/pydantic_core-2.41.1-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:4f276a6134fe1fc1daa692642a3eaa2b7b858599c49a7610816388f5e37566a1", size = 2111456, upload-time = "2025-10-06T21:10:09.824Z" },
    +    { url = "https://files.pythonhosted.org/packages/f0/f9/6bc15bacfd8dcfc073a1820a564516d9c12a435a9a332d4cbbfd48828ddd/pydantic_core-2.41.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:07588570a805296ece009c59d9a679dc08fab72fb337365afb4f3a14cfbfc176", size = 1915012, upload-time = "2025-10-06T21:10:11.599Z" },
    +    { url = "https://files.pythonhosted.org/packages/38/8a/d9edcdcdfe80bade17bed424284427c08bea892aaec11438fa52eaeaf79c/pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28527e4b53400cd60ffbd9812ccb2b5135d042129716d71afd7e45bf42b855c0", size = 1973762, upload-time = "2025-10-06T21:10:13.154Z" },
    +    { url = "https://files.pythonhosted.org/packages/d5/b3/ff225c6d49fba4279de04677c1c876fc3dc6562fd0c53e9bfd66f58c51a8/pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:46a1c935c9228bad738c8a41de06478770927baedf581d172494ab36a6b96575", size = 2065386, upload-time = "2025-10-06T21:10:14.436Z" },
    +    { url = "https://files.pythonhosted.org/packages/47/ba/183e8c0be4321314af3fd1ae6bfc7eafdd7a49bdea5da81c56044a207316/pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:447ddf56e2b7d28d200d3e9eafa936fe40485744b5a824b67039937580b3cb20", size = 2252317, upload-time = "2025-10-06T21:10:15.719Z" },
    +    { url = "https://files.pythonhosted.org/packages/57/c5/aab61e94fd02f45c65f1f8c9ec38bb3b33fbf001a1837c74870e97462572/pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63892ead40c1160ac860b5debcc95c95c5a0035e543a8b5a4eac70dd22e995f4", size = 2373405, upload-time = "2025-10-06T21:10:17.017Z" },
    +    { url = "https://files.pythonhosted.org/packages/e5/4f/3aaa3bd1ea420a15acc42d7d3ccb3b0bbc5444ae2f9dbc1959f8173e16b8/pydantic_core-2.41.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4a9543ca355e6df8fbe9c83e9faab707701e9103ae857ecb40f1c0cf8b0e94d", size = 2073794, upload-time = "2025-10-06T21:10:18.383Z" },
    +    { url = "https://files.pythonhosted.org/packages/58/bd/e3975cdebe03ec080ef881648de316c73f2a6be95c14fc4efb2f7bdd0d41/pydantic_core-2.41.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f2611bdb694116c31e551ed82e20e39a90bea9b7ad9e54aaf2d045ad621aa7a1", size = 2194430, upload-time = "2025-10-06T21:10:19.638Z" },
    +    { url = "https://files.pythonhosted.org/packages/2b/b8/6b7e7217f147d3b3105b57fb1caec3c4f667581affdfaab6d1d277e1f749/pydantic_core-2.41.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:fecc130893a9b5f7bfe230be1bb8c61fe66a19db8ab704f808cb25a82aad0bc9", size = 2154611, upload-time = "2025-10-06T21:10:21.28Z" },
    +    { url = "https://files.pythonhosted.org/packages/fe/7b/239c2fe76bd8b7eef9ae2140d737368a3c6fea4fd27f8f6b4cde6baa3ce9/pydantic_core-2.41.1-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:1e2df5f8344c99b6ea5219f00fdc8950b8e6f2c422fbc1cc122ec8641fac85a1", size = 2329809, upload-time = "2025-10-06T21:10:22.678Z" },
    +    { url = "https://files.pythonhosted.org/packages/bd/2e/77a821a67ff0786f2f14856d6bd1348992f695ee90136a145d7a445c1ff6/pydantic_core-2.41.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:35291331e9d8ed94c257bab6be1cb3a380b5eee570a2784bffc055e18040a2ea", size = 2327907, upload-time = "2025-10-06T21:10:24.447Z" },
    +    { url = "https://files.pythonhosted.org/packages/fd/9a/b54512bb9df7f64c586b369328c30481229b70ca6a5fcbb90b715e15facf/pydantic_core-2.41.1-cp311-cp311-win32.whl", hash = "sha256:2876a095292668d753f1a868c4a57c4ac9f6acbd8edda8debe4218d5848cf42f", size = 1989964, upload-time = "2025-10-06T21:10:25.676Z" },
    +    { url = "https://files.pythonhosted.org/packages/9d/72/63c9a4f1a5c950e65dd522d7dd67f167681f9d4f6ece3b80085a0329f08f/pydantic_core-2.41.1-cp311-cp311-win_amd64.whl", hash = "sha256:b92d6c628e9a338846a28dfe3fcdc1a3279388624597898b105e078cdfc59298", size = 2025158, upload-time = "2025-10-06T21:10:27.522Z" },
    +    { url = "https://files.pythonhosted.org/packages/d8/16/4e2706184209f61b50c231529257c12eb6bd9eb36e99ea1272e4815d2200/pydantic_core-2.41.1-cp311-cp311-win_arm64.whl", hash = "sha256:7d82ae99409eb69d507a89835488fb657faa03ff9968a9379567b0d2e2e56bc5", size = 1972297, upload-time = "2025-10-06T21:10:28.814Z" },
    +    { url = "https://files.pythonhosted.org/packages/ee/bc/5f520319ee1c9e25010412fac4154a72e0a40d0a19eb00281b1f200c0947/pydantic_core-2.41.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:db2f82c0ccbce8f021ad304ce35cbe02aa2f95f215cac388eed542b03b4d5eb4", size = 2099300, upload-time = "2025-10-06T21:10:30.463Z" },
    +    { url = "https://files.pythonhosted.org/packages/31/14/010cd64c5c3814fb6064786837ec12604be0dd46df3327cf8474e38abbbd/pydantic_core-2.41.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47694a31c710ced9205d5f1e7e8af3ca57cbb8a503d98cb9e33e27c97a501601", size = 1910179, upload-time = "2025-10-06T21:10:31.782Z" },
    +    { url = "https://files.pythonhosted.org/packages/8e/2e/23fc2a8a93efad52df302fdade0a60f471ecc0c7aac889801ac24b4c07d6/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e9decce94daf47baf9e9d392f5f2557e783085f7c5e522011545d9d6858e00", size = 1957225, upload-time = "2025-10-06T21:10:33.11Z" },
    +    { url = "https://files.pythonhosted.org/packages/b9/b6/6db08b2725b2432b9390844852e11d320281e5cea8a859c52c68001975fa/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ab0adafdf2b89c8b84f847780a119437a0931eca469f7b44d356f2b426dd9741", size = 2053315, upload-time = "2025-10-06T21:10:34.87Z" },
    +    { url = "https://files.pythonhosted.org/packages/61/d9/4de44600f2d4514b44f3f3aeeda2e14931214b6b5bf52479339e801ce748/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5da98cc81873f39fd56882e1569c4677940fbc12bce6213fad1ead784192d7c8", size = 2224298, upload-time = "2025-10-06T21:10:36.233Z" },
    +    { url = "https://files.pythonhosted.org/packages/7a/ae/dbe51187a7f35fc21b283c5250571a94e36373eb557c1cba9f29a9806dcf/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:209910e88afb01fd0fd403947b809ba8dba0e08a095e1f703294fda0a8fdca51", size = 2351797, upload-time = "2025-10-06T21:10:37.601Z" },
    +    { url = "https://files.pythonhosted.org/packages/b5/a7/975585147457c2e9fb951c7c8dab56deeb6aa313f3aa72c2fc0df3f74a49/pydantic_core-2.41.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:365109d1165d78d98e33c5bfd815a9b5d7d070f578caefaabcc5771825b4ecb5", size = 2074921, upload-time = "2025-10-06T21:10:38.927Z" },
    +    { url = "https://files.pythonhosted.org/packages/62/37/ea94d1d0c01dec1b7d236c7cec9103baab0021f42500975de3d42522104b/pydantic_core-2.41.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:706abf21e60a2857acdb09502bc853ee5bce732955e7b723b10311114f033115", size = 2187767, upload-time = "2025-10-06T21:10:40.651Z" },
    +    { url = "https://files.pythonhosted.org/packages/d3/fe/694cf9fdd3a777a618c3afd210dba7b414cb8a72b1bd29b199c2e5765fee/pydantic_core-2.41.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:bf0bd5417acf7f6a7ec3b53f2109f587be176cb35f9cf016da87e6017437a72d", size = 2136062, upload-time = "2025-10-06T21:10:42.09Z" },
    +    { url = "https://files.pythonhosted.org/packages/0f/ae/174aeabd89916fbd2988cc37b81a59e1186e952afd2a7ed92018c22f31ca/pydantic_core-2.41.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:2e71b1c6ceb9c78424ae9f63a07292fb769fb890a4e7efca5554c47f33a60ea5", size = 2317819, upload-time = "2025-10-06T21:10:43.974Z" },
    +    { url = "https://files.pythonhosted.org/packages/65/e8/e9aecafaebf53fc456314f72886068725d6fba66f11b013532dc21259343/pydantic_core-2.41.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:80745b9770b4a38c25015b517451c817799bfb9d6499b0d13d8227ec941cb513", size = 2312267, upload-time = "2025-10-06T21:10:45.34Z" },
    +    { url = "https://files.pythonhosted.org/packages/35/2f/1c2e71d2a052f9bb2f2df5a6a05464a0eb800f9e8d9dd800202fe31219e1/pydantic_core-2.41.1-cp312-cp312-win32.whl", hash = "sha256:83b64d70520e7890453f1aa21d66fda44e7b35f1cfea95adf7b4289a51e2b479", size = 1990927, upload-time = "2025-10-06T21:10:46.738Z" },
    +    { url = "https://files.pythonhosted.org/packages/b1/78/562998301ff2588b9c6dcc5cb21f52fa919d6e1decc75a35055feb973594/pydantic_core-2.41.1-cp312-cp312-win_amd64.whl", hash = "sha256:377defd66ee2003748ee93c52bcef2d14fde48fe28a0b156f88c3dbf9bc49a50", size = 2034703, upload-time = "2025-10-06T21:10:48.524Z" },
    +    { url = "https://files.pythonhosted.org/packages/b2/53/d95699ce5a5cdb44bb470bd818b848b9beadf51459fd4ea06667e8ede862/pydantic_core-2.41.1-cp312-cp312-win_arm64.whl", hash = "sha256:c95caff279d49c1d6cdfe2996e6c2ad712571d3b9caaa209a404426c326c4bde", size = 1972719, upload-time = "2025-10-06T21:10:50.256Z" },
    +    { url = "https://files.pythonhosted.org/packages/27/8a/6d54198536a90a37807d31a156642aae7a8e1263ed9fe6fc6245defe9332/pydantic_core-2.41.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70e790fce5f05204ef4403159857bfcd587779da78627b0babb3654f75361ebf", size = 2105825, upload-time = "2025-10-06T21:10:51.719Z" },
    +    { url = "https://files.pythonhosted.org/packages/4f/2e/4784fd7b22ac9c8439db25bf98ffed6853d01e7e560a346e8af821776ccc/pydantic_core-2.41.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9cebf1ca35f10930612d60bd0f78adfacee824c30a880e3534ba02c207cceceb", size = 1910126, upload-time = "2025-10-06T21:10:53.145Z" },
    +    { url = "https://files.pythonhosted.org/packages/f3/92/31eb0748059ba5bd0aa708fb4bab9fcb211461ddcf9e90702a6542f22d0d/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:170406a37a5bc82c22c3274616bf6f17cc7df9c4a0a0a50449e559cb755db669", size = 1961472, upload-time = "2025-10-06T21:10:55.754Z" },
    +    { url = "https://files.pythonhosted.org/packages/ab/91/946527792275b5c4c7dde4cfa3e81241bf6900e9fee74fb1ba43e0c0f1ab/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12d4257fc9187a0ccd41b8b327d6a4e57281ab75e11dda66a9148ef2e1fb712f", size = 2063230, upload-time = "2025-10-06T21:10:57.179Z" },
    +    { url = "https://files.pythonhosted.org/packages/31/5d/a35c5d7b414e5c0749f1d9f0d159ee2ef4bab313f499692896b918014ee3/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:a75a33b4db105dd1c8d57839e17ee12db8d5ad18209e792fa325dbb4baeb00f4", size = 2229469, upload-time = "2025-10-06T21:10:59.409Z" },
    +    { url = "https://files.pythonhosted.org/packages/21/4d/8713737c689afa57ecfefe38db78259d4484c97aa494979e6a9d19662584/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08a589f850803a74e0fcb16a72081cafb0d72a3cdda500106942b07e76b7bf62", size = 2347986, upload-time = "2025-10-06T21:11:00.847Z" },
    +    { url = "https://files.pythonhosted.org/packages/f6/ec/929f9a3a5ed5cda767081494bacd32f783e707a690ce6eeb5e0730ec4986/pydantic_core-2.41.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a97939d6ea44763c456bd8a617ceada2c9b96bb5b8ab3dfa0d0827df7619014", size = 2072216, upload-time = "2025-10-06T21:11:02.43Z" },
    +    { url = "https://files.pythonhosted.org/packages/26/55/a33f459d4f9cc8786d9db42795dbecc84fa724b290d7d71ddc3d7155d46a/pydantic_core-2.41.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d2ae423c65c556f09569524b80ffd11babff61f33055ef9773d7c9fabc11ed8d", size = 2193047, upload-time = "2025-10-06T21:11:03.787Z" },
    +    { url = "https://files.pythonhosted.org/packages/77/af/d5c6959f8b089f2185760a2779079e3c2c411bfc70ea6111f58367851629/pydantic_core-2.41.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:4dc703015fbf8764d6a8001c327a87f1823b7328d40b47ce6000c65918ad2b4f", size = 2140613, upload-time = "2025-10-06T21:11:05.607Z" },
    +    { url = "https://files.pythonhosted.org/packages/58/e5/2c19bd2a14bffe7fabcf00efbfbd3ac430aaec5271b504a938ff019ac7be/pydantic_core-2.41.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:968e4ffdfd35698a5fe659e5e44c508b53664870a8e61c8f9d24d3d145d30257", size = 2327641, upload-time = "2025-10-06T21:11:07.143Z" },
    +    { url = "https://files.pythonhosted.org/packages/93/ef/e0870ccda798c54e6b100aff3c4d49df5458fd64217e860cb9c3b0a403f4/pydantic_core-2.41.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:fff2b76c8e172d34771cd4d4f0ade08072385310f214f823b5a6ad4006890d32", size = 2318229, upload-time = "2025-10-06T21:11:08.73Z" },
    +    { url = "https://files.pythonhosted.org/packages/b1/4b/c3b991d95f5deb24d0bd52e47bcf716098fa1afe0ce2d4bd3125b38566ba/pydantic_core-2.41.1-cp313-cp313-win32.whl", hash = "sha256:a38a5263185407ceb599f2f035faf4589d57e73c7146d64f10577f6449e8171d", size = 1997911, upload-time = "2025-10-06T21:11:10.329Z" },
    +    { url = "https://files.pythonhosted.org/packages/a7/ce/5c316fd62e01f8d6be1b7ee6b54273214e871772997dc2c95e204997a055/pydantic_core-2.41.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42ae7fd6760782c975897e1fdc810f483b021b32245b0105d40f6e7a3803e4b", size = 2034301, upload-time = "2025-10-06T21:11:12.113Z" },
    +    { url = "https://files.pythonhosted.org/packages/29/41/902640cfd6a6523194123e2c3373c60f19006447f2fb06f76de4e8466c5b/pydantic_core-2.41.1-cp313-cp313-win_arm64.whl", hash = "sha256:ad4111acc63b7384e205c27a2f15e23ac0ee21a9d77ad6f2e9cb516ec90965fb", size = 1977238, upload-time = "2025-10-06T21:11:14.1Z" },
    +    { url = "https://files.pythonhosted.org/packages/04/04/28b040e88c1b89d851278478842f0bdf39c7a05da9e850333c6c8cbe7dfa/pydantic_core-2.41.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:440d0df7415b50084a4ba9d870480c16c5f67c0d1d4d5119e3f70925533a0edc", size = 1875626, upload-time = "2025-10-06T21:11:15.69Z" },
    +    { url = "https://files.pythonhosted.org/packages/d6/58/b41dd3087505220bb58bc81be8c3e8cbc037f5710cd3c838f44f90bdd704/pydantic_core-2.41.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:71eaa38d342099405dae6484216dcf1e8e4b0bebd9b44a4e08c9b43db6a2ab67", size = 2045708, upload-time = "2025-10-06T21:11:17.258Z" },
    +    { url = "https://files.pythonhosted.org/packages/d7/b8/760f23754e40bf6c65b94a69b22c394c24058a0ef7e2aa471d2e39219c1a/pydantic_core-2.41.1-cp313-cp313t-win_amd64.whl", hash = "sha256:555ecf7e50f1161d3f693bc49f23c82cf6cdeafc71fa37a06120772a09a38795", size = 1997171, upload-time = "2025-10-06T21:11:18.822Z" },
    +    { url = "https://files.pythonhosted.org/packages/41/12/cec246429ddfa2778d2d6301eca5362194dc8749ecb19e621f2f65b5090f/pydantic_core-2.41.1-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:05226894a26f6f27e1deb735d7308f74ef5fa3a6de3e0135bb66cdcaee88f64b", size = 2107836, upload-time = "2025-10-06T21:11:20.432Z" },
    +    { url = "https://files.pythonhosted.org/packages/20/39/baba47f8d8b87081302498e610aefc37142ce6a1cc98b2ab6b931a162562/pydantic_core-2.41.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:85ff7911c6c3e2fd8d3779c50925f6406d770ea58ea6dde9c230d35b52b16b4a", size = 1904449, upload-time = "2025-10-06T21:11:22.185Z" },
    +    { url = "https://files.pythonhosted.org/packages/50/32/9a3d87cae2c75a5178334b10358d631bd094b916a00a5993382222dbfd92/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47f1f642a205687d59b52dc1a9a607f45e588f5a2e9eeae05edd80c7a8c47674", size = 1961750, upload-time = "2025-10-06T21:11:24.348Z" },
    +    { url = "https://files.pythonhosted.org/packages/27/42/a96c9d793a04cf2a9773bff98003bb154087b94f5530a2ce6063ecfec583/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:df11c24e138876ace5ec6043e5cae925e34cf38af1a1b3d63589e8f7b5f5cdc4", size = 2063305, upload-time = "2025-10-06T21:11:26.556Z" },
    +    { url = "https://files.pythonhosted.org/packages/3e/8d/028c4b7d157a005b1f52c086e2d4b0067886b213c86220c1153398dbdf8f/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f0bf7f5c8f7bf345c527e8a0d72d6b26eda99c1227b0c34e7e59e181260de31", size = 2228959, upload-time = "2025-10-06T21:11:28.426Z" },
    +    { url = "https://files.pythonhosted.org/packages/08/f7/ee64cda8fcc9ca3f4716e6357144f9ee71166775df582a1b6b738bf6da57/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:82b887a711d341c2c47352375d73b029418f55b20bd7815446d175a70effa706", size = 2345421, upload-time = "2025-10-06T21:11:30.226Z" },
    +    { url = "https://files.pythonhosted.org/packages/13/c0/e8ec05f0f5ee7a3656973ad9cd3bc73204af99f6512c1a4562f6fb4b3f7d/pydantic_core-2.41.1-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5f1d5d6bbba484bdf220c72d8ecd0be460f4bd4c5e534a541bb2cd57589fb8b", size = 2065288, upload-time = "2025-10-06T21:11:32.019Z" },
    +    { url = "https://files.pythonhosted.org/packages/0a/25/d77a73ff24e2e4fcea64472f5e39b0402d836da9b08b5361a734d0153023/pydantic_core-2.41.1-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2bf1917385ebe0f968dc5c6ab1375886d56992b93ddfe6bf52bff575d03662be", size = 2189759, upload-time = "2025-10-06T21:11:33.753Z" },
    +    { url = "https://files.pythonhosted.org/packages/66/45/4a4ebaaae12a740552278d06fe71418c0f2869537a369a89c0e6723b341d/pydantic_core-2.41.1-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:4f94f3ab188f44b9a73f7295663f3ecb8f2e2dd03a69c8f2ead50d37785ecb04", size = 2140747, upload-time = "2025-10-06T21:11:35.781Z" },
    +    { url = "https://files.pythonhosted.org/packages/da/6d/b727ce1022f143194a36593243ff244ed5a1eb3c9122296bf7e716aa37ba/pydantic_core-2.41.1-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:3925446673641d37c30bd84a9d597e49f72eacee8b43322c8999fa17d5ae5bc4", size = 2327416, upload-time = "2025-10-06T21:11:37.75Z" },
    +    { url = "https://files.pythonhosted.org/packages/6f/8c/02df9d8506c427787059f87c6c7253435c6895e12472a652d9616ee0fc95/pydantic_core-2.41.1-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:49bd51cc27adb980c7b97357ae036ce9b3c4d0bb406e84fbe16fb2d368b602a8", size = 2318138, upload-time = "2025-10-06T21:11:39.463Z" },
    +    { url = "https://files.pythonhosted.org/packages/98/67/0cf429a7d6802536941f430e6e3243f6d4b68f41eeea4b242372f1901794/pydantic_core-2.41.1-cp314-cp314-win32.whl", hash = "sha256:a31ca0cd0e4d12ea0df0077df2d487fc3eb9d7f96bbb13c3c5b88dcc21d05159", size = 1998429, upload-time = "2025-10-06T21:11:41.989Z" },
    +    { url = "https://files.pythonhosted.org/packages/38/60/742fef93de5d085022d2302a6317a2b34dbfe15258e9396a535c8a100ae7/pydantic_core-2.41.1-cp314-cp314-win_amd64.whl", hash = "sha256:1b5c4374a152e10a22175d7790e644fbd8ff58418890e07e2073ff9d4414efae", size = 2028870, upload-time = "2025-10-06T21:11:43.66Z" },
    +    { url = "https://files.pythonhosted.org/packages/31/38/cdd8ccb8555ef7720bd7715899bd6cfbe3c29198332710e1b61b8f5dd8b8/pydantic_core-2.41.1-cp314-cp314-win_arm64.whl", hash = "sha256:4fee76d757639b493eb600fba668f1e17475af34c17dd61db7a47e824d464ca9", size = 1974275, upload-time = "2025-10-06T21:11:45.476Z" },
    +    { url = "https://files.pythonhosted.org/packages/e7/7e/8ac10ccb047dc0221aa2530ec3c7c05ab4656d4d4bd984ee85da7f3d5525/pydantic_core-2.41.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:f9b9c968cfe5cd576fdd7361f47f27adeb120517e637d1b189eea1c3ece573f4", size = 1875124, upload-time = "2025-10-06T21:11:47.591Z" },
    +    { url = "https://files.pythonhosted.org/packages/c3/e4/7d9791efeb9c7d97e7268f8d20e0da24d03438a7fa7163ab58f1073ba968/pydantic_core-2.41.1-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1ebc7ab67b856384aba09ed74e3e977dded40e693de18a4f197c67d0d4e6d8e", size = 2043075, upload-time = "2025-10-06T21:11:49.542Z" },
    +    { url = "https://files.pythonhosted.org/packages/2d/c3/3f6e6b2342ac11ac8cd5cb56e24c7b14afa27c010e82a765ffa5f771884a/pydantic_core-2.41.1-cp314-cp314t-win_amd64.whl", hash = "sha256:8ae0dc57b62a762985bc7fbf636be3412394acc0ddb4ade07fe104230f1b9762", size = 1995341, upload-time = "2025-10-06T21:11:51.497Z" },
    +    { url = "https://files.pythonhosted.org/packages/16/89/d0afad37ba25f5801735af1472e650b86baad9fe807a42076508e4824a2a/pydantic_core-2.41.1-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:68f2251559b8efa99041bb63571ec7cdd2d715ba74cc82b3bc9eff824ebc8bf0", size = 2124001, upload-time = "2025-10-07T10:49:54.369Z" },
    +    { url = "https://files.pythonhosted.org/packages/8e/c4/08609134b34520568ddebb084d9ed0a2a3f5f52b45739e6e22cb3a7112eb/pydantic_core-2.41.1-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:c7bc140c596097cb53b30546ca257dbe3f19282283190b1b5142928e5d5d3a20", size = 1941841, upload-time = "2025-10-07T10:49:56.248Z" },
    +    { url = "https://files.pythonhosted.org/packages/2a/43/94a4877094e5fe19a3f37e7e817772263e2c573c94f1e3fa2b1eee56ef3b/pydantic_core-2.41.1-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2896510fce8f4725ec518f8b9d7f015a00db249d2fd40788f442af303480063d", size = 1961129, upload-time = "2025-10-07T10:49:58.298Z" },
    +    { url = "https://files.pythonhosted.org/packages/a2/30/23a224d7e25260eb5f69783a63667453037e07eb91ff0e62dabaadd47128/pydantic_core-2.41.1-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ced20e62cfa0f496ba68fa5d6c7ee71114ea67e2a5da3114d6450d7f4683572a", size = 2148770, upload-time = "2025-10-07T10:49:59.959Z" },
    +    { url = "https://files.pythonhosted.org/packages/2b/3e/a51c5f5d37b9288ba30683d6e96f10fa8f1defad1623ff09f1020973b577/pydantic_core-2.41.1-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:b04fa9ed049461a7398138c604b00550bc89e3e1151d84b81ad6dc93e39c4c06", size = 2115344, upload-time = "2025-10-07T10:50:02.466Z" },
    +    { url = "https://files.pythonhosted.org/packages/5a/bd/389504c9e0600ef4502cd5238396b527afe6ef8981a6a15cd1814fc7b434/pydantic_core-2.41.1-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:b3b7d9cfbfdc43c80a16638c6dc2768e3956e73031fca64e8e1a3ae744d1faeb", size = 1927994, upload-time = "2025-10-07T10:50:04.379Z" },
    +    { url = "https://files.pythonhosted.org/packages/ff/9c/5111c6b128861cb792a4c082677e90dac4f2e090bb2e2fe06aa5b2d39027/pydantic_core-2.41.1-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eec83fc6abef04c7f9bec616e2d76ee9a6a4ae2a359b10c21d0f680e24a247ca", size = 1959394, upload-time = "2025-10-07T10:50:06.335Z" },
    +    { url = "https://files.pythonhosted.org/packages/14/3f/cfec8b9a0c48ce5d64409ec5e1903cb0b7363da38f14b41de2fcb3712700/pydantic_core-2.41.1-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6771a2d9f83c4038dfad5970a3eef215940682b2175e32bcc817bdc639019b28", size = 2147365, upload-time = "2025-10-07T10:50:07.978Z" },
    +    { url = "https://files.pythonhosted.org/packages/e6/6c/fa3e45c2b054a1e627a89a364917f12cbe3abc3e91b9004edaae16e7b3c5/pydantic_core-2.41.1-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:af2385d3f98243fb733862f806c5bb9122e5fba05b373e3af40e3c82d711cef1", size = 2112094, upload-time = "2025-10-07T10:50:25.513Z" },
    +    { url = "https://files.pythonhosted.org/packages/e5/17/7eebc38b4658cc8e6902d0befc26388e4c2a5f2e179c561eeb43e1922c7b/pydantic_core-2.41.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:6550617a0c2115be56f90c31a5370261d8ce9dbf051c3ed53b51172dd34da696", size = 1935300, upload-time = "2025-10-07T10:50:27.715Z" },
    +    { url = "https://files.pythonhosted.org/packages/2b/00/9fe640194a1717a464ab861d43595c268830f98cb1e2705aa134b3544b70/pydantic_core-2.41.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dc17b6ecf4983d298686014c92ebc955a9f9baf9f57dad4065e7906e7bee6222", size = 1970417, upload-time = "2025-10-07T10:50:29.573Z" },
    +    { url = "https://files.pythonhosted.org/packages/b2/ad/f4cdfaf483b78ee65362363e73b6b40c48e067078d7b146e8816d5945ad6/pydantic_core-2.41.1-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:42ae9352cf211f08b04ea110563d6b1e415878eea5b4c70f6bdb17dca3b932d2", size = 2190745, upload-time = "2025-10-07T10:50:31.48Z" },
    +    { url = "https://files.pythonhosted.org/packages/cb/c1/18f416d40a10f44e9387497ba449f40fdb1478c61ba05c4b6bdb82300362/pydantic_core-2.41.1-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:e82947de92068b0a21681a13dd2102387197092fbe7defcfb8453e0913866506", size = 2150888, upload-time = "2025-10-07T10:50:33.477Z" },
    +    { url = "https://files.pythonhosted.org/packages/42/30/134c8a921630d8a88d6f905a562495a6421e959a23c19b0f49b660801d67/pydantic_core-2.41.1-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:e244c37d5471c9acdcd282890c6c4c83747b77238bfa19429b8473586c907656", size = 2324489, upload-time = "2025-10-07T10:50:36.48Z" },
    +    { url = "https://files.pythonhosted.org/packages/9c/48/a9263aeaebdec81e941198525b43edb3b44f27cfa4cb8005b8d3eb8dec72/pydantic_core-2.41.1-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:1e798b4b304a995110d41ec93653e57975620ccb2842ba9420037985e7d7284e", size = 2322763, upload-time = "2025-10-07T10:50:38.751Z" },
    +    { url = "https://files.pythonhosted.org/packages/1d/62/755d2bd2593f701c5839fc084e9c2c5e2418f460383ad04e3b5d0befc3ca/pydantic_core-2.41.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f1fc716c0eb1663c59699b024428ad5ec2bcc6b928527b8fe28de6cb89f47efb", size = 2144046, upload-time = "2025-10-07T10:50:40.686Z" },
    +]
    +
     [[package]]
     name = "pyee"
     version = "13.0.1"
    @@ -412,6 +875,79 @@ wheels = [
         { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" },
     ]
     
    +[[package]]
    +name = "python-crfsuite"
    +version = "0.9.12"
    +source = { registry = "https://pypi.org/simple" }
    +sdist = { url = "https://files.pythonhosted.org/packages/b6/bb/946c0f96b4d3f7916f0558e19245d2248caebb3f470bcffae8fbf8d862e9/python_crfsuite-0.9.12.tar.gz", hash = "sha256:db37fccc3bd8f0c49c28a7697ca79c89d67b3fd5bf119122866169240ac4c480", size = 488298, upload-time = "2025-12-23T19:07:21.423Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/2d/c1/74843c44bbb7b047669be31da0fd9bb199ce679c789d6f1d21390ad07606/python_crfsuite-0.9.12-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9b7cd1f10ae5b6f13dac6e0a20456c28c3450bc86f5cf4b41a11250b2b4fc269", size = 319275, upload-time = "2025-12-23T19:06:31.361Z" },
    +    { url = "https://files.pythonhosted.org/packages/75/6a/21ece1901eb55b41839057eac982058a5bb270c5b73d7269564f3c8108e7/python_crfsuite-0.9.12-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:41cc55053799d6eac13d496f1bd9c28a73d39c99eb194f37e356bd557bcca3fe", size = 1229940, upload-time = "2025-12-23T19:06:32.757Z" },
    +    { url = "https://files.pythonhosted.org/packages/18/88/6152166516a59534d54ff4fba0a2d225a49aa7b9ee16c4017be34f32355f/python_crfsuite-0.9.12-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5cd2b153bed935e4d6bd37d35f8cba9fcb73c01b539bbb63153589a10ac9fc6f", size = 1261888, upload-time = "2025-12-23T19:06:34.45Z" },
    +    { url = "https://files.pythonhosted.org/packages/ba/56/9f0e56eaef64ba4097fbf2791bf8351912b10aa1c09845a1ee919e9add6e/python_crfsuite-0.9.12-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bb114a6c22c0df7c6a78921d1f0ef913116fc5486adca9a96e76f47ce2755311", size = 2192258, upload-time = "2025-12-23T19:06:36.173Z" },
    +    { url = "https://files.pythonhosted.org/packages/68/77/acd6926f67fbc97abf6e30e322ffd674c1ad213ed28bf9434cdf6c97bd7e/python_crfsuite-0.9.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:aec187fb28550b5a1611152011bf9d36797b26b5fca88278d9dbd40d9ac941c1", size = 2286268, upload-time = "2025-12-23T19:06:37.549Z" },
    +    { url = "https://files.pythonhosted.org/packages/3c/ee/05471f22bc17dfb0f719886f10102c30b5fc8bc7fff5c0d57f72e6e2d769/python_crfsuite-0.9.12-cp311-cp311-win32.whl", hash = "sha256:0bc929cbfe88b775361feba36f9632170d411a3a3a1e31d53e3a07452bb54588", size = 282960, upload-time = "2025-12-23T19:06:38.599Z" },
    +    { url = "https://files.pythonhosted.org/packages/73/60/c19773826d2c9ac26bd82af6e411cf596c7a62a8c8953704634e7b34a602/python_crfsuite-0.9.12-cp311-cp311-win_amd64.whl", hash = "sha256:3b646fe2ea2c172c1823272107039ac4d00aeea81acaf0253f5c16beb64926b0", size = 303261, upload-time = "2025-12-23T19:06:40.06Z" },
    +    { url = "https://files.pythonhosted.org/packages/2a/be/c388376c4ca05b383dd17f0e1024b85c726a009543afd21e145a5fafff97/python_crfsuite-0.9.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e68009911b28ff899da5a6be3ec1efc3c24886c92318d02d39ec29d329b08b90", size = 319352, upload-time = "2025-12-23T19:06:41.26Z" },
    +    { url = "https://files.pythonhosted.org/packages/7a/0c/9725b097738f4a6aac9ac4e5a5fc6494eca69f17663d3d6ba8d0ea3858d2/python_crfsuite-0.9.12-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7118a3b267c437a9701362f5eacd6d1ff2360305a9c872cc20a716cd005c13eb", size = 1207132, upload-time = "2025-12-23T19:06:43.035Z" },
    +    { url = "https://files.pythonhosted.org/packages/63/3f/da9732ccb24b71a7539470dcdfcd16c923692788f39553f37238f208ca55/python_crfsuite-0.9.12-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:891bf2a5f410f17c5f9d76ab7330178a10142d48ed12f5c15b84f4c23fee80c7", size = 1245808, upload-time = "2025-12-23T19:06:44.293Z" },
    +    { url = "https://files.pythonhosted.org/packages/45/6f/a0186566f7480725ec4027ed63a11a38c9cbbad53f7cd6ece4e0ec4961f9/python_crfsuite-0.9.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:812f963fb61cfa5bfbc91b92e058cee41808a9ce813c84ecab6691848cc3b51c", size = 2164442, upload-time = "2025-12-23T19:06:45.57Z" },
    +    { url = "https://files.pythonhosted.org/packages/57/81/4c82aad97851431ec70c1cf46fdd2a58d2f79f68fa36bf4b7b4b8ed7ea6e/python_crfsuite-0.9.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a696ef90c77344ba88e5d241ace35fd21ad31e43f878fc734668741db18ed186", size = 2266336, upload-time = "2025-12-23T19:06:47.123Z" },
    +    { url = "https://files.pythonhosted.org/packages/d6/0f/19a46db879e400b0b82e6760d5c19e106a74870b5c0ce744da2efc32b143/python_crfsuite-0.9.12-cp312-cp312-win32.whl", hash = "sha256:e32c826e43fe8ac5c3b436bbddd8483f735a5638ea5dc07778d505cde78dc875", size = 282261, upload-time = "2025-12-23T19:06:48.81Z" },
    +    { url = "https://files.pythonhosted.org/packages/36/ae/9a7bceadbc962871819b1752f8aa53f60e657579fb94520bfa9b0495acf3/python_crfsuite-0.9.12-cp312-cp312-win_amd64.whl", hash = "sha256:fa6258bf10d8185262dee8fe2ca8d3de3c7aecb990846329043fc895344cc939", size = 303081, upload-time = "2025-12-23T19:06:49.933Z" },
    +    { url = "https://files.pythonhosted.org/packages/ac/ea/29f9891b22903ab50ca90cc291c886803d519ee04bd62c2b3c601d3da41e/python_crfsuite-0.9.12-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2048d8768a0b4c6a6d9390e879a2b7a760bb57a7f2ba491316f5dc36f9cfd836", size = 318868, upload-time = "2025-12-23T19:06:51.119Z" },
    +    { url = "https://files.pythonhosted.org/packages/1d/74/61ccfdda6f98e8719dabf9fee18cf0660b0a33487cfad222fa5174f489a9/python_crfsuite-0.9.12-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5294008716b65606c4d416c3b2597ca14422359a4a84734ead239b29b95f2780", size = 1207620, upload-time = "2025-12-23T19:06:52.53Z" },
    +    { url = "https://files.pythonhosted.org/packages/40/06/8e2219853504de660fb497a2f99e729f69b6dba76e44ffbdbb77035d3695/python_crfsuite-0.9.12-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f1641b9263c3cd1190711d0383d871b002ad325aa800fcf3c8583ef36f0bb07", size = 1246409, upload-time = "2025-12-23T19:06:54.463Z" },
    +    { url = "https://files.pythonhosted.org/packages/66/dc/4c88933a6ab16ce797e8f30ff491f371bee4890ceb9b9a90192fdb278591/python_crfsuite-0.9.12-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f0e73d0a8859db0c3d1a7a3595a83810efc535d95cb79f2f675eb44ad7a7954a", size = 2166123, upload-time = "2025-12-23T19:06:56.3Z" },
    +    { url = "https://files.pythonhosted.org/packages/74/52/f396a51f2089df8efdd8607ce9bdcb5228c0594dfe0e6c7b99fcbdbbc563/python_crfsuite-0.9.12-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:96f27f343ff7e7cb1e29a785d8ed4626a3470f8d42c41cda734dcbaede566722", size = 2267988, upload-time = "2025-12-23T19:06:57.591Z" },
    +    { url = "https://files.pythonhosted.org/packages/ba/2e/abcb7d53eda1355c5ef9aff6a620ecc21f527c593d0a7a7aace51a2fd2cd/python_crfsuite-0.9.12-cp313-cp313-win32.whl", hash = "sha256:385fda7f407be778f6a9440dffdeed3cdafc6f6923065a856e45997626283589", size = 282157, upload-time = "2025-12-23T19:06:59.048Z" },
    +    { url = "https://files.pythonhosted.org/packages/7a/da/282120f8d88def791610ba8310413c6ae3002f945f008979c3e440a6ad88/python_crfsuite-0.9.12-cp313-cp313-win_amd64.whl", hash = "sha256:21334c298318d4de057eacaad2ed179b7f63640e9cbd0c8141d656f58e7bd3f1", size = 303082, upload-time = "2025-12-23T19:07:00.14Z" },
    +    { url = "https://files.pythonhosted.org/packages/8b/1f/79f885a496da25f61508ec87156b465b11931812b1400428c0dfb9e25c84/python_crfsuite-0.9.12-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2e18bb1d7b4913bc321a5768284c8e86b5eefcd583462bfed5875671223451a8", size = 324173, upload-time = "2025-12-23T19:07:01.417Z" },
    +    { url = "https://files.pythonhosted.org/packages/7a/4b/cf49ed0c20fb0f95920bfdea2c0401276bc773f882d4f15e715bea5ff2cb/python_crfsuite-0.9.12-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30028c9b6cd06cafb43861f2577d4ef5c57f90a59908efb3df38be9e6e7c1c98", size = 1203650, upload-time = "2025-12-23T19:07:02.729Z" },
    +    { url = "https://files.pythonhosted.org/packages/3b/72/eea7c742783c9aa15e9505b0361c9e40c4e3ba86ba7976179a590a8b1ab6/python_crfsuite-0.9.12-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2fe0e6760365d7288e63661c4ab3c1110ae0cb1c36fbbbed23e5e889c138eb1", size = 1237142, upload-time = "2025-12-23T19:07:04.424Z" },
    +    { url = "https://files.pythonhosted.org/packages/b1/3a/fb5822e0b07a2c15ac4b4ad41a64f00dde8235609f21036a2975eb034d65/python_crfsuite-0.9.12-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d8d0e416ae999f9ff8a183383d9b917d4818f677dd3e370d19b6d1f9786af4bf", size = 2166273, upload-time = "2025-12-23T19:07:05.782Z" },
    +    { url = "https://files.pythonhosted.org/packages/43/9e/b5d5a34a559f6afa41062f59962c7916c32a567abfa760ce384421d4c8f1/python_crfsuite-0.9.12-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1b7204fdadf596a968d115b94c419899f3299fbd9c753abfced933b831e1ace3", size = 2264115, upload-time = "2025-12-23T19:07:07.147Z" },
    +    { url = "https://files.pythonhosted.org/packages/99/d8/5c328763a0561677be72b64e992098d3086e2c93dc56d1042124c2326711/python_crfsuite-0.9.12-cp314-cp314-win32.whl", hash = "sha256:be282686a90134851aa636d38ea520ab73aabb8103e79de458fffd49ff016bd2", size = 288459, upload-time = "2025-12-23T19:07:08.232Z" },
    +    { url = "https://files.pythonhosted.org/packages/34/e4/a856fdd7c9047aee9b265fab3e72314c6ee4c15eb8338ab42bd6c685aeb4/python_crfsuite-0.9.12-cp314-cp314-win_amd64.whl", hash = "sha256:94ab3f1666ec4244d8190b7e624505bc6e845d54b4faa0dacd9ea8fd1ac7eef8", size = 309861, upload-time = "2025-12-23T19:07:09.449Z" },
    +    { url = "https://files.pythonhosted.org/packages/21/f7/7dd9e0d739e8a8fdc16e2508593172234bf6aaa1c9f48526320c099056b0/python_crfsuite-0.9.12-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1fac24a04ebe58fcfd8e6a3c48e1e03021427852b7495573d7fc41d0d9ee297d", size = 330693, upload-time = "2025-12-23T19:07:11.365Z" },
    +    { url = "https://files.pythonhosted.org/packages/8b/de/a50c52b0220fb8f6cdb9ba2a873003760ba2e11d4c0c8b5a8dd504d95ae4/python_crfsuite-0.9.12-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:532cfbeffe8c8b0a0bf360a31f6486e655b29703a02ecefaf86d519f12fd470b", size = 1232670, upload-time = "2025-12-23T19:07:12.653Z" },
    +    { url = "https://files.pythonhosted.org/packages/58/d4/771f3622c220660daed3796ef641ec5398fc8dbb22a9990ee0a757525647/python_crfsuite-0.9.12-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fa2a20a74a094bb80b76af7937f68b710d60539a2905d942ba655d90d5c90677", size = 1230571, upload-time = "2025-12-23T19:07:14.209Z" },
    +    { url = "https://files.pythonhosted.org/packages/6f/99/32126ca6c2ce1dba2fceeee7a5f1e0f51303b71e366a6faf5feb390d62a4/python_crfsuite-0.9.12-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ff3b8e8c524b952e0dd85aa3bc34f24b502507411e776739d9e4ad4b46e61f51", size = 2183590, upload-time = "2025-12-23T19:07:15.986Z" },
    +    { url = "https://files.pythonhosted.org/packages/00/31/ecb009bb74943fbc84788fe728d2e88797969e78f21864734039654c8bcd/python_crfsuite-0.9.12-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b15bd6bbc4bf893e84084e9be010f350e8dfcc716b40bd6e5243d34cbc7dfb61", size = 2257120, upload-time = "2025-12-23T19:07:17.914Z" },
    +    { url = "https://files.pythonhosted.org/packages/8c/ff/3381915d6713aa04af06b8fd542fe685b27f2a0d7f4abc3eb57356a7b65f/python_crfsuite-0.9.12-cp314-cp314t-win32.whl", hash = "sha256:e6f8d0b71329b015a6d167305c2c00097e39a947644f0cbdc7fb4f9ffc1dc28e", size = 301710, upload-time = "2025-12-23T19:07:19.024Z" },
    +    { url = "https://files.pythonhosted.org/packages/e7/75/45ebbe7884fad92d80566cc5842ff158d2fb42c0c702d730bb06407935ba/python_crfsuite-0.9.12-cp314-cp314t-win_amd64.whl", hash = "sha256:9a74ea7c043e0b12a68175502b948bb58153dafd3e90f69d63de3c4a37ce4f4b", size = 326819, upload-time = "2025-12-23T19:07:20.104Z" },
    +]
    +
    +[[package]]
    +name = "python-dateutil"
    +version = "2.9.0.post0"
    +source = { registry = "https://pypi.org/simple" }
    +dependencies = [
    +    { name = "six" },
    +]
    +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
    +]
    +
    +[[package]]
    +name = "python-dotenv"
    +version = "1.2.3"
    +source = { registry = "https://pypi.org/simple" }
    +sdist = { url = "https://files.pythonhosted.org/packages/6a/53/ed9d74092561d4b01a2ef1349d52cdbc135e526c245f366b089cfca6de49/python_dotenv-1.2.3.tar.gz", hash = "sha256:a20a594dabeaa385725aa239d5244871c143ecb356add8a20fcf23773a6c3a35", size = 58945, upload-time = "2026-08-16T16:54:54.067Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/0d/17/c5c6b53ddc18f297992099b3d9ec16c855c0ccc83263a21fe4d1c625ec6c/python_dotenv-1.2.3-py3-none-any.whl", hash = "sha256:904552145e8bfed22162c09dab1c2b9b54fefa7b23ba780f4f26ca0316b0f0d9", size = 22780, upload-time = "2026-08-16T16:54:52.473Z" },
    +]
    +
    +[[package]]
    +name = "pytimeparse2"
    +version = "1.7.1"
    +source = { registry = "https://pypi.org/simple" }
    +sdist = { url = "https://files.pythonhosted.org/packages/19/10/cc63fecd69905eb4d300fe71bd580e4a631483e9f53fdcb8c0ad345ce832/pytimeparse2-1.7.1.tar.gz", hash = "sha256:98668cdcba4890e1789e432e8ea0059ccf72402f13f5d52be15bdfaeb3a8b253", size = 10431, upload-time = "2023-05-11T21:40:55.774Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/17/9e/85abf91ef5df452f56498927affdb7128194d15644084f6c6722477c305b/pytimeparse2-1.7.1-py3-none-any.whl", hash = "sha256:a162ea6a7707fd0bb82dd99556efb783935f51885c8bdced0fce3fffe85ab002", size = 6136, upload-time = "2023-05-11T21:40:46.051Z" },
    +]
    +
     [[package]]
     name = "pywin32"
     version = "312"
    @@ -434,6 +970,183 @@ wheels = [
         { url = "https://files.pythonhosted.org/packages/23/ed/4532e9388e65fa16b46776ef47ad631a64eda1631884488af707666350ed/pywin32-312-cp315-cp315-win_arm64.whl", hash = "sha256:a8597d28f267b39074aef51fa593530082b39cbe5a074226096857b1fed2dfb9", size = 6840337, upload-time = "2026-06-04T07:49:57.531Z" },
     ]
     
    +[[package]]
    +name = "pyyaml"
    +version = "6.0.3"
    +source = { registry = "https://pypi.org/simple" }
    +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
    +    { url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
    +    { url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
    +    { url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
    +    { url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
    +    { url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
    +    { url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
    +    { url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
    +    { url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
    +    { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
    +    { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
    +    { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
    +    { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
    +    { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
    +    { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
    +    { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
    +    { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
    +    { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
    +    { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
    +    { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
    +    { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
    +    { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
    +    { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
    +    { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
    +    { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
    +    { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
    +    { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
    +    { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
    +    { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
    +    { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
    +    { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
    +    { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
    +    { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
    +    { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
    +    { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
    +    { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
    +    { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
    +    { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
    +    { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
    +    { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
    +    { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
    +    { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
    +    { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
    +    { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
    +    { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
    +    { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
    +    { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
    +]
    +
    +[[package]]
    +name = "rapidfuzz"
    +version = "3.14.6"
    +source = { registry = "https://pypi.org/simple" }
    +sdist = { url = "https://files.pythonhosted.org/packages/18/97/226c43b7b5d957bc3840ed52ea99eed261f99834c4619be7a4742cbaeafa/rapidfuzz-3.14.6.tar.gz", hash = "sha256:e13a8160d017b499ec7a2fa9d0ce1ae2e7377080815785819f966fb235d4eb60", size = 57955060, upload-time = "2026-08-30T21:45:51.097Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/4c/09/144d6fcd84fadb124d282f727d197a92dc48ae279e80d4b7d23795ba164d/rapidfuzz-3.14.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1c0dd0d765184366b6e213a8af3b0b3bb39dad27943bbfb193515d4ff96ac82a", size = 1975267, upload-time = "2026-08-30T21:41:54.195Z" },
    +    { url = "https://files.pythonhosted.org/packages/b9/8f/17985248f0f651a518b543f802fa706b7810cbe96a434a5a9dc24f99b7d2/rapidfuzz-3.14.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:0c61cade182f130c9903231946bd1074539121721693a918e7b70382ae802bd8", size = 1246874, upload-time = "2026-08-30T21:41:57.063Z" },
    +    { url = "https://files.pythonhosted.org/packages/de/8f/9cf3b552bb84911add3c86e014e8704d20ea4e274295686106dc010356ae/rapidfuzz-3.14.6-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3781cf14f9fc933d7198c2b25a8bbbd1a62b752746d5cd26de14957edc0e802f", size = 1394531, upload-time = "2026-08-30T21:41:58.745Z" },
    +    { url = "https://files.pythonhosted.org/packages/e3/7f/c4824d855cb1f89f8db0802b7ae22705187be55e0ab2f9873b574a0a6713/rapidfuzz-3.14.6-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:71a5bbfd00da1963f27dd1432068929694cf0e00007ae2b9c1ad2a187ec29a16", size = 1702106, upload-time = "2026-08-30T21:42:00.398Z" },
    +    { url = "https://files.pythonhosted.org/packages/9f/ff/556d3aefbd1f115fcda6bdf3ea578405fcaa44c233b525fda583943f3692/rapidfuzz-3.14.6-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:eabaf06ca4896c59cfd9162480f0d37a15a2304ce2efe83ae2bbcfa1cf13534e", size = 2735203, upload-time = "2026-08-30T21:42:02.115Z" },
    +    { url = "https://files.pythonhosted.org/packages/11/ae/a781ec62825990319483c82ef962b509e9ce22a67a9f97d63d70b2b175b9/rapidfuzz-3.14.6-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3d5d90bae3c6fb7ea34da968c9f23070e8440edb827a28b242580e0108110b14", size = 3180952, upload-time = "2026-08-30T21:42:03.918Z" },
    +    { url = "https://files.pythonhosted.org/packages/d9/cc/a8cdeaa64db2e914f3475551b19ea2a6187b5458b50eac707e10f1bcf9d7/rapidfuzz-3.14.6-cp311-cp311-manylinux_2_39_riscv64.whl", hash = "sha256:d6b58daadbe6974884ec39aee30cfb8bd2e126f8d03503f0069f70d5e84656a3", size = 1485205, upload-time = "2026-08-30T21:42:05.659Z" },
    +    { url = "https://files.pythonhosted.org/packages/09/4e/6394e8d79088124bf39a8103ac2ae166a3f62ffc67b51c4e869dfe38b6d1/rapidfuzz-3.14.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ab4386ef7c2cb3e5eb46e815be49715dfcd301bb9f0a431f18da7aa0007de54f", size = 2415347, upload-time = "2026-08-30T21:42:07.847Z" },
    +    { url = "https://files.pythonhosted.org/packages/f0/2e/92acf13a03c45884aabe9d637c620f5b7806e56bd6f6f8d8016f95614722/rapidfuzz-3.14.6-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:33a2f7faedaa3608c4876c41b448fc786d54e6cd7c6e732f7de466319b5a73c2", size = 2819438, upload-time = "2026-08-30T21:42:09.788Z" },
    +    { url = "https://files.pythonhosted.org/packages/95/54/3ed4286d9ebf0b623b021970a46d7befa053dd09c85cd213bfb2ad2a0bbc/rapidfuzz-3.14.6-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:adb160a100f6122aa45c78d686e198da3f9e815d4182e0c4fe730608479f7f9c", size = 2521065, upload-time = "2026-08-30T21:42:11.923Z" },
    +    { url = "https://files.pythonhosted.org/packages/6a/ff/ae8ecf60ce25eab3accfe5a0c9ba6499b02c5e2ab03ee9defdf5475eb4e7/rapidfuzz-3.14.6-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:ad60297c001d15af24338440bca85dfee8710e9e3222733c906b33e89d986166", size = 3319384, upload-time = "2026-08-30T21:42:14.191Z" },
    +    { url = "https://files.pythonhosted.org/packages/4a/1d/d39dfc6cdc5c1d0452d4af563c678f2d5821f0df306bc3ab9502f3555690/rapidfuzz-3.14.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3d5b1cfa67bbe6239a643bca1d986f8a07e0a045286c674946e1648c132baa46", size = 4297470, upload-time = "2026-08-30T21:42:16.667Z" },
    +    { url = "https://files.pythonhosted.org/packages/1b/f6/0a64983c5cf5b2ce8cf2ce4fc54ecd6b5ee6cd6a3af8b870657f28e31a07/rapidfuzz-3.14.6-cp311-cp311-win32.whl", hash = "sha256:46ddb42af4cad3ac9d5e0c97ee1e687500c529a1ad5cbf9c949ce35f6edd4537", size = 1902086, upload-time = "2026-08-30T21:42:18.576Z" },
    +    { url = "https://files.pythonhosted.org/packages/41/72/638db21d63041ba17c4ed482a8cd1fe6dc4d90bc84b2a28aaccc2611ff84/rapidfuzz-3.14.6-cp311-cp311-win_amd64.whl", hash = "sha256:737a57cbca3e5c16decac86e205727bcd4b99c52f77c48bb44123078c5cd9a7a", size = 1738042, upload-time = "2026-08-30T21:42:20.427Z" },
    +    { url = "https://files.pythonhosted.org/packages/10/f7/d0fb82451c1f0c701a742939120b32a092ac64bbacf8bf8fa21d61fc89e7/rapidfuzz-3.14.6-cp311-cp311-win_arm64.whl", hash = "sha256:19c1cda8198cc57ffd4ff69a1c02cbe4297e9ca7b506bca03ec584da0a9fe1ff", size = 1190829, upload-time = "2026-08-30T21:42:22.322Z" },
    +    { url = "https://files.pythonhosted.org/packages/03/d2/5a7646b185a61400220e4783d23461c1e864a9ee82ba443b18c218e2364b/rapidfuzz-3.14.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b46cecf27025e7a934332ade033e6a394da8a493f19fa1d835e3b2968a4ff7da", size = 1965178, upload-time = "2026-08-30T21:42:24.164Z" },
    +    { url = "https://files.pythonhosted.org/packages/8b/72/10fc4e414eeed7963e2f1c315c731cb68196f0478cb244c78a21f5ce8662/rapidfuzz-3.14.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1901414b135afb1a7f4b1ef940b95523b49cc5642aecf02af740f37567e98137", size = 1248230, upload-time = "2026-08-30T21:42:26.088Z" },
    +    { url = "https://files.pythonhosted.org/packages/39/e9/0794043c1a0af09cacdbb6a9e8b9b2079cdf73337e7c29b4a9f117415bb9/rapidfuzz-3.14.6-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96a548979cd939b2c69358a0f5088a408524fbf7454f04bf90939fa971e64310", size = 1380396, upload-time = "2026-08-30T21:42:27.97Z" },
    +    { url = "https://files.pythonhosted.org/packages/2f/73/9218cf4424ab86260ee88ebdb612c5ed4d9bfd6b6d1e2f3c3bf4599d13bf/rapidfuzz-3.14.6-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b22ef7e5e2341efc6216b666491022027b984e5aef93446064742f43f3c1d926", size = 1674037, upload-time = "2026-08-30T21:42:29.754Z" },
    +    { url = "https://files.pythonhosted.org/packages/a1/f5/bad528b6dfc608a48838508f270c79332ab05592703c9a46504ba95e9eab/rapidfuzz-3.14.6-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f0d2d95c787d812b9106cfbcb94ad37a49f59df9287e00a75eb61afc246e8759", size = 2722897, upload-time = "2026-08-30T21:42:31.737Z" },
    +    { url = "https://files.pythonhosted.org/packages/13/da/49ab137f788a0e03e872d4c6b3d5c9c6c6bed4e4ccea381f69c4d186341b/rapidfuzz-3.14.6-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0debb5f43662ea84d2f0228a0c7407ff647f9c3d13f3b692efff0cde46eebce0", size = 3168023, upload-time = "2026-08-30T21:42:33.663Z" },
    +    { url = "https://files.pythonhosted.org/packages/59/33/81ca664a15194b8b4a7e863b534e36c057724f9709c7781e9400d0edf024/rapidfuzz-3.14.6-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:1d253e1fe44648242a0029b42ba23adf238ed2a7eb3d8ed0a03731a23f074ae0", size = 1474666, upload-time = "2026-08-30T21:42:35.5Z" },
    +    { url = "https://files.pythonhosted.org/packages/87/eb/b16f9f8cc255c8dc7c0d7712aa7e7c12a6fd85c8b2b56665f2a24222a941/rapidfuzz-3.14.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e06c6050c9bf6cd72305e3e6a293918b2b92cf2a067007585a53898624902e3c", size = 2402289, upload-time = "2026-08-30T21:42:37.309Z" },
    +    { url = "https://files.pythonhosted.org/packages/4a/73/eaa1ca89f6ab12c0fe7f943226ce4ad1d2c67eb281dfd706279771fcff5a/rapidfuzz-3.14.6-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:d85a6e9180e53cde95c95dfeb05a2ac94ead4d9d803a8fd186d2719a678b8483", size = 2788332, upload-time = "2026-08-30T21:42:39.412Z" },
    +    { url = "https://files.pythonhosted.org/packages/5d/ad/db927fbe23f621dd292a6332a19822703084617c0281a88156a8c138d4e0/rapidfuzz-3.14.6-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:35db2670f69fa3a4eb4741055581477ff92f2cf39e7e06f43ebcb97c2192fe7c", size = 2510540, upload-time = "2026-08-30T21:42:41.629Z" },
    +    { url = "https://files.pythonhosted.org/packages/2d/b2/8e9012968fab837babe1292edcbe1c972605f5b3af19c7fcac2ded731d39/rapidfuzz-3.14.6-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:f9d93e5424d1e4c103b57906b8beba270e680afda3ffdff7ea3bc6173b37083c", size = 3299876, upload-time = "2026-08-30T21:42:43.803Z" },
    +    { url = "https://files.pythonhosted.org/packages/19/99/799ce99328ea97fe5d7510048ffea148b8ad4a838366f908691be52342a5/rapidfuzz-3.14.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9b0a501f37fb852c54469375baa25874246b3bbc8b6e21fb4cd186a32335868", size = 4277032, upload-time = "2026-08-30T21:42:46.08Z" },
    +    { url = "https://files.pythonhosted.org/packages/07/8a/995b4746c5bc1f561e64de1fa546927183fec7a369fe988716ef394a6d0a/rapidfuzz-3.14.6-cp312-cp312-win32.whl", hash = "sha256:9e974251a9833791bc557b46f975676a56c2d58946f795cd2964b095496dfdcc", size = 1887051, upload-time = "2026-08-30T21:42:48.265Z" },
    +    { url = "https://files.pythonhosted.org/packages/84/c4/12f01df5778227c8655fcd9b429fc001d43270f5d8d154edc9066bab1de3/rapidfuzz-3.14.6-cp312-cp312-win_amd64.whl", hash = "sha256:cfca36e4612208875e08611a779164b6cb8900ab8bbd3d82d4cfdfae9efbfac9", size = 1731992, upload-time = "2026-08-30T21:42:50.211Z" },
    +    { url = "https://files.pythonhosted.org/packages/19/8d/92217f0bc81ec458b4134ad53714b1be0cd3be21494227d73510b06467d6/rapidfuzz-3.14.6-cp312-cp312-win_arm64.whl", hash = "sha256:96bbd5a1c67d135334d02fae74f1d933fdda204ea03d544a59dab6b1cbfbf565", size = 1186693, upload-time = "2026-08-30T21:42:52.63Z" },
    +    { url = "https://files.pythonhosted.org/packages/a0/ad/4901a37256bc5027f3873ebd538b851349d7627d8aa2e91743c79b500f48/rapidfuzz-3.14.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:55dc9a55924b4ecfcf4a60a701bcfae7d9daf0129c41dc16139270d75be0996c", size = 1961301, upload-time = "2026-08-30T21:42:54.46Z" },
    +    { url = "https://files.pythonhosted.org/packages/b9/d3/5a56e26db79c00191bc7c5387a04dfa5b6326c2c81c468a976ee2aa8fa15/rapidfuzz-3.14.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bba0e9fad4dbea80227cde9cef3aaa984a934a84aec5f7505532e19838b14769", size = 1244370, upload-time = "2026-08-30T21:42:56.425Z" },
    +    { url = "https://files.pythonhosted.org/packages/2b/12/0958686418e596961642c41e9162906363649e70f6a12cfcff212f77ccb3/rapidfuzz-3.14.6-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b34b7ee4f4f760690d6477163aabbec05705b5dd764cb6c3a6ba95aa1fffc42", size = 1377336, upload-time = "2026-08-30T21:42:58.687Z" },
    +    { url = "https://files.pythonhosted.org/packages/60/09/a0a70c35996fa5225c8cddca38e2e594c82518aeefa08edb5d875ce0d82b/rapidfuzz-3.14.6-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:abe92a70134c8b40790bb5c78b2a0a790686c26e83b6e99a456127ca141fe06a", size = 1670277, upload-time = "2026-08-30T21:43:00.798Z" },
    +    { url = "https://files.pythonhosted.org/packages/9f/d7/b9deea614b32e933e37d77eecf539ffe2b41c0a922a6fd759993865e7ee5/rapidfuzz-3.14.6-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:659b41570fcc6e02631ac361c47cc8db9ad26d740e4be2177df1b63005a49174", size = 2722260, upload-time = "2026-08-30T21:43:02.655Z" },
    +    { url = "https://files.pythonhosted.org/packages/70/42/4bf9dc905df33bb4515895ff87f777d8df25a3617c0bf8f5d4716813d9ea/rapidfuzz-3.14.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6bb896f89a387219c671ebc33c4a636b222010cc3c5c83884a7fc8707bf0bbf9", size = 3165730, upload-time = "2026-08-30T21:43:04.632Z" },
    +    { url = "https://files.pythonhosted.org/packages/25/76/454acc3abfa6b958511d6e761f5a95e6c3128936a1eed4f23643c3267d8b/rapidfuzz-3.14.6-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:11d76bb2b2cd038df708ae18f521fb3a50af477cc5a0dffce812da43a2f1beb3", size = 1469515, upload-time = "2026-08-30T21:43:06.612Z" },
    +    { url = "https://files.pythonhosted.org/packages/2e/f9/29b0f0d7764423573d35db4970dd573b324f4d41abe74d48adca542bcf79/rapidfuzz-3.14.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:28e9ce91bd41a8203185887ef9b1541a891aa61c5c1cb2e46f1689cd4288d372", size = 2401073, upload-time = "2026-08-30T21:43:08.742Z" },
    +    { url = "https://files.pythonhosted.org/packages/7a/f7/86ac824a7dd2b58729187cc31edebfa7805418f66d97d625010b7383d1de/rapidfuzz-3.14.6-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:864658e5a10d249a2277374e800f944fe990346d70eea6f3a51b712b6dd01984", size = 2786567, upload-time = "2026-08-30T21:43:11.048Z" },
    +    { url = "https://files.pythonhosted.org/packages/c6/a6/39fc42e45eb8ee70304862523b2e55cfbd2561c560dd8da1071015fa0ff0/rapidfuzz-3.14.6-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:3c2444f5cd757ded2c3ba8b1734253b801b9b2ba9ecb3ee40cd505cebbfa7341", size = 2504907, upload-time = "2026-08-30T21:43:13.281Z" },
    +    { url = "https://files.pythonhosted.org/packages/0a/ea/61f25272239ffef036eb3de1cc63372dfbff27193ca6f9f259d844f41a9c/rapidfuzz-3.14.6-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:2cc9b5dde0ac89f7856f997ef917cac8e18e9dea473e9b3090a84bd600de6a91", size = 3298728, upload-time = "2026-08-30T21:43:15.518Z" },
    +    { url = "https://files.pythonhosted.org/packages/6d/02/f9bfff9e19e852b097afa837a8000592bcd714fe80827a76367b958771b8/rapidfuzz-3.14.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:faebff9b9a287fb673f9a66465a7e03043601c9bfe5e71c3f91b3f2e7b8a37f6", size = 4272030, upload-time = "2026-08-30T21:43:17.785Z" },
    +    { url = "https://files.pythonhosted.org/packages/b3/d4/5845698661cb23bc7935536c28f5b86b2b3606de1f54722c1cfac39f170a/rapidfuzz-3.14.6-cp313-cp313-win32.whl", hash = "sha256:4406b2517b85febcf9419f8fbcdfbd534872ea32608050f9562224933ca49a4c", size = 1886313, upload-time = "2026-08-30T21:43:20.173Z" },
    +    { url = "https://files.pythonhosted.org/packages/67/f1/5b7c56737b9e5af7523ea79e90df732e9e4b2fa66fe2b333ee013ea6e541/rapidfuzz-3.14.6-cp313-cp313-win_amd64.whl", hash = "sha256:c69fb0e064d10c79908dcda76d7ca8ecdf8393a39acbb74dbad3f709f2c60e95", size = 1728638, upload-time = "2026-08-30T21:43:22.169Z" },
    +    { url = "https://files.pythonhosted.org/packages/05/5e/fc1da16b7f5245a7cc61dc08f70391ddaa1c538be1cf92681e7c763b77a4/rapidfuzz-3.14.6-cp313-cp313-win_arm64.whl", hash = "sha256:a0c8bef04f6b1d9fdbb319576350af53151a64692d477db7d4844c220bc8e212", size = 1185777, upload-time = "2026-08-30T21:43:24.27Z" },
    +    { url = "https://files.pythonhosted.org/packages/67/9e/8f862d2c8d80ee02633f1c9ce3e5121ce955e61efae24a61a05dd8a55fef/rapidfuzz-3.14.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:0f8d6718e7edacdb16455c0472e7552fd518decb91e91250c58784fd6163f54f", size = 1964420, upload-time = "2026-08-30T21:43:26.328Z" },
    +    { url = "https://files.pythonhosted.org/packages/3e/28/282e8c76b7dcc91e8f5aa1a594168d2136639f29dfda11384c6d36aabca0/rapidfuzz-3.14.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:8fa7d45388dec34a86038f2a38380f4922b74b5dd8991247f629a531178db10f", size = 1246072, upload-time = "2026-08-30T21:43:28.475Z" },
    +    { url = "https://files.pythonhosted.org/packages/4b/ae/8e0f714c55180667d66346e46a3d680dd9809bcee1c5f03557a58b4f2ef6/rapidfuzz-3.14.6-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:760ee152af5e8b4d241a469f933ba2d7215248618ae19770fec7d80d9e149db6", size = 1381829, upload-time = "2026-08-30T21:43:30.67Z" },
    +    { url = "https://files.pythonhosted.org/packages/eb/9a/4a106d68033a81c24ab71129e3016cc6a27a668f30f436e729cae79048e5/rapidfuzz-3.14.6-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dbe3378db3ae0453accf6196e2ed943f43d416cfacdcb8883db105bc14a0130f", size = 1676195, upload-time = "2026-08-30T21:43:32.862Z" },
    +    { url = "https://files.pythonhosted.org/packages/6e/f0/b456a74d8e33051b76b3f156cf4d55f717614d68b44b6312ae1f5d85b31d/rapidfuzz-3.14.6-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:9ddb0ddf3ee616fdc066add4ef05639c5cf59b58d83779b6023488e5435f6191", size = 2714364, upload-time = "2026-08-30T21:43:35.003Z" },
    +    { url = "https://files.pythonhosted.org/packages/6d/56/1203b46cedefc3f0c16e10d87123fdd4ec0f2e209f65cd2bf221ec669217/rapidfuzz-3.14.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:08bc63b88048376114d1e66cf8fa6926495d03bb873eb87854fa74cf6848a70b", size = 3167618, upload-time = "2026-08-30T21:43:37.625Z" },
    +    { url = "https://files.pythonhosted.org/packages/57/17/fa4a0853979b885ff27488d9b80e7c5c985dfed74c5021ea95a3b54ddfad/rapidfuzz-3.14.6-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:50cd6718bcda7ec5293635a9d0b3fb5906251013d3b99ca403ba9dfa8965f661", size = 1471360, upload-time = "2026-08-30T21:43:39.852Z" },
    +    { url = "https://files.pythonhosted.org/packages/7d/f2/757615ab88f7922b4477f9c93356c4512d744ea042e3e2b41554aab5ec1e/rapidfuzz-3.14.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:63b0e84faec3c5706cae8ae51246ff103407d54efa32a615a548b7b67392ebcf", size = 2403946, upload-time = "2026-08-30T21:43:42.038Z" },
    +    { url = "https://files.pythonhosted.org/packages/8f/c3/1c2670ff528f7e625d7b552e7ebccd5c4dfdcb84dc08ee85d1bcc0cf1465/rapidfuzz-3.14.6-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9080a730fdcf3cb8a07464c90f9cf40c1b4ffc73a8375b56a8898aba619dda30", size = 2793123, upload-time = "2026-08-30T21:43:44.438Z" },
    +    { url = "https://files.pythonhosted.org/packages/5d/92/a01444687bb9a5a2679aa71325c227760e9c475cd02054b45fd8b219cb0c/rapidfuzz-3.14.6-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:178557c7a50c8c8d65369ede7f3d845bf23590a951c9a368caf166b105d58cf3", size = 2507361, upload-time = "2026-08-30T21:43:46.568Z" },
    +    { url = "https://files.pythonhosted.org/packages/98/90/43d80ba73fd297c744f7fe0a949af2a610b4b9be96688799c3e73d002b13/rapidfuzz-3.14.6-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:44f1cddbc2010700e2d88063d0ab64183efe2578d9b52770ce1cd283dda230c5", size = 3304287, upload-time = "2026-08-30T21:43:48.966Z" },
    +    { url = "https://files.pythonhosted.org/packages/5d/e9/fd9a160699b72b6857551642fe109a1d0a86b06b7ecc0d2b4bbecbc6b61b/rapidfuzz-3.14.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:17081a0e904c12bb4ed49619a2bbb6528f6af00fe850e7ace22487bfd2aea455", size = 4273338, upload-time = "2026-08-30T21:43:51.574Z" },
    +    { url = "https://files.pythonhosted.org/packages/d0/72/3bc42217fadd07ea0ff9d249cc8001d6f285197c253db95d3a03aac8c254/rapidfuzz-3.14.6-cp314-cp314-win32.whl", hash = "sha256:9e00c8c9500aacbc0c52b66369f54533ecbdcb92e5aa87e160fc8e293000a696", size = 1927357, upload-time = "2026-08-30T21:43:53.851Z" },
    +    { url = "https://files.pythonhosted.org/packages/57/8d/3ea3bf93a2f22858e1b1298126db35cbf58592d05571ca757f2f16071b17/rapidfuzz-3.14.6-cp314-cp314-win_amd64.whl", hash = "sha256:41ee893c4d7d0fb1844f6cad966540a833784b3bad2c239a0d80195d9231cef4", size = 1783090, upload-time = "2026-08-30T21:43:56.202Z" },
    +    { url = "https://files.pythonhosted.org/packages/13/17/4add9d94236b37b6f857a3bf34d696b32304e3debc6830584fda95413ac6/rapidfuzz-3.14.6-cp314-cp314-win_arm64.whl", hash = "sha256:10576c39fe6a49fad0bf1069371a77300ce166a3f36d2900d2d0bae08f297104", size = 1221915, upload-time = "2026-08-30T21:43:58.335Z" },
    +    { url = "https://files.pythonhosted.org/packages/23/a4/af0509bffac37645841e2a6b55a4c6c46f7b2fc0757610b0cba0cbcfa900/rapidfuzz-3.14.6-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1b0a9546a7328d3cfc2f1385501db7c4c374fb566dc1a3b22ad56092846c0134", size = 1994141, upload-time = "2026-08-30T21:44:00.931Z" },
    +    { url = "https://files.pythonhosted.org/packages/67/da/d46da45e393937509111d4affa4db794fb064341735cfdcffe1f5f13a78a/rapidfuzz-3.14.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9989280902b9c4ecf7de95fbb906e94df0d8c047290ed315c7aa1760cec9b3de", size = 1279969, upload-time = "2026-08-30T21:44:03.253Z" },
    +    { url = "https://files.pythonhosted.org/packages/4a/8a/1db5582d5c9684c57b1e292dc88d70177233b570e684fe30736140697658/rapidfuzz-3.14.6-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fc166efa4ca2fc9cc52e43784a54cbea95fc0e03e533f8266ef66b1c04c7cb76", size = 1381099, upload-time = "2026-08-30T21:44:05.402Z" },
    +    { url = "https://files.pythonhosted.org/packages/06/9b/a9dba69d174b4436c115fcd877a67745d355a859109e0f59955c14577519/rapidfuzz-3.14.6-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:32352a3ed1aad9c097d31fd4f2eece3030169e2de3dedde7a2fadc2652b768ad", size = 1638869, upload-time = "2026-08-30T21:44:07.51Z" },
    +    { url = "https://files.pythonhosted.org/packages/61/34/67915218f5f84ec2cda57560d81425929b8ea97956eb31283bf95768fefc/rapidfuzz-3.14.6-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ecb45d616002751b58914d5b7c2e66acd39e12242be12717a1258148a1b36526", size = 2687831, upload-time = "2026-08-30T21:44:09.709Z" },
    +    { url = "https://files.pythonhosted.org/packages/5e/80/07985e10b534dbdd48df0ddf2e42f9d27cf98dc44e09fe047fc4b38471f5/rapidfuzz-3.14.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f9ad513e3a3e045b60b421d5cd3887ae0a33b38fc6c6db3ea5e27c0a2e0412c", size = 3185373, upload-time = "2026-08-30T21:44:12.162Z" },
    +    { url = "https://files.pythonhosted.org/packages/91/09/db64291ce5f11c0f79486b435b49f5dc66680f605077cb011d282bf767b4/rapidfuzz-3.14.6-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:f35723caef8cc31b6f34209708fb172fc88bab0077c12e9b36bbb829baaf1b16", size = 1459628, upload-time = "2026-08-30T21:44:14.427Z" },
    +    { url = "https://files.pythonhosted.org/packages/d0/99/7eeaf6f7f42d4ec8b90db54c73f7c2a727e208b4db6fd5ea807e87133b9c/rapidfuzz-3.14.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:408b2e8e8c1ac71b57f0923cf964d6932539725e07b69e70ec66f22c4a403891", size = 2407348, upload-time = "2026-08-30T21:44:16.832Z" },
    +    { url = "https://files.pythonhosted.org/packages/19/bb/db04caff7bf26718e97592f8cc007988ef18eb088ebb0742addcb25f0819/rapidfuzz-3.14.6-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:5667c56fdc902fa1e12449b5c042e8b1c7e9b30040db20c396fbdb3d0a750866", size = 2758630, upload-time = "2026-08-30T21:44:19.196Z" },
    +    { url = "https://files.pythonhosted.org/packages/3f/26/962fc396a56ec37146eb5331e55ae53d19dc564fd921f49a6d524c83ee05/rapidfuzz-3.14.6-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:76a122fc573df603deb5fb827df31bb5efbd0826b50bb7aeca8535a6e8c70cf9", size = 2494519, upload-time = "2026-08-30T21:44:21.687Z" },
    +    { url = "https://files.pythonhosted.org/packages/83/0f/d2067e23d9b7fb2aeb70a6b36173f0b2376635483f670aa5c47f17e55135/rapidfuzz-3.14.6-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:e221366e24709b9d41d5f9cc99053b04cfc575d429e956a82cfbc4c4e9e8860a", size = 3262241, upload-time = "2026-08-30T21:44:24.218Z" },
    +    { url = "https://files.pythonhosted.org/packages/ce/bd/05e48e21b1dd722b41c0cb8ab8867996f6e0c0a1b46e42921ace09799b0c/rapidfuzz-3.14.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:36710ff214b7a8049d26a9c81d99948026593cacb47663742c4119072b651ecd", size = 4296246, upload-time = "2026-08-30T21:44:26.911Z" },
    +    { url = "https://files.pythonhosted.org/packages/12/ce/f4b355f05b17bdb3a56f1c5e9bd864965dbb810f93d1b5d6044ecfcbd42d/rapidfuzz-3.14.6-cp314-cp314t-win32.whl", hash = "sha256:66ece6f5e2586c742fc3e0b8487e06783d27c6c24adcdcfdd7f306afbd8b5737", size = 1977694, upload-time = "2026-08-30T21:44:29.431Z" },
    +    { url = "https://files.pythonhosted.org/packages/4a/15/d2c20c57b357ec4157e74a197b3f622dbda0b2a82d1fc708ed7b262758f9/rapidfuzz-3.14.6-cp314-cp314t-win_amd64.whl", hash = "sha256:cab4a932cec02d09471e2c9f1434049ef5bfe1f6e646ff10939c222dc610ad60", size = 1827262, upload-time = "2026-08-30T21:44:31.683Z" },
    +    { url = "https://files.pythonhosted.org/packages/15/e5/c38c19fbc1de82980e05bd3adbe1dc7f3dd0680e38e868646082317572d6/rapidfuzz-3.14.6-cp314-cp314t-win_arm64.whl", hash = "sha256:b056ce19eaea2ea70c6a6fb387a605ca2af8979de5b9d507597e8012820ddb14", size = 1245604, upload-time = "2026-08-30T21:44:34.066Z" },
    +    { url = "https://files.pythonhosted.org/packages/10/37/b015bf56f88e9b18b81ad462f610e70cc1145a9df39154fcbe7ddf9f8868/rapidfuzz-3.14.6-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:bc3d74d18543ddfbc8babe1faadb19927a7999fd0d01181cce9e721c14c36ab6", size = 1964451, upload-time = "2026-08-30T21:44:36.695Z" },
    +    { url = "https://files.pythonhosted.org/packages/d2/1a/7b88284d85b4f7dfdf3038263e11eb11871472aa32902c7063a5fdd7a7c5/rapidfuzz-3.14.6-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:aaa83b633d877a05d549d2073629134998d1b3b9dbc114873d3ff4277984979f", size = 1245701, upload-time = "2026-08-30T21:44:38.841Z" },
    +    { url = "https://files.pythonhosted.org/packages/a7/f3/444d939f4b6c3c86f67083cb792978f3f42c28f944e66e9152e910cd212a/rapidfuzz-3.14.6-cp315-cp315-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cbe6a62f71fcbca72acbf5a30e53380600369f257f951d664d81d30c0c598595", size = 1382770, upload-time = "2026-08-30T21:44:40.978Z" },
    +    { url = "https://files.pythonhosted.org/packages/23/a8/1830f07f7d3fcc56508135f130dbd24a917ddedb71107b04b2fbb33d5da9/rapidfuzz-3.14.6-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b82c21c30568e096ef2a9dda7d45c379e6141694e0472dac73bc4372ce13ccee", size = 3163591, upload-time = "2026-08-30T21:44:43.513Z" },
    +    { url = "https://files.pythonhosted.org/packages/10/e8/da76d94af820707dcbfce224b635fb7c389c19525426c31645c97bedd601/rapidfuzz-3.14.6-cp315-cp315-manylinux_2_39_riscv64.whl", hash = "sha256:fc950bb77105a2717d03d9f9c9e21e9ace7df2b8e864dd91edef7e32fa143be2", size = 1467985, upload-time = "2026-08-30T21:44:45.871Z" },
    +    { url = "https://files.pythonhosted.org/packages/30/75/5cfc0d1491e3c60a8669e8e2b78942c4f395cccabfb9c73bc8b209664e29/rapidfuzz-3.14.6-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:c53a269bdbd71ffbc856d3db9e609478251001ee272507578fa838bc2bd421fe", size = 2405269, upload-time = "2026-08-30T21:44:48.376Z" },
    +    { url = "https://files.pythonhosted.org/packages/c3/81/9c522c26cfe1909714eb840856106f1e419a44c4e0de034a3eeb873da00b/rapidfuzz-3.14.6-cp315-cp315-musllinux_1_2_riscv64.whl", hash = "sha256:bf4fb0f19c9dfce7a908c3e309753602ce3edb83bb74e9ff997e278765bf89df", size = 2507334, upload-time = "2026-08-30T21:44:50.903Z" },
    +    { url = "https://files.pythonhosted.org/packages/40/29/0bbd158eeddf05e5b581f89bf7c9f0cf330953579309b3806862d360a454/rapidfuzz-3.14.6-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:189ce2bf14938bfa003fbbe7e6da7584ed6ebbc4c560686255dbc20e2829f470", size = 4270388, upload-time = "2026-08-30T21:44:55.271Z" },
    +    { url = "https://files.pythonhosted.org/packages/be/be/2b67b32988cb96b7fa9461ff3436e275716df00f7817212ed0a1c1779062/rapidfuzz-3.14.6-cp315-cp315-win32.whl", hash = "sha256:7ca0f498bf771a87557e6d8b573aa6cf3daded58ae2eaeb6973618ce3e1615ad", size = 1927539, upload-time = "2026-08-30T21:44:57.796Z" },
    +    { url = "https://files.pythonhosted.org/packages/51/42/640e1bd16422392fbb6394def1f7dfd4d05bd13c986016ce4b3f91295430/rapidfuzz-3.14.6-cp315-cp315-win_amd64.whl", hash = "sha256:d4c5adb921b67dd79ffc0a14f92b9f8df3d012e66aab340b154ed87014229d93", size = 1783415, upload-time = "2026-08-30T21:45:00.091Z" },
    +    { url = "https://files.pythonhosted.org/packages/06/ba/c6966904eb7b3d1c6344e6c29245447625d156b11e9757b29adc3cb46037/rapidfuzz-3.14.6-cp315-cp315-win_arm64.whl", hash = "sha256:c9d135fb93709d707577da8a7a8ffc7283525a5b6d0ce55aa3724be5639ed65b", size = 1221931, upload-time = "2026-08-30T21:45:02.531Z" },
    +    { url = "https://files.pythonhosted.org/packages/ae/97/6dd7f10756eb703e11803c5c838191c2151112f632e29f5eacb1ed1cf86c/rapidfuzz-3.14.6-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:dd89abd1c4b3776c3471a817216830bd275441c8344bbda5d51a3bffe1e0fbdf", size = 1985107, upload-time = "2026-08-30T21:45:04.965Z" },
    +    { url = "https://files.pythonhosted.org/packages/75/4a/be587adefd9539a89cc6016bac44d222cda4c8212856759c82501fd89e4a/rapidfuzz-3.14.6-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:eab2d4680d7f438dbb1d484b187d59a943edea9c83f792c764a0c148a417a60a", size = 1272093, upload-time = "2026-08-30T21:45:07.304Z" },
    +    { url = "https://files.pythonhosted.org/packages/de/3f/982b2f1b2a16c46d4598829b6b2d7185921f146d5893630f917cb9d27542/rapidfuzz-3.14.6-cp315-cp315t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8683fefdd3484d64a191b3efbc8cbe9162c3eac891fd62d0a1b70e117ffcd434", size = 1371112, upload-time = "2026-08-30T21:45:09.699Z" },
    +    { url = "https://files.pythonhosted.org/packages/e6/12/2a1fe61cb9f0ac0dc4166bcb016df695047e75251481a197d47aa5ce8ea5/rapidfuzz-3.14.6-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2bc7af3a699371a941aac86dc8a79ac92adeb3c2add2aab02230e76068a0029e", size = 3175780, upload-time = "2026-08-30T21:45:12.266Z" },
    +    { url = "https://files.pythonhosted.org/packages/8d/01/abd33d0b7595643e598802a07466af388f1560d7b7cb70f442cc292f4067/rapidfuzz-3.14.6-cp315-cp315t-manylinux_2_39_riscv64.whl", hash = "sha256:40c2753e2d4dc96b25f8a25adc23ab0bb6cfd8bc8125a1753ac4b037d6ff6511", size = 1458364, upload-time = "2026-08-30T21:45:14.68Z" },
    +    { url = "https://files.pythonhosted.org/packages/9d/8e/efc98b0cfb540f41661f6a8bf21b67807e221102e5e8fb1585233b39a3bd/rapidfuzz-3.14.6-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:36a37ddc729c33618d89fa221d3333b9b956dc38cf15d31301e6169d962399a3", size = 2398037, upload-time = "2026-08-30T21:45:17.434Z" },
    +    { url = "https://files.pythonhosted.org/packages/7f/c1/4d89214a453215d897cc76cd6e13937c8ea5dc9f8217993fe2b1eeaf39a5/rapidfuzz-3.14.6-cp315-cp315t-musllinux_1_2_riscv64.whl", hash = "sha256:635f242f4bdf05d1477fa409815bd73e5f78896773ace84997bc472ffeef685f", size = 2497044, upload-time = "2026-08-30T21:45:20.328Z" },
    +    { url = "https://files.pythonhosted.org/packages/a9/2d/70aacf6cb577470bdd6f06890d25ecb7ee8a56baa07b114d5877a93ecedd/rapidfuzz-3.14.6-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:40d0cd9c82083aeb30bae8dee265ae571e6748d0d7b222ddd777f33d95a3b712", size = 4284741, upload-time = "2026-08-30T21:45:22.983Z" },
    +    { url = "https://files.pythonhosted.org/packages/92/7d/943a04a134a5d333c00d3a77169226defef5e081be9219a765afc176dda0/rapidfuzz-3.14.6-cp315-cp315t-win32.whl", hash = "sha256:15da2b258908eb38853c1a6a58a1d09d9aad9c721e03a68c8ba691cd31dff739", size = 1974478, upload-time = "2026-08-30T21:45:25.475Z" },
    +    { url = "https://files.pythonhosted.org/packages/21/0e/8356ca3e190e2bcced9b80e374d95b0925c4716b51e65720a55399983f41/rapidfuzz-3.14.6-cp315-cp315t-win_amd64.whl", hash = "sha256:3d502769263318690d4f6638b08483979d1b88cdc7c6f087482eea935fde4031", size = 1823286, upload-time = "2026-08-30T21:45:28.368Z" },
    +    { url = "https://files.pythonhosted.org/packages/fb/04/a0b0e6324b6384d1ab40feb4d16400af3b3101d38cbd15957edd9d17cbe0/rapidfuzz-3.14.6-cp315-cp315t-win_arm64.whl", hash = "sha256:07c7aa0b1e4b9999a54f9e73317d6743ff85442c8ef7b7fbbe6b190fd37d9e75", size = 1243815, upload-time = "2026-08-30T21:45:31.187Z" },
    +    { url = "https://files.pythonhosted.org/packages/08/9a/7d4949406e2d391e160ead12036bba05e7c90e09bba77a782d33e7e6a1b0/rapidfuzz-3.14.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0844066900cdc9909ce4ab4fb5ba1d8e0c021252d770f2ea476f3443df1d22ef", size = 1912210, upload-time = "2026-08-30T21:45:33.653Z" },
    +    { url = "https://files.pythonhosted.org/packages/7c/00/a1a077f5cf90c9fa13b28c721f931529ad02748d418d7750590a388832a9/rapidfuzz-3.14.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:1398bd2c197b79bfc40b615999fd3599dc60265fdd5b59edc18156ae048c4cde", size = 1209219, upload-time = "2026-08-30T21:45:36.035Z" },
    +    { url = "https://files.pythonhosted.org/packages/48/69/a573c2e5e1b1a4f19e98a8fb3f6a792a44f5b8a067895a2654890ffd35a4/rapidfuzz-3.14.6-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e2fc748d1fde4109e5d0dab27f1e61f53b3136a235dfee5a4fb579da44808b6a", size = 1361237, upload-time = "2026-08-30T21:45:38.582Z" },
    +    { url = "https://files.pythonhosted.org/packages/ea/0b/375ebdfc4ca149e23793bb6b72461954ec64d0acbb826030787e88b90ff3/rapidfuzz-3.14.6-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b42536675c930cb76b7998bfc4d8e59cb35d8df47f2103020265743b6b2ccd2a", size = 3136631, upload-time = "2026-08-30T21:45:41.426Z" },
    +    { url = "https://files.pythonhosted.org/packages/55/56/799accc99532ecaaa2c1d04c7e594d6bb8f1afdddc327389c61196741cb8/rapidfuzz-3.14.6-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:1e6911e3a14971719ddc35af98f181d2e5369ab273a5a3488ab7685d23c31ad5", size = 1722739, upload-time = "2026-08-30T21:45:44.301Z" },
    +]
    +
    +[[package]]
    +name = "ratelim"
    +version = "0.1.6"
    +source = { registry = "https://pypi.org/simple" }
    +dependencies = [
    +    { name = "decorator" },
    +]
    +sdist = { url = "https://files.pythonhosted.org/packages/c5/5a/e1440017bccb14523bb76356e6f3a5468386b8a9192bd901e98babd1a1ea/ratelim-0.1.6.tar.gz", hash = "sha256:826d32177e11f9a12831901c9fda6679fd5bbea3605910820167088f5acbb11d", size = 2793, upload-time = "2015-02-27T18:06:08.53Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/f2/98/7e6d147fd16a10a5f821db6e25f192265d6ecca3d82957a4fdd592cad49c/ratelim-0.1.6-py2.py3-none-any.whl", hash = "sha256:e1a7dd39e6b552b7cc7f52169cd66cdb826a1a30198e355d7016012987c9ad08", size = 4017, upload-time = "2015-02-27T18:06:06.464Z" },
    +]
    +
     [[package]]
     name = "regex"
     version = "2026.9.3"
    @@ -594,6 +1307,24 @@ wheels = [
         { url = "https://files.pythonhosted.org/packages/fc/07/d781f8f8e1ac24bef9f3269cf62ffb1407ca24c3a8f12e5e22874f90528c/ruff-0.16.6-py3-none-win_arm64.whl", hash = "sha256:7a976c79b958f94e50a022a19f0f8c87387448020935ec14fc74331bd0a7f2c5", size = 10412850, upload-time = "2026-09-03T16:57:26.416Z" },
     ]
     
    +[[package]]
    +name = "setuptools"
    +version = "84.0.0"
    +source = { registry = "https://pypi.org/simple" }
    +sdist = { url = "https://files.pythonhosted.org/packages/6d/44/f5da03a8ef95d369145c5bb53050e7877c9f3d312e128605fd9504829143/setuptools-84.0.0.tar.gz", hash = "sha256:f4695c21257f0d9b537ec2692c941d02ee143b7cc1276941349a546573b2ef73", size = 1168449, upload-time = "2026-08-08T18:27:58.365Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/95/9c/c510029fc6ef33a6275cd2c5d3cecd6613dfd6aa401d57c54f1c18852ccf/setuptools-84.0.0-py3-none-any.whl", hash = "sha256:51a52592b3b99e102b609654876bd65f19f999935166d1352678931132b0c670", size = 818216, upload-time = "2026-08-08T18:27:56.719Z" },
    +]
    +
    +[[package]]
    +name = "six"
    +version = "1.17.0"
    +source = { registry = "https://pypi.org/simple" }
    +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
    +]
    +
     [[package]]
     name = "soupsieve"
     version = "2.9.2"
    @@ -603,6 +1334,18 @@ wheels = [
         { url = "https://files.pythonhosted.org/packages/eb/dc/ad025c1ee131eba60c69f4dd5779b18fcf1e6b21a343e2162a84d5d133c7/soupsieve-2.9.2-py3-none-any.whl", hash = "sha256:8089a26fd974ca7a1f30276d3d8492ab266ab15af581642dfe8aa162e0c1c823", size = 37370, upload-time = "2026-08-07T00:57:23.524Z" },
     ]
     
    +[[package]]
    +name = "thefuzz"
    +version = "0.22.1"
    +source = { registry = "https://pypi.org/simple" }
    +dependencies = [
    +    { name = "rapidfuzz" },
    +]
    +sdist = { url = "https://files.pythonhosted.org/packages/81/4b/d3eb25831590d6d7d38c2f2e3561d3ba41d490dc89cd91d9e65e7c812508/thefuzz-0.22.1.tar.gz", hash = "sha256:7138039a7ecf540da323792d8592ef9902b1d79eb78c147d4f20664de79f3680", size = 19993, upload-time = "2024-01-19T19:18:23.135Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/82/4f/1695e70ceb3604f19eda9908e289c687ea81c4fecef4d90a9d1d0f2f7ae9/thefuzz-0.22.1-py3-none-any.whl", hash = "sha256:59729b33556850b90e1093c4cf9e618af6f2e4c985df193fdf3c5b5cf02ca481", size = 8245, upload-time = "2024-01-19T19:18:20.362Z" },
    +]
    +
     [[package]]
     name = "tiktoken"
     version = "0.14.0"
    @@ -673,6 +1416,39 @@ wheels = [
         { url = "https://files.pythonhosted.org/packages/49/d3/b8441a820a491ddfc024b0b0cf0393375b75ea13866d9c66727e54c2fc80/typing_extensions-4.16.0-py3-none-any.whl", hash = "sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8", size = 45571, upload-time = "2026-07-02T08:40:04.659Z" },
     ]
     
    +[[package]]
    +name = "typing-inspection"
    +version = "0.4.4"
    +source = { registry = "https://pypi.org/simple" }
    +dependencies = [
    +    { name = "typing-extensions" },
    +]
    +sdist = { url = "https://files.pythonhosted.org/packages/a3/26/b09b8010994eccc3c09092e6b34058f36a460eea2d4c3e8b910c695975a0/typing_inspection-0.4.4.tar.gz", hash = "sha256:547274fa6b0a561ccf549cc9524b999a578e737d015d8709d021f9d0d13bea47", size = 76928, upload-time = "2026-08-12T12:37:25.997Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/67/81/4add07e5172b7ac40d8ed5ff580409a7801a4fe26d529bdd915401dabfbe/typing_inspection-0.4.4-py3-none-any.whl", hash = "sha256:65b8397ba37ccbce054456aaccddfc91e6e3083c92824df348d96ca832f3f147", size = 14750, upload-time = "2026-08-12T12:37:24.648Z" },
    +]
    +
    +[[package]]
    +name = "unidecode"
    +version = "1.4.0"
    +source = { registry = "https://pypi.org/simple" }
    +sdist = { url = "https://files.pythonhosted.org/packages/94/7d/a8a765761bbc0c836e397a2e48d498305a865b70a8600fd7a942e85dcf63/Unidecode-1.4.0.tar.gz", hash = "sha256:ce35985008338b676573023acc382d62c264f307c8f7963733405add37ea2b23", size = 200149, upload-time = "2025-04-24T08:45:03.798Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/8f/b7/559f59d57d18b44c6d1250d2eeaa676e028b9c527431f5d0736478a73ba1/Unidecode-1.4.0-py3-none-any.whl", hash = "sha256:c3c7606c27503ad8d501270406e345ddb480a7b5f38827eafe4fa82a137f0021", size = 235837, upload-time = "2025-04-24T08:45:01.609Z" },
    +]
    +
    +[[package]]
    +name = "url-normalize"
    +version = "2.2.1"
    +source = { registry = "https://pypi.org/simple" }
    +dependencies = [
    +    { name = "idna" },
    +]
    +sdist = { url = "https://files.pythonhosted.org/packages/80/31/febb777441e5fcdaacb4522316bf2a527c44551430a4873b052d545e3279/url_normalize-2.2.1.tar.gz", hash = "sha256:74a540a3b6eba1d95bdc610c24f2c0141639f3ba903501e61a52a8730247ff37", size = 18846, upload-time = "2025-04-26T20:37:58.553Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/bc/d9/5ec15501b675f7bc07c5d16aa70d8d778b12375686b6efd47656efdc67cd/url_normalize-2.2.1-py3-none-any.whl", hash = "sha256:3deb687587dc91f7b25c9ae5162ffc0f057ae85d22b1e15cf5698311247f567b", size = 14728, upload-time = "2025-04-26T20:37:57.217Z" },
    +]
    +
     [[package]]
     name = "urllib3"
     version = "2.7.0"
    @@ -681,3 +1457,96 @@ sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e
     wheels = [
         { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
     ]
    +
    +[[package]]
    +name = "us"
    +version = "3.2.0"
    +source = { registry = "https://pypi.org/simple" }
    +dependencies = [
    +    { name = "jellyfish" },
    +]
    +sdist = { url = "https://files.pythonhosted.org/packages/35/12/06f87be706ccc5794569d14f903c2f755aa98e1a9d53e4e7e17d9986e9d1/us-3.2.0.tar.gz", hash = "sha256:cb223e85393dcc5171ead0dd212badc47f9667b23700fea3e7ea5f310d545338", size = 16046, upload-time = "2024-07-22T01:09:42.736Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/65/a8/1791660a87f03d10a3bce00401a66035999c91f5a9a6987569b84df5719d/us-3.2.0-py3-none-any.whl", hash = "sha256:571714ad6d473c72bbd2058a53404cdf4ecc0129e4f19adfcbeb4e2d7e3dc3e7", size = 13775, upload-time = "2024-07-22T01:09:41.432Z" },
    +]
    +
    +[[package]]
    +name = "usaddress"
    +version = "0.5.16"
    +source = { registry = "https://pypi.org/simple" }
    +dependencies = [
    +    { name = "probableparsing" },
    +    { name = "python-crfsuite" },
    +]
    +sdist = { url = "https://files.pythonhosted.org/packages/5e/f9/860e55d404434bba2c5d31ea2052d3aba4ffd32b352ebb9fd400c8ccbec5/usaddress-0.5.16.tar.gz", hash = "sha256:f7e614e2489c159cf85be1b6f86edbfaf329dbeedef4d002a025757afd1ec6c6", size = 914927, upload-time = "2025-08-07T14:53:55.717Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/b0/4a/ffeb924e9a5d51bdbfff2e2309fb938603dc8bee30b01cd0421e8212c95c/usaddress-0.5.16-py3-none-any.whl", hash = "sha256:ce2bd73e3a41176fa29f093e9ad153327a87862430f1b0b121f0cb230c182a6a", size = 70562, upload-time = "2025-08-07T14:53:54.397Z" },
    +]
    +
    +[[package]]
    +name = "usaddress-scourgify"
    +version = "0.6.0"
    +source = { registry = "https://pypi.org/simple" }
    +dependencies = [
    +    { name = "geocoder" },
    +    { name = "usaddress" },
    +    { name = "yaml-config" },
    +]
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/6c/94/c27759db9c3d838c32c601e1fa28692e6bffff1434c748d34c05ff90597a/usaddress_scourgify-0.6.0-py3-none-any.whl", hash = "sha256:2bd1e2136ac70ba2fe412482dcd6cfad40f5298dd49685e105b68fd89f881999", size = 27210, upload-time = "2023-12-14T22:42:11.868Z" },
    +]
    +
    +[[package]]
    +name = "wcwidth"
    +version = "0.8.3"
    +source = { registry = "https://pypi.org/simple" }
    +sdist = { url = "https://files.pythonhosted.org/packages/36/57/ed58088fafdf4c55a0ad6bde846502567645424d7ebf325230b9237f4085/wcwidth-0.8.3.tar.gz", hash = "sha256:d128512515fbf4612e0ff21fd6380399210318b7b54a9af59dff8454cf9730eb", size = 1458450, upload-time = "2026-08-28T18:10:06.875Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/c4/0e/57f6bb3024a597b2e8ec4aee710ffe62ddc95af2e2bb1ee7a7abdc22c68c/wcwidth-0.8.3-py3-none-any.whl", hash = "sha256:d5b73dba6158a595ec9370350e7f2637bcac8d6c5e4fde34f30fcffb6103a5e4", size = 331669, upload-time = "2026-08-28T18:10:04.909Z" },
    +]
    +
    +[[package]]
    +name = "webarena-verified"
    +version = "1.2.3"
    +source = { registry = "https://pypi.org/simple" }
    +dependencies = [
    +    { name = "beautifulsoup4" },
    +    { name = "compact-json" },
    +    { name = "geographiclib" },
    +    { name = "geopy" },
    +    { name = "jsonpath-ng" },
    +    { name = "pint" },
    +    { name = "price-parser" },
    +    { name = "pydantic" },
    +    { name = "python-dateutil" },
    +    { name = "pytimeparse2" },
    +    { name = "thefuzz" },
    +    { name = "unidecode" },
    +    { name = "url-normalize" },
    +    { name = "us" },
    +    { name = "usaddress" },
    +    { name = "usaddress-scourgify" },
    +    { name = "word2number" },
    +]
    +sdist = { url = "https://files.pythonhosted.org/packages/29/94/f6188ad5de7d299368c55aa0e57fb0ae07dbfc5065f56fab7b58cc800f93/webarena_verified-1.2.3.tar.gz", hash = "sha256:668ff976ec000f60592ca14e3d935df48facb0482bc81d3c077258cb2f139869", size = 303911, upload-time = "2026-02-07T22:56:07.048Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/d0/d6/f69571f12c96f7ae74335a0e48e165ed2bcedd981808e3fe3f64c70ac396/webarena_verified-1.2.3-py3-none-any.whl", hash = "sha256:91a0297a096a343a13ff5e0ebc35c39b3d926b27c4d5a8a57c833681acbc6b7a", size = 349324, upload-time = "2026-02-07T22:56:05.058Z" },
    +]
    +
    +[[package]]
    +name = "word2number"
    +version = "1.1"
    +source = { registry = "https://pypi.org/simple" }
    +sdist = { url = "https://files.pythonhosted.org/packages/4a/29/a31940c848521f0725f0df6b25dca8917f13a2025b0e8fcbe5d0457e45e6/word2number-1.1.zip", hash = "sha256:70e27a5d387f67b04c71fbb7621c05930b19bfd26efd6851e6e0f9969dcde7d0", size = 9723, upload-time = "2017-06-02T15:45:14.488Z" }
    +
    +[[package]]
    +name = "yaml-config"
    +version = "0.1.5"
    +source = { registry = "https://pypi.org/simple" }
    +dependencies = [
    +    { name = "pyyaml" },
    +]
    +sdist = { url = "https://files.pythonhosted.org/packages/ae/d9/001bf4da6c05edbce0aabc9e19a95841c5ac7c1749234d6b1ec96a3b7563/yaml-config-0.1.5.tar.gz", hash = "sha256:5924c92d8e7c49fa9ab2ef8bb3122794b261f217b14e69698a481d4e3ba535b5", size = 7125, upload-time = "2020-06-22T19:59:37.21Z" }
    +wheels = [
    +    { url = "https://files.pythonhosted.org/packages/20/2f/b3ea915cfe22b8388b07d2f712c5f9a9687829064c230cfb75fe962a9472/yaml_config-0.1.5-py2.py3-none-any.whl", hash = "sha256:8d30a65eab4eb303af252350375c75fbae179854d47e26327c7ae0a9177f29be", size = 9509, upload-time = "2020-06-22T19:59:34.058Z" },
    +]
    
    From de7e70b57a0330a05c99f2511dcaa1598603bde9 Mon Sep 17 00:00:00 2001
    From: kevin 
    Date: Tue, 8 Sep 2026 22:32:22 +0800
    Subject: [PATCH 002/127] Harden autonomous evaluation inputs
    
    ---
     .gitignore                        |  1 +
     examples/browser_agent/README.md  |  3 +++
     harness/browser_agent/model.py    |  7 ++++++-
     harness/browser_agent/runner.py   | 25 ++++++++++++++++++++++---
     tests/browser_agent/test_model.py | 13 +++++++++++++
     5 files changed, 45 insertions(+), 4 deletions(-)
    
    diff --git a/.gitignore b/.gitignore
    index 28287a01f..4991735eb 100644
    --- a/.gitignore
    +++ b/.gitignore
    @@ -30,4 +30,5 @@ run.log
     .pytest_cache/
     .ruff_cache/
     artifacts/browser_kernel/
    +artifacts/browser_agent/
     /docs/reports/browser-kernel-evaluation-*
    diff --git a/examples/browser_agent/README.md b/examples/browser_agent/README.md
    index db9982e5f..9db4fd0e3 100644
    --- a/examples/browser_agent/README.md
    +++ b/examples/browser_agent/README.md
    @@ -18,6 +18,9 @@ repository's `.env`, then run:
     
         ./examples/browser_agent/run.sh
     
    +`COWAGENT_REPO` is a trusted code-execution boundary, not a data input. The
    +runner refuses a dirty checkout and records its Git commit in the run config.
    +
     Every task runs once per engine. Only a failed official evaluation receives one
     retry. The official benchmark score is calculated from primary attempts only;
     retry recovery is reported separately and never replaces the primary score.
    diff --git a/harness/browser_agent/model.py b/harness/browser_agent/model.py
    index 848a0e54a..f059ee5b5 100644
    --- a/harness/browser_agent/model.py
    +++ b/harness/browser_agent/model.py
    @@ -174,6 +174,11 @@ def call_stream(self, request: Any) -> Iterable[dict[str, Any]]:
             )
             response.raise_for_status()
             self.last_response = response.json()
    +        response_id = self.last_response.get("id")
    +        if not isinstance(response_id, str) or not response_id:
    +            raise ValueError("OpenAI Responses API success payload omitted a non-empty id")
    +        if not isinstance(self.last_response.get("output"), list):
    +            raise ValueError("OpenAI Responses API success payload omitted the output list")
             self.request_count += 1
             usage = self.last_response.get("usage") or {}
             details = usage.get("output_tokens_details") or {}
    @@ -184,6 +189,6 @@ def call_stream(self, request: Any) -> Iterable[dict[str, Any]]:
                 ("total_tokens", usage.get("total_tokens")),
             ):
                 self.usage[key] = self.usage.get(key, 0) + int(value or 0)
    -        self._previous_response_id = str(self.last_response["id"])
    +        self._previous_response_id = response_id
             self._seen_message_count = len(messages)
             yield response_to_chat_chunk(self.last_response)
    diff --git a/harness/browser_agent/runner.py b/harness/browser_agent/runner.py
    index c5ce61bd6..9a3c526a0 100644
    --- a/harness/browser_agent/runner.py
    +++ b/harness/browser_agent/runner.py
    @@ -78,6 +78,21 @@ def _git_head(path: Path) -> str:
         ).stdout.strip()
     
     
    +def _require_clean_git_checkout(path: Path) -> str:
    +    commit = _git_head(path)
    +    status = subprocess.run(
    +        ["git", "-C", str(path), "status", "--porcelain"],
    +        check=True,
    +        capture_output=True,
    +        text=True,
    +    ).stdout
    +    if status.strip():
    +        raise RuntimeError(
    +            "CowAgent checkout must be clean so its recorded commit identifies the code"
    +        )
    +    return commit
    +
    +
     def _cdp_metadata(http_endpoint: str) -> dict[str, str]:
         response = requests.get(http_endpoint.rstrip("/") + "/json/version", timeout=15)
         response.raise_for_status()
    @@ -400,6 +415,7 @@ def run(args: argparse.Namespace) -> Path:  # noqa: PLR0915
         cowagent_repo = Path(args.cowagent_repo or env.get("COWAGENT_REPO", "")).resolve()
         if not (cowagent_repo / "agent/protocol/agent.py").exists():
             raise RuntimeError("--cowagent-repo must point to a CowAgent checkout")
    +    cowagent_commit = _require_clean_git_checkout(cowagent_repo)
     
         output_dir = Path(args.output).expanduser().resolve()
         output_dir.mkdir(parents=True, exist_ok=True)
    @@ -453,7 +469,7 @@ def run(args: argparse.Namespace) -> Path:  # noqa: PLR0915
                 {
                     "schema": "lex_browser_agent_run_config/1",
                     "selection_sha256": selection.sha256,
    -                "cowagent_commit": _git_head(cowagent_repo),
    +                "cowagent_commit": cowagent_commit,
                     "model": args.model,
                     "reasoning_effort": "low",
                     "browser_images": image_metadata,
    @@ -538,7 +554,7 @@ def run(args: argparse.Namespace) -> Path:  # noqa: PLR0915
             "selection": resolved,
             "protocol": {
                 "agent": "CowAgent",
    -            "cowagent_commit": _git_head(cowagent_repo),
    +            "cowagent_commit": cowagent_commit,
                 "model": args.model,
                 "reasoning_effort": "low",
                 "primary_attempts_per_engine_task": 1,
    @@ -573,7 +589,10 @@ def parse_args() -> argparse.Namespace:
             "--selection", default="evaluation/browser-agent/webarena-verified-pilot.json"
         )
         parser.add_argument("--output", required=True)
    -    parser.add_argument("--cowagent-repo")
    +    parser.add_argument(
    +        "--cowagent-repo",
    +        help="trusted, clean CowAgent Git checkout; Python code is imported and executed from it",
    +    )
         parser.add_argument("--model", default="gpt-6-astra")
         parser.add_argument("--cpus", type=float, default=2.0)
         parser.add_argument("--memory", default="2g")
    diff --git a/tests/browser_agent/test_model.py b/tests/browser_agent/test_model.py
    index 89f7f003c..1ba51d6c6 100644
    --- a/tests/browser_agent/test_model.py
    +++ b/tests/browser_agent/test_model.py
    @@ -1,3 +1,5 @@
    +import pytest
    +
     from harness.browser_agent.model import (
         AstraResponsesModel,
         cow_messages_to_responses,
    @@ -141,3 +143,14 @@ def test_model_continues_responses_conversation_without_resending_assistant_call
         assert session.payloads[1]["input"] == [
             {"type": "function_call_output", "call_id": "call_1", "output": "done"}
         ]
    +
    +
    +def test_model_rejects_malformed_success_payload() -> None:
    +    class MalformedSession:
    +        def post(self, _url, **_kwargs):
    +            return _Response({"output": []})
    +
    +    model = AstraResponsesModel(api_key="test", session=MalformedSession())
    +
    +    with pytest.raises(ValueError, match="non-empty id"):
    +        list(model.call_stream(_Request([{"role": "user", "content": "open it"}])))
    
    From 109c1c5fb3720526431712308c39a2bae63c507d Mon Sep 17 00:00:00 2001
    From: kevin 
    Date: Tue, 8 Sep 2026 22:34:23 +0800
    Subject: [PATCH 003/127] Preflight Astra model access
    
    ---
     harness/browser_agent/model.py    | 12 ++++++++++++
     harness/browser_agent/runner.py   |  1 +
     tests/browser_agent/test_model.py |  9 +++++++++
     3 files changed, 22 insertions(+)
    
    diff --git a/harness/browser_agent/model.py b/harness/browser_agent/model.py
    index f059ee5b5..93d524920 100644
    --- a/harness/browser_agent/model.py
    +++ b/harness/browser_agent/model.py
    @@ -146,6 +146,18 @@ def reset(self) -> None:
         def _normalized_reasoning_effort(self) -> str:
             return self.reasoning_effort
     
    +    def verify_access(self) -> None:
    +        """Fail before benchmark attempts if the configured project cannot access the model."""
    +        response = self.session.get(
    +            f"{self.base_url}/models/{self.model}",
    +            headers={"Authorization": f"Bearer {self.api_key}"},
    +            timeout=30,
    +        )
    +        response.raise_for_status()
    +        payload = response.json()
    +        if payload.get("id") != self.model:
    +            raise ValueError(f"model lookup returned an unexpected id: {payload.get('id')!r}")
    +
         def call_stream(self, request: Any) -> Iterable[dict[str, Any]]:
             messages = list(request.messages or [])
             if self._previous_response_id:
    diff --git a/harness/browser_agent/runner.py b/harness/browser_agent/runner.py
    index 9a3c526a0..515fd1161 100644
    --- a/harness/browser_agent/runner.py
    +++ b/harness/browser_agent/runner.py
    @@ -428,6 +428,7 @@ def run(args: argparse.Namespace) -> Path:  # noqa: PLR0915
             model=args.model,
             reasoning_effort="low",
         )
    +    model.verify_access()
     
         client = docker.from_env()
         run_id = uuid.uuid4().hex[:12]
    diff --git a/tests/browser_agent/test_model.py b/tests/browser_agent/test_model.py
    index 1ba51d6c6..8b48e5898 100644
    --- a/tests/browser_agent/test_model.py
    +++ b/tests/browser_agent/test_model.py
    @@ -49,6 +49,9 @@ def post(self, _url, **kwargs):
                 }
             )
     
    +    def get(self, _url, **_kwargs):
    +        return _Response({"id": "gpt-6-astra"})
    +
     
     def test_translates_tool_round_trip() -> None:
         messages = [
    @@ -154,3 +157,9 @@ def post(self, _url, **_kwargs):
     
         with pytest.raises(ValueError, match="non-empty id"):
             list(model.call_stream(_Request([{"role": "user", "content": "open it"}])))
    +
    +
    +def test_model_access_preflight_checks_exact_model() -> None:
    +    model = AstraResponsesModel(api_key="test", session=_Session())
    +
    +    model.verify_access()
    
    From f68a398f2c7fced68a77043a49669df5cac709d8 Mon Sep 17 00:00:00 2001
    From: kevin 
    Date: Tue, 8 Sep 2026 22:53:05 +0800
    Subject: [PATCH 004/127] Clarify Astra interface and resumable pilot runs
    
    ---
     harness/browser_agent/report.py    |  2 +-
     harness/browser_agent/runner.py    | 21 ++++++++++++++++++++-
     tests/browser_agent/test_runner.py | 18 +++++++++++++++++-
     3 files changed, 38 insertions(+), 3 deletions(-)
    
    diff --git a/harness/browser_agent/report.py b/harness/browser_agent/report.py
    index d621d5dd5..0840c9c49 100644
    --- a/harness/browser_agent/report.py
    +++ b/harness/browser_agent/report.py
    @@ -189,7 +189,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path:
     

    Mind2Web 的 easy / medium / hard 要先纠正

    原始 Mind2Web 的官方拆分不是难度档,而是 cross-task、cross-website、cross-domain:测试任务是否见过同站任务、是否换了网站、是否换了整个领域。Online‑Mind2Web 官方也没有稳定的 easy/medium/hard 定义;有些第三方按参考动作长度再分档,那是第三方分析口径。Mind2Web 2 论文把任务 horizon 定义为 short(少于 10 次动作)、medium(10–50)、long(超过 50),仍不等同于 easy/medium/hard。

    所以本轮的 5/10/15 是我们自己的预注册操作长度层级。它只用于保证样本覆盖,不改写官方字段,也不把它当成跨论文可比较的难度标签。

    Browser Use 官方现在更偏向什么

    要区分三种信号。Browser Use 产品首页强调开放的 100 题 BU Bench V1,同时展示 Odysseys 的 200 题长链排行榜成绩;其 benchmark 仓库当前把 71 题 Stealth Bench V1 放在最前。Stealth Bench 测的是反机器人检测与浏览器基础设施,不是 CowAgent 能否自主完成业务任务,因此不能代替本报告的 agent 评测。

    BU Bench 由 Custom、WebBench、Mind2Web 2、GAIA、BrowseComp 各 20 题组成,另有独立的 300 题 Online‑Mind2Web 运行仓库。最值得借鉴的是:运行前确认任务可完成、保存逐题 trace、支持断点续跑、把“不可完成”与 Agent 真失败分开。Odysseys 还把长任务拆成平均 6.1 个可检查 rubric,并同时报告完整成功率、部分进度和每步效率。

    首轮不直接照搬的原因也很具体:这些工作流都包含模型评审,BU Bench 还混入搜索与问答任务,Odysseys 又是实时多站长链任务。它们适合第二阶段测真实互联网泛化和长链能力,但会把网页漂移、judge 波动和浏览器内核差异同时混进结果。

    WebArena‑Verified 本地部署要不要账号付费

    四个首轮站点使用官方 Docker 镜像和内置测试账号,本地运行不需要向 GitLab、Reddit 或电商站点申请真实账号,也没有按题平台费用。成本来自本机 CPU、内存、磁盘和 GPT‑6 Astra API。

    Astra 通过 Responses API 调用,模型 ID 是 gpt-6-astra,本轮固定 reasoning.effort=low。OpenAI 官方模型页把 Free API tier 标为不支持,因此需要一个已开通 API 计费且有该模型权限的 OpenAI 账号和项目密钥;Codex 不能代替团队注册账号、开通计费或承担费用。

    “本地可部署”也不等于轻量。本轮核对的官方 GitLab 镜像 manifest 含一个约 22.0 GB 的压缩主层,官方文档建议至少 4 GB 内存;四站点与两浏览器因此放在独立的 100 GB Docker profile 中。Wikipedia 与 Map 还需要额外数据卷,所以首轮不纳入。自托管能消除网站内容漂移、封号和真实支付风险,但不会自动消除镜像下载、环境初始化、登录兼容、浏览器 CDP 实现差异或 evaluator 缺陷。

    -

    本轮怎样保证是可比较的自主 Agent 实验

    {_table(['项目','预注册规则'], [['样本','固定 30 题:简单 5、中等 10、困难 15;运行后不换题,不重复抽样'],['Agent 与模型','CowAgent 真实自主观察、规划和调用 BrowserTool;GPT‑6 Astra,reasoning=low;每个试次清空模型会话'],['唯一变量','Agent、提示、模型、任务、站点数据和资源限额相同,只替换 Chromium / Moli'],['环境','每个试次都从官方镜像重置该题站点;Shopping、Shopping Admin、Reddit 使用官方头登录,GitLab 使用官方 UI 登录'],['顺序','逐题交替先跑 Chromium 或 Moli,减轻固定先后顺序偏差'],['主分','每个“题 × 浏览器”只跑一次,官方二元 pass@1;两边构成逐题配对'],['重试','只对首轮失败项再试一次,最多一次;重试仅报告恢复率,不回填或抬高主分'],['留证','保存最终 JSON、CowAgent 事件、Playwright trace、官方 evaluator 明细、镜像 ID、代码 commit、token 与资源指标'],['排除','首轮不选 Wikipedia / Map;不选无法由官方确定性 evaluator 验收的外部实时站点任务']])}

    “操作步数层级”只决定覆盖结构,不参与得分。环境准备失败应标成基础设施错误并修复后重跑同一预注册试次,不能计作 Agent 失败,也不能借机换题。

    +

    本轮怎样保证是可比较的自主 Agent 实验

    {_table(['项目','预注册规则'], [['样本','固定 30 题:简单 5、中等 10、困难 15;运行后不换题,不重复抽样'],['Agent 与模型','CowAgent 真实自主观察、规划和调用 BrowserTool;GPT‑6 Astra,reasoning=low;每个试次清空模型会话'],['工具接口','沿用 CowAgent 的页面快照与 browser 函数工具,不调用 Astra 原生 computer tool;测的是既有 CowAgent 接入方式下的自主 browser use'],['唯一变量','Agent、提示、模型、任务、站点数据和资源限额相同,只替换 Chromium / Moli'],['环境','每个试次都从官方镜像重置该题站点;Shopping、Shopping Admin、Reddit 使用官方头登录,GitLab 使用官方 UI 登录'],['顺序','逐题交替先跑 Chromium 或 Moli,减轻固定先后顺序偏差'],['主分','每个“题 × 浏览器”只跑一次,官方二元 pass@1;两边构成逐题配对'],['重试','只对首轮失败项再试一次,最多一次;重试仅报告恢复率,不回填或抬高主分'],['留证','保存最终 JSON、CowAgent 事件、Playwright trace、官方 evaluator 明细、镜像 ID、代码 commit、token 与资源指标'],['排除','首轮不选 Wikipedia / Map;不选无法由官方确定性 evaluator 验收的外部实时站点任务']])}

    “操作步数层级”只决定覆盖结构,不参与得分。环境准备失败应标成基础设施错误并修复后重跑同一预注册试次,不能计作 Agent 失败,也不能借机换题。由于工具接口不同,本轮结果不能直接代表 Astra 发布页中原生 computer-use harness 的成绩。

    {_result_section(attempts)}{_tier_section(attempts)}{_task_table(selection_path, attempts)}

    两条轨道必须分开读

    {_table(['轨道','Agent 是否自主','控制变量','主指标','回答的问题'], [['浏览器内核微基准(原 30 题)','否;重放固定 BrowserTool 等价调用','同一动作序列,只替换浏览器','严格输出正确率、动作执行、资源与耗时','相同操作交给两个内核,谁更兼容/省资源?'],['WebArena‑Verified Agent 评测(本报告)','是;CowAgent 自主观察、规划和调用 BrowserTool','同一 Agent、模型、提示、站点快照与任务,只替换浏览器','官方首轮 pass@1;失败重试恢复率另列','浏览器差异会怎样影响完整自主任务成功率?']])}

    固定动作轨道不是“有问题的假 Agent”,它是有价值的内核微基准;问题只是旧命名把它说成了自主 browser use。两条轨道互补,但分数不能相加,也不能用一条替代另一条。

    公开证据与复现边界

    采用统计只计公开一手材料。未找到 DeepSeek 或 Kimi 的官方浏览器交互 benchmark,不代表其内部没有评测;Kimi 公布的 BrowseComp 属于 agentic search,本报告不把它算成 UI browser use。

    diff --git a/harness/browser_agent/runner.py b/harness/browser_agent/runner.py index 515fd1161..cfa0dc460 100644 --- a/harness/browser_agent/runner.py +++ b/harness/browser_agent/runner.py @@ -406,6 +406,15 @@ def _write_or_validate_run_config(path: Path, planned: dict[str, Any]) -> None: path.write_text(json.dumps(planned, ensure_ascii=False, indent=2), encoding="utf-8") +def _scheduled_tasks(tasks: tuple[PilotTask, ...], max_tasks: int | None) -> tuple[PilotTask, ...]: + """Limit only this invocation; a later invocation resumes the same pre-registered cohort.""" + if max_tasks is None: + return tasks + if max_tasks < 1 or max_tasks > len(tasks): + raise ValueError(f"--max-tasks must be between 1 and {len(tasks)}") + return tasks[:max_tasks] + + def run(args: argparse.Namespace) -> Path: # noqa: PLR0915 repo_root = Path(args.repo_root).resolve() env = _load_repo_env(repo_root) @@ -485,7 +494,9 @@ def run(args: argparse.Namespace) -> Path: # noqa: PLR0915 ) with attempts_file.open("a", encoding="utf-8") as stream: - for task_index, selected in enumerate(selection.tasks): + for task_index, selected in enumerate( + _scheduled_tasks(selection.tasks, args.max_tasks) + ): order = ("chromium", "moli") if task_index % 2 == 0 else ("moli", "chromium") for engine in order: for attempt in (1, 2): @@ -598,6 +609,14 @@ def parse_args() -> argparse.Namespace: parser.add_argument("--cpus", type=float, default=2.0) parser.add_argument("--memory", default="2g") parser.add_argument("--sample-interval-ms", type=int, default=100) + parser.add_argument( + "--max-tasks", + type=int, + help=( + "run only the first N pre-registered tasks in this invocation; " + "resume later without this option to finish the unchanged cohort" + ), + ) return parser.parse_args() diff --git a/tests/browser_agent/test_runner.py b/tests/browser_agent/test_runner.py index edd30f0a5..31be9395d 100644 --- a/tests/browser_agent/test_runner.py +++ b/tests/browser_agent/test_runner.py @@ -1,6 +1,13 @@ +import pytest from webarena_verified.types.task import WebArenaSite -from harness.browser_agent.runner import SITE_AUTH_HEADERS, _configure_benchmark, _user_prompt +from harness.browser_agent.runner import ( + SITE_AUTH_HEADERS, + _configure_benchmark, + _scheduled_tasks, + _user_prompt, +) +from harness.browser_agent.selection import load_pilot def test_official_container_urls_and_header_auth() -> None: @@ -25,3 +32,12 @@ def test_user_prompt_exposes_schema_but_not_reference_answer() -> None: assert "task_type: RETRIEVE" in prompt assert '"type":"array"' in prompt assert "hollister" not in prompt + + +def test_max_tasks_limits_only_the_current_invocation() -> None: + tasks = load_pilot("evaluation/browser-agent/webarena-verified-pilot.json").tasks + + assert _scheduled_tasks(tasks, None) == tasks + assert _scheduled_tasks(tasks, 1) == tasks[:1] + with pytest.raises(ValueError, match="between 1 and 30"): + _scheduled_tasks(tasks, 0) From 3f6996a3612e091eda2ec7b2c0166512e0bbb030 Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 8 Sep 2026 23:08:30 +0800 Subject: [PATCH 005/127] Record evaluator-compatible network HAR --- harness/browser_agent/har.py | 156 ++++++++++++++++++++++++++++++++ harness/browser_agent/runner.py | 37 ++++++-- tests/browser_agent/test_har.py | 48 ++++++++++ 3 files changed, 233 insertions(+), 8 deletions(-) create mode 100644 harness/browser_agent/har.py create mode 100644 tests/browser_agent/test_har.py diff --git a/harness/browser_agent/har.py b/harness/browser_agent/har.py new file mode 100644 index 000000000..36538d417 --- /dev/null +++ b/harness/browser_agent/har.py @@ -0,0 +1,156 @@ +"""CDP network recorder that writes evaluator-compatible HAR.""" + +from __future__ import annotations + +import base64 +import json +from pathlib import Path +from typing import Any + + +def _headers(values: dict[str, Any]) -> list[dict[str, str]]: + return [{"name": str(name), "value": str(value)} for name, value in values.items()] + + +def _har_request(request: dict[str, Any]) -> dict[str, Any]: + headers = request.get("headers", {}) + result: dict[str, Any] = { + "method": str(request["method"]), + "url": str(request["url"]), + "headers": _headers(headers), + "cookies": [], + } + if request.get("postData") is not None: + content_type = next( + (str(value) for name, value in headers.items() if name.lower() == "content-type"), + "", + ) + result["postData"] = {"mimeType": content_type, "text": str(request["postData"])} + return result + + +def _har_response(response: dict[str, Any], body: dict[str, Any] | None) -> dict[str, Any]: + headers = response.get("headers", {}) + mime_type = str(response.get("mimeType") or "") + content: dict[str, Any] = {"mimeType": mime_type, "text": ""} + if body is not None: + content["text"] = str(body.get("body") or "") + if body.get("base64Encoded"): + content["encoding"] = "base64" + return { + "status": int(response["status"]), + "statusText": str(response.get("statusText") or ""), + "headers": _headers(headers), + "cookies": [], + "redirectURL": next( + (str(value) for name, value in headers.items() if name.lower() == "location"), + "", + ), + "content": content, + } + + +class CdpHarRecorder: + """Capture raw CDP requests from an existing browser context. + + Playwright trace ZIPs from an attached CDP browser can omit multipart POST + bodies. Raw ``Network.requestWillBeSent`` events retain those bodies, which + the official WebArena-Verified mutation evaluators require. + """ + + def __init__(self) -> None: + self._context: Any = None + self._sessions: list[Any] = [] + self._requests: dict[tuple[int, str], dict[str, Any]] = {} + self._responses: list[tuple[Any, str, dict[str, Any], dict[str, Any]]] = [] + self._finished: set[tuple[int, str]] = set() + self.errors: list[str] = [] + + def start(self, context: Any) -> None: + self._context = context + context.on("page", self._attach) + for page in context.pages: + self._attach(page) + + def stop(self, context: Any, path: Path) -> None: + if self._context is not None: + context.remove_listener("page", self._attach) + entries: list[dict[str, Any]] = [] + last_response_by_key = { + (id(session), request_id): index + for index, (session, request_id, _request, _response) in enumerate(self._responses) + } + for index, (session, request_id, request, response) in enumerate(self._responses): + body = None + key = (id(session), request_id) + mime_type = str(response.get("mimeType") or "") + if ( + index == last_response_by_key[key] + and key in self._finished + and (mime_type == "application/json" or mime_type.endswith("+json")) + ): + try: + body = session.send("Network.getResponseBody", {"requestId": request_id}) + if body.get("base64Encoded"): + base64.b64decode(body.get("body") or "", validate=True) + except Exception as exc: + self.errors.append(f"{request.get('url')}: response body unavailable: {exc}") + entries.append( + {"request": _har_request(request), "response": _har_response(response, body)} + ) + for session in self._sessions: + try: + session.detach() + except Exception as exc: + self.errors.append(f"CDP detach failed: {exc}") + self._context = None + path.write_text( + json.dumps( + { + "log": { + "version": "1.2", + "creator": {"name": "browser-eval", "version": "1"}, + "entries": entries, + } + }, + ensure_ascii=False, + ), + encoding="utf-8", + ) + + def _attach(self, page: Any) -> None: + try: + assert self._context is not None + session = self._context.new_cdp_session(page) + session_index = id(session) + session.on( + "Network.requestWillBeSent", + lambda event: self._record_request(session, session_index, event), + ) + session.on( + "Network.responseReceived", + lambda event: self._record_response(session, session_index, event), + ) + session.on( + "Network.loadingFinished", + lambda event: self._finished.add((session_index, str(event["requestId"]))), + ) + session.send("Network.enable") + self._sessions.append(session) + except Exception as exc: + self.errors.append(f"CDP attach failed: {exc}") + + def _record_request(self, session: Any, session_index: int, event: dict[str, Any]) -> None: + request_id = str(event["requestId"]) + key = (session_index, request_id) + previous = self._requests.get(key) + redirect_response = event.get("redirectResponse") + if previous is not None and redirect_response is not None: + self._responses.append((session, request_id, dict(previous), redirect_response)) + self._requests[key] = event["request"] + + def _record_response(self, session: Any, session_index: int, event: dict[str, Any]) -> None: + request_id = str(event["requestId"]) + request = self._requests.get((session_index, request_id)) + if request is not None: + self._responses.append((session, request_id, dict(request), event["response"])) diff --git a/harness/browser_agent/runner.py b/harness/browser_agent/runner.py index cfa0dc460..799b906b3 100644 --- a/harness/browser_agent/runner.py +++ b/harness/browser_agent/runner.py @@ -37,6 +37,7 @@ _wait_cdp, ) +from .har import CdpHarRecorder from .model import AstraResponsesModel from .selection import PilotTask, load_pilot, validate_against_official @@ -270,7 +271,8 @@ def _run_attempt( # noqa: PLR0913 attempt_dir = output_dir / engine / str(selected.task_id) / f"attempt-{attempt}" attempt_dir.mkdir(parents=True, exist_ok=True) - trace_path = attempt_dir / "network.zip" + trace_path = attempt_dir / "agent_trace.zip" + har_path = attempt_dir / "network.har" response_path = attempt_dir / "agent_response.json" eval_path = attempt_dir / "eval_result.json" events_path = attempt_dir / "agent_events.json" @@ -289,6 +291,7 @@ def _run_attempt( # noqa: PLR0913 raw_response = "" error = "" prepared = False + recorder = CdpHarRecorder() sampler_started = False started = time.perf_counter() try: @@ -296,7 +299,12 @@ def _run_attempt( # noqa: PLR0913 service = tool._get_service() service._submit(lambda: None) _login_context(service, sites, benchmark) - service._submit(lambda: service._context.tracing.start(screenshots=True, snapshots=True)) + service._submit( + lambda: ( + recorder.start(service._context), + service._context.tracing.start(screenshots=True, snapshots=True), + ) + ) prepared = True sampler.start() sampler_started = True @@ -317,12 +325,23 @@ def _run_attempt( # noqa: PLR0913 raw_response = agent.run_stream( _user_prompt(task, start_urls), on_event=events.append, clear_history=True ) - service._submit(lambda: service._context.tracing.stop(path=str(trace_path))) + service._submit( + lambda: ( + service._page.wait_for_timeout(500), + recorder.stop(service._context, har_path), + service._context.tracing.stop(path=str(trace_path)), + ) + ) except Exception: error = traceback.format_exc() try: service = tool._get_service() - service._submit(lambda: service._context.tracing.stop(path=str(trace_path))) + service._submit( + lambda: ( + recorder.stop(service._context, har_path), + service._context.tracing.stop(path=str(trace_path)), + ) + ) except Exception: pass finally: @@ -342,15 +361,15 @@ def _run_attempt( # noqa: PLR0913 json.dumps(parsed_response, ensure_ascii=False, indent=2), encoding="utf-8" ) events_path.write_text(json.dumps(events, ensure_ascii=False, indent=2), encoding="utf-8") - if trace_path.exists(): + if har_path.exists(): evaluation = benchmark.evaluate_task( task_id=selected.task_id, agent_response=parsed_response, - network_trace=trace_path, + network_trace=har_path, ) eval_data = evaluation.model_dump(mode="json") else: - eval_data = {"status": "error", "score": 0.0, "error_msg": error or "trace missing"} + eval_data = {"status": "error", "score": 0.0, "error_msg": error or "HAR missing"} eval_path.write_text(json.dumps(eval_data, ensure_ascii=False, indent=2), encoding="utf-8") resource = ( _resource_summary(resources, sampler, before, after, duration_ms) @@ -370,10 +389,12 @@ def _run_attempt( # noqa: PLR0913 "duration_ms": round(duration_ms, 3), "agent_response": parsed_response, "error": error, + "har_capture_errors": recorder.errors, "resource": resource, "model_usage": {**model.usage, "request_count": model.request_count}, "paths": { - "trace": str(trace_path), + "network_har": str(har_path), + "agent_trace": str(trace_path), "response": str(response_path), "evaluation": str(eval_path), "events": str(events_path), diff --git a/tests/browser_agent/test_har.py b/tests/browser_agent/test_har.py new file mode 100644 index 000000000..d9e2e8c0a --- /dev/null +++ b/tests/browser_agent/test_har.py @@ -0,0 +1,48 @@ +from harness.browser_agent.har import CdpHarRecorder, _har_request, _har_response + + +def test_har_preserves_multipart_post_data_and_json_response() -> None: + request = _har_request( + { + "method": "POST", + "url": "http://example.test/vote", + "headers": {"Content-Type": "multipart/form-data; boundary=test"}, + "postData": '--test\r\nContent-Disposition: form-data; name="choice"\r\n\r\n1', + } + ) + response = _har_response( + { + "status": 200, + "statusText": "OK", + "mimeType": "application/json", + "headers": {}, + }, + {"body": '{"items_qty":1}', "base64Encoded": False}, + ) + + assert request["postData"]["mimeType"] == "multipart/form-data; boundary=test" + assert 'name="choice"' in request["postData"]["text"] + assert response["content"]["text"] == '{"items_qty":1}' + + +def test_recorder_retains_redirect_response_before_request_id_is_reused() -> None: + recorder = CdpHarRecorder() + session = object() + recorder._record_request( + session, + 7, + {"requestId": "1", "request": {"method": "POST", "url": "http://x/edit"}}, + ) + recorder._record_request( + session, + 7, + { + "requestId": "1", + "redirectResponse": {"status": 302}, + "request": {"method": "GET", "url": "http://x/profile"}, + }, + ) + + assert recorder._responses[0][2]["method"] == "POST" + assert recorder._responses[0][3]["status"] == 302 + assert recorder._requests[(7, "1")]["method"] == "GET" From f6e5198591704d240c3f4c2bcc67d4f06d6b5d77 Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 8 Sep 2026 23:26:24 +0800 Subject: [PATCH 006/127] Gate trials on stable site readiness --- harness/browser_agent/har.py | 28 +++++++++++++++++++++++++-- harness/browser_agent/runner.py | 34 +++++++++++++++++++++++++++++++++ tests/browser_agent/test_har.py | 13 +++++++++++++ 3 files changed, 73 insertions(+), 2 deletions(-) diff --git a/harness/browser_agent/har.py b/harness/browser_agent/har.py index 36538d417..83aef4b87 100644 --- a/harness/browser_agent/har.py +++ b/harness/browser_agent/har.py @@ -13,7 +13,13 @@ def _headers(values: dict[str, Any]) -> list[dict[str, str]]: def _har_request(request: dict[str, Any]) -> dict[str, Any]: - headers = request.get("headers", {}) + headers = dict(request.get("headers", {})) + if request.get("__resource_type") == "Document" and not any( + name.lower() == "accept" for name in headers + ): + # CDP marks the request as a top-level document even when a browser + # implementation omits default Accept headers from requestWillBeSent. + headers["Accept"] = "text/html" result: dict[str, Any] = { "method": str(request["method"]), "url": str(request["url"]), @@ -62,6 +68,8 @@ def __init__(self) -> None: self._context: Any = None self._sessions: list[Any] = [] self._requests: dict[tuple[int, str], dict[str, Any]] = {} + self._request_ordinals: dict[tuple[int, str], int] = {} + self._extra_headers: dict[tuple[int, str], list[dict[str, Any]]] = {} self._responses: list[tuple[Any, str, dict[str, Any], dict[str, Any]]] = [] self._finished: set[tuple[int, str]] = set() self.errors: list[str] = [] @@ -83,6 +91,11 @@ def stop(self, context: Any, path: Path) -> None: for index, (session, request_id, request, response) in enumerate(self._responses): body = None key = (id(session), request_id) + request = dict(request) + ordinal = int(request.pop("__ordinal", 0)) + extras = self._extra_headers.get(key, []) + if ordinal < len(extras): + request["headers"] = {**request.get("headers", {}), **extras[ordinal]} mime_type = str(response.get("mimeType") or "") if ( index == last_response_by_key[key] @@ -131,6 +144,12 @@ def _attach(self, page: Any) -> None: "Network.responseReceived", lambda event: self._record_response(session, session_index, event), ) + session.on( + "Network.requestWillBeSentExtraInfo", + lambda event: self._extra_headers.setdefault( + (session_index, str(event["requestId"])), [] + ).append(event.get("headers", {})), + ) session.on( "Network.loadingFinished", lambda event: self._finished.add((session_index, str(event["requestId"]))), @@ -147,7 +166,12 @@ def _record_request(self, session: Any, session_index: int, event: dict[str, Any redirect_response = event.get("redirectResponse") if previous is not None and redirect_response is not None: self._responses.append((session, request_id, dict(previous), redirect_response)) - self._requests[key] = event["request"] + ordinal = self._request_ordinals.get(key, 0) + self._request_ordinals[key] = ordinal + 1 + request = dict(event["request"]) + request["__ordinal"] = ordinal + request["__resource_type"] = event.get("type") + self._requests[key] = request def _record_response(self, session: Any, session_index: int, event: dict[str, Any]) -> None: request_id = str(event["requestId"]) diff --git a/harness/browser_agent/runner.py b/harness/browser_agent/runner.py index 799b906b3..deb9d803f 100644 --- a/harness/browser_agent/runner.py +++ b/harness/browser_agent/runner.py @@ -130,6 +130,40 @@ def _ensure_site(site: WebArenaSite, *, reset: bool) -> None: ) if not ready.success: raise RuntimeError(f"{site.value} did not become ready: {ready.message}") + _wait_application_ready(site, manager.config.health_check_path) + + +def _wait_application_ready( + site: WebArenaSite, + health_path: str, + *, + timeout_seconds: float = 300, + stable_responses: int = 3, +) -> None: + """Require a stable app endpoint after env-ctrl reports ready. + + GitLab's control process can report ready during the Puma handover performed + by container initialization. A single successful request can therefore be + followed by a run of 502 responses. Consecutive app-level responses prevent + that startup race from consuming an Agent attempt. + """ + port = SITE_PORTS[site][0] + url = f"http://127.0.0.1:{port}{health_path}" + deadline = time.monotonic() + timeout_seconds + consecutive = 0 + last_detail = "no response" + while time.monotonic() < deadline: + try: + response = requests.get(url, timeout=15, allow_redirects=True) + last_detail = f"HTTP {response.status_code}" + consecutive = consecutive + 1 if response.status_code < 500 else 0 + if consecutive >= stable_responses: + return + except requests.RequestException as exc: + consecutive = 0 + last_detail = str(exc) + time.sleep(2) + raise RuntimeError(f"{site.value} application was not stably ready: {last_detail}") def _prepare_sites(sites: tuple[WebArenaSite, ...]) -> None: diff --git a/tests/browser_agent/test_har.py b/tests/browser_agent/test_har.py index d9e2e8c0a..d64ff04c4 100644 --- a/tests/browser_agent/test_har.py +++ b/tests/browser_agent/test_har.py @@ -46,3 +46,16 @@ def test_recorder_retains_redirect_response_before_request_id_is_reused() -> Non assert recorder._responses[0][2]["method"] == "POST" assert recorder._responses[0][3]["status"] == 302 assert recorder._requests[(7, "1")]["method"] == "GET" + + +def test_document_request_has_navigation_accept_fallback() -> None: + request = _har_request( + { + "method": "GET", + "url": "http://example.test/page", + "headers": {}, + "__resource_type": "Document", + } + ) + + assert {item["name"]: item["value"] for item in request["headers"]}["Accept"] == "text/html" From 7f6fd10f460b8b10b479a69d4e4f529fef6a261d Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 8 Sep 2026 23:29:06 +0800 Subject: [PATCH 007/127] Document official HAR scoring evidence --- harness/browser_agent/report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/harness/browser_agent/report.py b/harness/browser_agent/report.py index 0840c9c49..969fbe490 100644 --- a/harness/browser_agent/report.py +++ b/harness/browser_agent/report.py @@ -189,7 +189,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path:

    Mind2Web 的 easy / medium / hard 要先纠正

    原始 Mind2Web 的官方拆分不是难度档,而是 cross-task、cross-website、cross-domain:测试任务是否见过同站任务、是否换了网站、是否换了整个领域。Online‑Mind2Web 官方也没有稳定的 easy/medium/hard 定义;有些第三方按参考动作长度再分档,那是第三方分析口径。Mind2Web 2 论文把任务 horizon 定义为 short(少于 10 次动作)、medium(10–50)、long(超过 50),仍不等同于 easy/medium/hard。

    所以本轮的 5/10/15 是我们自己的预注册操作长度层级。它只用于保证样本覆盖,不改写官方字段,也不把它当成跨论文可比较的难度标签。

    Browser Use 官方现在更偏向什么

    要区分三种信号。Browser Use 产品首页强调开放的 100 题 BU Bench V1,同时展示 Odysseys 的 200 题长链排行榜成绩;其 benchmark 仓库当前把 71 题 Stealth Bench V1 放在最前。Stealth Bench 测的是反机器人检测与浏览器基础设施,不是 CowAgent 能否自主完成业务任务,因此不能代替本报告的 agent 评测。

    BU Bench 由 Custom、WebBench、Mind2Web 2、GAIA、BrowseComp 各 20 题组成,另有独立的 300 题 Online‑Mind2Web 运行仓库。最值得借鉴的是:运行前确认任务可完成、保存逐题 trace、支持断点续跑、把“不可完成”与 Agent 真失败分开。Odysseys 还把长任务拆成平均 6.1 个可检查 rubric,并同时报告完整成功率、部分进度和每步效率。

    首轮不直接照搬的原因也很具体:这些工作流都包含模型评审,BU Bench 还混入搜索与问答任务,Odysseys 又是实时多站长链任务。它们适合第二阶段测真实互联网泛化和长链能力,但会把网页漂移、judge 波动和浏览器内核差异同时混进结果。

    WebArena‑Verified 本地部署要不要账号付费

    四个首轮站点使用官方 Docker 镜像和内置测试账号,本地运行不需要向 GitLab、Reddit 或电商站点申请真实账号,也没有按题平台费用。成本来自本机 CPU、内存、磁盘和 GPT‑6 Astra API。

    Astra 通过 Responses API 调用,模型 ID 是 gpt-6-astra,本轮固定 reasoning.effort=low。OpenAI 官方模型页把 Free API tier 标为不支持,因此需要一个已开通 API 计费且有该模型权限的 OpenAI 账号和项目密钥;Codex 不能代替团队注册账号、开通计费或承担费用。

    “本地可部署”也不等于轻量。本轮核对的官方 GitLab 镜像 manifest 含一个约 22.0 GB 的压缩主层,官方文档建议至少 4 GB 内存;四站点与两浏览器因此放在独立的 100 GB Docker profile 中。Wikipedia 与 Map 还需要额外数据卷,所以首轮不纳入。自托管能消除网站内容漂移、封号和真实支付风险,但不会自动消除镜像下载、环境初始化、登录兼容、浏览器 CDP 实现差异或 evaluator 缺陷。

    -

    本轮怎样保证是可比较的自主 Agent 实验

    {_table(['项目','预注册规则'], [['样本','固定 30 题:简单 5、中等 10、困难 15;运行后不换题,不重复抽样'],['Agent 与模型','CowAgent 真实自主观察、规划和调用 BrowserTool;GPT‑6 Astra,reasoning=low;每个试次清空模型会话'],['工具接口','沿用 CowAgent 的页面快照与 browser 函数工具,不调用 Astra 原生 computer tool;测的是既有 CowAgent 接入方式下的自主 browser use'],['唯一变量','Agent、提示、模型、任务、站点数据和资源限额相同,只替换 Chromium / Moli'],['环境','每个试次都从官方镜像重置该题站点;Shopping、Shopping Admin、Reddit 使用官方头登录,GitLab 使用官方 UI 登录'],['顺序','逐题交替先跑 Chromium 或 Moli,减轻固定先后顺序偏差'],['主分','每个“题 × 浏览器”只跑一次,官方二元 pass@1;两边构成逐题配对'],['重试','只对首轮失败项再试一次,最多一次;重试仅报告恢复率,不回填或抬高主分'],['留证','保存最终 JSON、CowAgent 事件、Playwright trace、官方 evaluator 明细、镜像 ID、代码 commit、token 与资源指标'],['排除','首轮不选 Wikipedia / Map;不选无法由官方确定性 evaluator 验收的外部实时站点任务']])}

    “操作步数层级”只决定覆盖结构,不参与得分。环境准备失败应标成基础设施错误并修复后重跑同一预注册试次,不能计作 Agent 失败,也不能借机换题。由于工具接口不同,本轮结果不能直接代表 Astra 发布页中原生 computer-use harness 的成绩。

    +

    本轮怎样保证是可比较的自主 Agent 实验

    {_table(['项目','预注册规则'], [['样本','固定 30 题:简单 5、中等 10、困难 15;运行后不换题,不重复抽样'],['Agent 与模型','CowAgent 真实自主观察、规划和调用 BrowserTool;GPT‑6 Astra,reasoning=low;每个试次清空模型会话'],['工具接口','沿用 CowAgent 的页面快照与 browser 函数工具,不调用 Astra 原生 computer tool;测的是既有 CowAgent 接入方式下的自主 browser use'],['唯一变量','Agent、提示、模型、任务、站点数据和资源限额相同,只替换 Chromium / Moli'],['环境','每个试次都从官方镜像重置该题站点并等待应用连续稳定;Shopping、Shopping Admin、Reddit 使用官方头登录,GitLab 使用官方 UI 登录'],['顺序','逐题交替先跑 Chromium 或 Moli,减轻固定先后顺序偏差'],['主分','每个“题 × 浏览器”只跑一次,官方二元 pass@1;两边构成逐题配对'],['重试','只对首轮失败项再试一次,最多一次;重试仅报告恢复率,不回填或抬高主分'],['留证','保存最终 JSON、CowAgent 事件、标准网络 HAR、可视 Playwright trace、官方 evaluator 明细、镜像 ID、代码 commit、token 与资源指标'],['排除','首轮不选 Wikipedia / Map;不选无法由官方确定性 evaluator 验收的外部实时站点任务']])}

    “操作步数层级”只决定覆盖结构,不参与得分。环境准备失败应标成基础设施错误并修复后重跑同一预注册试次,不能计作 Agent 失败,也不能借机换题。官方 evaluator 读取 HAR;可视 trace 只用于调试,不参与评分。由于工具接口不同,本轮结果不能直接代表 Astra 发布页中原生 computer-use harness 的成绩。

    {_result_section(attempts)}{_tier_section(attempts)}{_task_table(selection_path, attempts)}

    两条轨道必须分开读

    {_table(['轨道','Agent 是否自主','控制变量','主指标','回答的问题'], [['浏览器内核微基准(原 30 题)','否;重放固定 BrowserTool 等价调用','同一动作序列,只替换浏览器','严格输出正确率、动作执行、资源与耗时','相同操作交给两个内核,谁更兼容/省资源?'],['WebArena‑Verified Agent 评测(本报告)','是;CowAgent 自主观察、规划和调用 BrowserTool','同一 Agent、模型、提示、站点快照与任务,只替换浏览器','官方首轮 pass@1;失败重试恢复率另列','浏览器差异会怎样影响完整自主任务成功率?']])}

    固定动作轨道不是“有问题的假 Agent”,它是有价值的内核微基准;问题只是旧命名把它说成了自主 browser use。两条轨道互补,但分数不能相加,也不能用一条替代另一条。

    公开证据与复现边界

    采用统计只计公开一手材料。未找到 DeepSeek 或 Kimi 的官方浏览器交互 benchmark,不代表其内部没有评测;Kimi 公布的 BrowseComp 属于 agentic search,本报告不把它算成 UI browser use。

    From 9032301a27a048236d97c6a3be835a4deea927f9 Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 8 Sep 2026 23:33:21 +0800 Subject: [PATCH 008/127] Preserve evidence across cleanup failures --- harness/browser_agent/har.py | 89 ++++++++++++++++++--------------- harness/browser_agent/runner.py | 20 ++++++-- tests/browser_agent/test_har.py | 13 +++++ 3 files changed, 79 insertions(+), 43 deletions(-) diff --git a/harness/browser_agent/har.py b/harness/browser_agent/har.py index 83aef4b87..cd2055939 100644 --- a/harness/browser_agent/har.py +++ b/harness/browser_agent/har.py @@ -72,6 +72,8 @@ def __init__(self) -> None: self._extra_headers: dict[tuple[int, str], list[dict[str, Any]]] = {} self._responses: list[tuple[Any, str, dict[str, Any], dict[str, Any]]] = [] self._finished: set[tuple[int, str]] = set() + self._payload: str | None = None + self._stopped = False self.errors: list[str] = [] def start(self, context: Any) -> None: @@ -81,44 +83,53 @@ def start(self, context: Any) -> None: self._attach(page) def stop(self, context: Any, path: Path) -> None: - if self._context is not None: - context.remove_listener("page", self._attach) - entries: list[dict[str, Any]] = [] - last_response_by_key = { - (id(session), request_id): index - for index, (session, request_id, _request, _response) in enumerate(self._responses) - } - for index, (session, request_id, request, response) in enumerate(self._responses): - body = None - key = (id(session), request_id) - request = dict(request) - ordinal = int(request.pop("__ordinal", 0)) - extras = self._extra_headers.get(key, []) - if ordinal < len(extras): - request["headers"] = {**request.get("headers", {}), **extras[ordinal]} - mime_type = str(response.get("mimeType") or "") - if ( - index == last_response_by_key[key] - and key in self._finished - and (mime_type == "application/json" or mime_type.endswith("+json")) - ): + if self._stopped: + return + if self._payload is None: + if self._context is not None: try: - body = session.send("Network.getResponseBody", {"requestId": request_id}) - if body.get("base64Encoded"): - base64.b64decode(body.get("body") or "", validate=True) + context.remove_listener("page", self._attach) except Exception as exc: - self.errors.append(f"{request.get('url')}: response body unavailable: {exc}") - entries.append( - {"request": _har_request(request), "response": _har_response(response, body)} - ) - for session in self._sessions: - try: - session.detach() - except Exception as exc: - self.errors.append(f"CDP detach failed: {exc}") - self._context = None - path.write_text( - json.dumps( + self.errors.append(f"page listener detach failed: {exc}") + entries: list[dict[str, Any]] = [] + last_response_by_key = { + (id(session), request_id): index + for index, (session, request_id, _request, _response) in enumerate( + self._responses + ) + } + for index, (session, request_id, request, response) in enumerate(self._responses): + body = None + key = (id(session), request_id) + request = dict(request) + ordinal = int(request.pop("__ordinal", 0)) + extras = self._extra_headers.get(key, []) + if ordinal < len(extras): + request["headers"] = {**request.get("headers", {}), **extras[ordinal]} + mime_type = str(response.get("mimeType") or "") + if ( + index == last_response_by_key[key] + and key in self._finished + and (mime_type == "application/json" or mime_type.endswith("+json")) + ): + try: + body = session.send("Network.getResponseBody", {"requestId": request_id}) + if body.get("base64Encoded"): + base64.b64decode(body.get("body") or "", validate=True) + except Exception as exc: + self.errors.append( + f"{request.get('url')}: response body unavailable: {exc}" + ) + entries.append( + {"request": _har_request(request), "response": _har_response(response, body)} + ) + for session in self._sessions: + try: + session.detach() + except Exception as exc: + self.errors.append(f"CDP detach failed: {exc}") + self._context = None + self._payload = json.dumps( { "log": { "version": "1.2", @@ -127,9 +138,9 @@ def stop(self, context: Any, path: Path) -> None: } }, ensure_ascii=False, - ), - encoding="utf-8", - ) + ) + path.write_text(self._payload, encoding="utf-8") + self._stopped = True def _attach(self, page: Any) -> None: try: diff --git a/harness/browser_agent/runner.py b/harness/browser_agent/runner.py index deb9d803f..4cc0c6895 100644 --- a/harness/browser_agent/runner.py +++ b/harness/browser_agent/runner.py @@ -379,11 +379,22 @@ def _run_attempt( # noqa: PLR0913 except Exception: pass finally: + cleanup_errors: list[str] = [] if sampler_started: - sampler.stop() + try: + sampler.stop() + except Exception as exc: + cleanup_errors.append(f"resource sampler stop failed: {exc}") duration_ms = (time.perf_counter() - started) * 1000 - after = resources.counters() - tool.close() + try: + after = resources.counters() + except Exception as exc: + after = None + cleanup_errors.append(f"final resource counters failed: {exc}") + try: + tool.close() + except Exception as exc: + cleanup_errors.append(f"browser tool close failed: {exc}") if not prepared: raise RuntimeError( @@ -407,7 +418,7 @@ def _run_attempt( # noqa: PLR0913 eval_path.write_text(json.dumps(eval_data, ensure_ascii=False, indent=2), encoding="utf-8") resource = ( _resource_summary(resources, sampler, before, after, duration_ms) - if sampler.samples + if sampler.samples and after is not None else {} ) return { @@ -423,6 +434,7 @@ def _run_attempt( # noqa: PLR0913 "duration_ms": round(duration_ms, 3), "agent_response": parsed_response, "error": error, + "cleanup_errors": cleanup_errors, "har_capture_errors": recorder.errors, "resource": resource, "model_usage": {**model.usage, "request_count": model.request_count}, diff --git a/tests/browser_agent/test_har.py b/tests/browser_agent/test_har.py index d64ff04c4..c797deb70 100644 --- a/tests/browser_agent/test_har.py +++ b/tests/browser_agent/test_har.py @@ -59,3 +59,16 @@ def test_document_request_has_navigation_accept_fallback() -> None: ) assert {item["name"]: item["value"] for item in request["headers"]}["Accept"] == "text/html" + + +def test_stop_is_idempotent_and_does_not_overwrite_har(tmp_path) -> None: + recorder = CdpHarRecorder() + path = tmp_path / "network.har" + recorder.stop(object(), path) + first = path.read_text() + path.write_text("sentinel") + + recorder.stop(object(), path) + + assert first != "sentinel" + assert path.read_text() == "sentinel" From 900950b54becbafec5ba5942cdd688deb679f47e Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 8 Sep 2026 23:36:41 +0800 Subject: [PATCH 009/127] Start every trial on the official task URL --- harness/browser_agent/runner.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/harness/browser_agent/runner.py b/harness/browser_agent/runner.py index 4cc0c6895..2f218ff40 100644 --- a/harness/browser_agent/runner.py +++ b/harness/browser_agent/runner.py @@ -177,11 +177,19 @@ def _prepare_sites(sites: tuple[WebArenaSite, ...]) -> None: def _login_context( - service: Any, sites: tuple[WebArenaSite, ...], benchmark: WebArenaVerified + service: Any, + sites: tuple[WebArenaSite, ...], + benchmark: WebArenaVerified, + start_urls: list[str], ) -> None: def login() -> None: context = service._context assert context is not None + primary_page = service._page + assert primary_page is not None + for existing_page in context.pages: + if existing_page != primary_page: + existing_page.close() context.clear_cookies() context.set_extra_http_headers( { @@ -226,6 +234,10 @@ def login() -> None: page.wait_for_timeout(1_000) finally: page.close() + primary_page.set_default_timeout(60_000) + primary_page.set_default_navigation_timeout(90_000) + primary_page.goto(start_urls[0], wait_until="domcontentloaded") + primary_page.wait_for_timeout(500) service._submit(login) @@ -296,6 +308,7 @@ def _run_attempt( # noqa: PLR0913 ) -> dict[str, Any]: task = benchmark.get_task(selected.task_id) sites = tuple(task.sites) + start_urls = list(benchmark.config.render_url(list(task.start_urls), list(task.sites))) _prepare_sites(sites) if str(cowagent_repo) not in sys.path: @@ -332,7 +345,7 @@ def _run_attempt( # noqa: PLR0913 model.reset() service = tool._get_service() service._submit(lambda: None) - _login_context(service, sites, benchmark) + _login_context(service, sites, benchmark, start_urls) service._submit( lambda: ( recorder.start(service._context), @@ -355,7 +368,6 @@ def _run_attempt( # noqa: PLR0913 # CowAgent rebuilds its base prompt at run time; the suffix is the supported # way to retain benchmark-specific completion and output constraints. agent.extra_system_suffix = _system_prompt() - start_urls = list(benchmark.config.render_url(list(task.start_urls), list(task.sites))) raw_response = agent.run_stream( _user_prompt(task, start_urls), on_event=events.append, clear_history=True ) From e9949fe850c2e802a39b087ccf78fcc87a1631f3 Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 8 Sep 2026 23:40:52 +0800 Subject: [PATCH 010/127] Test official start-page isolation --- tests/browser_agent/test_runner.py | 47 ++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/tests/browser_agent/test_runner.py b/tests/browser_agent/test_runner.py index 31be9395d..aef50760b 100644 --- a/tests/browser_agent/test_runner.py +++ b/tests/browser_agent/test_runner.py @@ -4,6 +4,7 @@ from harness.browser_agent.runner import ( SITE_AUTH_HEADERS, _configure_benchmark, + _login_context, _scheduled_tasks, _user_prompt, ) @@ -41,3 +42,49 @@ def test_max_tasks_limits_only_the_current_invocation() -> None: assert _scheduled_tasks(tasks, 1) == tasks[:1] with pytest.raises(ValueError, match="between 1 and 30"): _scheduled_tasks(tasks, 0) + + +def test_login_context_closes_old_tabs_and_opens_official_start_url() -> None: + class Page: + def __init__(self) -> None: + self.closed = False + self.url = "about:blank" + + def close(self) -> None: + self.closed = True + + def set_default_timeout(self, _timeout: int) -> None: + pass + + def set_default_navigation_timeout(self, _timeout: int) -> None: + pass + + def goto(self, url: str, **_kwargs) -> None: + self.url = url + + def wait_for_timeout(self, _timeout: int) -> None: + pass + + primary = Page() + stale = Page() + + class Context: + pages = [primary, stale] + + def clear_cookies(self) -> None: + pass + + def set_extra_http_headers(self, _headers) -> None: + pass + + class Service: + _context = Context() + _page = primary + + def _submit(self, callback): + return callback() + + _login_context(Service(), (), object(), ["http://official.test/start"]) + + assert stale.closed is True + assert primary.url == "http://official.test/start" From f0cdbc8eccef6b7cabcb32a8bf7675071d2e202a Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 8 Sep 2026 23:44:38 +0800 Subject: [PATCH 011/127] Normalize encoded JSON HAR bodies --- harness/browser_agent/har.py | 9 ++++++--- tests/browser_agent/test_har.py | 34 +++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/harness/browser_agent/har.py b/harness/browser_agent/har.py index cd2055939..2d6360b54 100644 --- a/harness/browser_agent/har.py +++ b/harness/browser_agent/har.py @@ -41,8 +41,6 @@ def _har_response(response: dict[str, Any], body: dict[str, Any] | None) -> dict content: dict[str, Any] = {"mimeType": mime_type, "text": ""} if body is not None: content["text"] = str(body.get("body") or "") - if body.get("base64Encoded"): - content["encoding"] = "base64" return { "status": int(response["status"]), "statusText": str(response.get("statusText") or ""), @@ -115,7 +113,12 @@ def stop(self, context: Any, path: Path) -> None: try: body = session.send("Network.getResponseBody", {"requestId": request_id}) if body.get("base64Encoded"): - base64.b64decode(body.get("body") or "", validate=True) + body = { + "body": base64.b64decode( + body.get("body") or "", validate=True + ).decode("utf-8", errors="replace"), + "base64Encoded": False, + } except Exception as exc: self.errors.append( f"{request.get('url')}: response body unavailable: {exc}" diff --git a/tests/browser_agent/test_har.py b/tests/browser_agent/test_har.py index c797deb70..d47f6f587 100644 --- a/tests/browser_agent/test_har.py +++ b/tests/browser_agent/test_har.py @@ -1,3 +1,6 @@ +import base64 +import json + from harness.browser_agent.har import CdpHarRecorder, _har_request, _har_response @@ -72,3 +75,34 @@ def test_stop_is_idempotent_and_does_not_overwrite_har(tmp_path) -> None: assert first != "sentinel" assert path.read_text() == "sentinel" + + +def test_stop_decodes_base64_json_for_official_har_parser(tmp_path) -> None: + class Session: + def send(self, _method, _params): + return { + "body": base64.b64encode(b'{"items_qty":1}').decode(), + "base64Encoded": True, + } + + def detach(self): + pass + + session = Session() + recorder = CdpHarRecorder() + recorder._sessions.append(session) + recorder._finished.add((id(session), "1")) + recorder._responses.append( + ( + session, + "1", + {"method": "GET", "url": "http://example.test/totals", "headers": {}}, + {"status": 200, "mimeType": "application/json", "headers": {}}, + ) + ) + path = tmp_path / "network.har" + + recorder.stop(object(), path) + + content = json.loads(path.read_text())["log"]["entries"][0]["response"]["content"] + assert content == {"mimeType": "application/json", "text": '{"items_qty":1}'} From 95b8aa1f10f4c009fc6cd8f85be5ba5954eb3532 Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 8 Sep 2026 23:49:04 +0800 Subject: [PATCH 012/127] Publish measured runtime identities --- harness/browser_agent/report.py | 44 ++++++++++++++++++++- harness/browser_agent/runner.py | 45 ++++++++++++++++++++-- tests/browser_agent/test_report.py | 62 ++++++++++++++++++++++++++++++ 3 files changed, 146 insertions(+), 5 deletions(-) diff --git a/harness/browser_agent/report.py b/harness/browser_agent/report.py index 969fbe490..6c7d33b77 100644 --- a/harness/browser_agent/report.py +++ b/harness/browser_agent/report.py @@ -25,6 +25,10 @@ def _read_attempts(evidence: Path) -> list[dict[str, Any]]: return [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line] +def _read_json(path: Path) -> dict[str, Any]: + return json.loads(path.read_text(encoding="utf-8")) if path.is_file() else {} + + def _primary_summary(rows: list[dict[str, Any]]) -> dict[str, dict[str, Any]]: result: dict[str, dict[str, Any]] = {} for engine in ("chromium", "moli"): @@ -165,6 +169,44 @@ def _task_table(selection_path: Path, attempts: list[dict[str, Any]]) -> str: return f"""

    预注册的 30 题

    选择文件在任何模型调用前固定,失败后不换题。样本含 23 个 RETRIEVE、4 个 MUTATE 和 3 个 NAVIGATE;按任务涉及站点计,Shopping Admin 12 题、Shopping 7 题、GitLab 6 题、Reddit 5 题。站点限定为这四个官方自托管环境;Wikipedia 与 Map 首轮排除,不是因为任务“太难”,而是它们分别要求大型 ZIM 和多套地图数据卷,会把环境下载与存储问题混进浏览器对比。

    {_table(['ID','本项目层级','估计最少步','官方 hard','入选理由','Chromium','Moli'], rows)}
    """ +def _environment_section(evidence: Path) -> str: + manifest = _read_json(evidence / "manifest.json") + run_config = _read_json(evidence / "run-config.json") + if not manifest or not run_config: + return """

    实测环境与证据身份

    正式运行尚未形成 manifest;发布门槛会阻止这份草稿作为完整结果上线。

    """ + protocol = manifest.get("protocol") or {} + environment = manifest.get("environment") or {} + versions = environment.get("browser_versions") or {} + images = environment.get("browser_images") or {} + official = environment.get("official_evaluator") or {} + rows = [ + ["CowAgent", str(protocol.get("cowagent_commit") or "—")], + ["模型", f"{protocol.get('model', '—')} · reasoning={protocol.get('reasoning_effort', '—')}"], + ["WebArena‑Verified", str(environment.get("webarena_verified_version") or "—")], + ["官方 evaluator checksum", str(official.get("evaluator_checksum") or "—")], + ["官方 data checksum", str(official.get("data_checksum") or "—")], + ["Docker", f"{environment.get('docker_architecture', '—')} · {environment.get('docker_server_version', '—')} · context={run_config.get('docker_context', '—')}"], + ] + for engine, label in (("chromium", "Chromium"), ("moli", "Moli")): + image = images.get(engine) or {} + rows.append( + [ + label, + f"{(versions.get(engine) or {}).get('Browser', '—')} · {image.get('architecture', '—')} · {str(image.get('id') or '—')}", + ] + ) + site_rows = [] + for site, image in sorted((environment.get("site_images") or {}).items()): + identity = (image.get("repo_digests") or [image.get("id") or "—"])[0] + site_rows.append([site, image.get("reference") or "—", identity]) + sites = ( + _table(["站点", "官方镜像", "digest / image ID"], site_rows) + if site_rows + else '

    站点镜像身份尚未记录。

    ' + ) + return f"""

    实测环境与证据身份

    {_table(['项目','记录值'], rows)}

    自托管站点镜像

    {sites}

    报告从运行 manifest 读取这些值,不按产品名推测版本。镜像 digest、浏览器 CDP 版本、代码 commit、数据 checksum 与选择集哈希共同限定本轮可复现边界。

    """ + + def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: attempts = _read_attempts(evidence) output.mkdir(parents=True, exist_ok=True) @@ -190,7 +232,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path:

    Browser Use 官方现在更偏向什么

    要区分三种信号。Browser Use 产品首页强调开放的 100 题 BU Bench V1,同时展示 Odysseys 的 200 题长链排行榜成绩;其 benchmark 仓库当前把 71 题 Stealth Bench V1 放在最前。Stealth Bench 测的是反机器人检测与浏览器基础设施,不是 CowAgent 能否自主完成业务任务,因此不能代替本报告的 agent 评测。

    BU Bench 由 Custom、WebBench、Mind2Web 2、GAIA、BrowseComp 各 20 题组成,另有独立的 300 题 Online‑Mind2Web 运行仓库。最值得借鉴的是:运行前确认任务可完成、保存逐题 trace、支持断点续跑、把“不可完成”与 Agent 真失败分开。Odysseys 还把长任务拆成平均 6.1 个可检查 rubric,并同时报告完整成功率、部分进度和每步效率。

    首轮不直接照搬的原因也很具体:这些工作流都包含模型评审,BU Bench 还混入搜索与问答任务,Odysseys 又是实时多站长链任务。它们适合第二阶段测真实互联网泛化和长链能力,但会把网页漂移、judge 波动和浏览器内核差异同时混进结果。

    WebArena‑Verified 本地部署要不要账号付费

    四个首轮站点使用官方 Docker 镜像和内置测试账号,本地运行不需要向 GitLab、Reddit 或电商站点申请真实账号,也没有按题平台费用。成本来自本机 CPU、内存、磁盘和 GPT‑6 Astra API。

    Astra 通过 Responses API 调用,模型 ID 是 gpt-6-astra,本轮固定 reasoning.effort=low。OpenAI 官方模型页把 Free API tier 标为不支持,因此需要一个已开通 API 计费且有该模型权限的 OpenAI 账号和项目密钥;Codex 不能代替团队注册账号、开通计费或承担费用。

    “本地可部署”也不等于轻量。本轮核对的官方 GitLab 镜像 manifest 含一个约 22.0 GB 的压缩主层,官方文档建议至少 4 GB 内存;四站点与两浏览器因此放在独立的 100 GB Docker profile 中。Wikipedia 与 Map 还需要额外数据卷,所以首轮不纳入。自托管能消除网站内容漂移、封号和真实支付风险,但不会自动消除镜像下载、环境初始化、登录兼容、浏览器 CDP 实现差异或 evaluator 缺陷。

    本轮怎样保证是可比较的自主 Agent 实验

    {_table(['项目','预注册规则'], [['样本','固定 30 题:简单 5、中等 10、困难 15;运行后不换题,不重复抽样'],['Agent 与模型','CowAgent 真实自主观察、规划和调用 BrowserTool;GPT‑6 Astra,reasoning=low;每个试次清空模型会话'],['工具接口','沿用 CowAgent 的页面快照与 browser 函数工具,不调用 Astra 原生 computer tool;测的是既有 CowAgent 接入方式下的自主 browser use'],['唯一变量','Agent、提示、模型、任务、站点数据和资源限额相同,只替换 Chromium / Moli'],['环境','每个试次都从官方镜像重置该题站点并等待应用连续稳定;Shopping、Shopping Admin、Reddit 使用官方头登录,GitLab 使用官方 UI 登录'],['顺序','逐题交替先跑 Chromium 或 Moli,减轻固定先后顺序偏差'],['主分','每个“题 × 浏览器”只跑一次,官方二元 pass@1;两边构成逐题配对'],['重试','只对首轮失败项再试一次,最多一次;重试仅报告恢复率,不回填或抬高主分'],['留证','保存最终 JSON、CowAgent 事件、标准网络 HAR、可视 Playwright trace、官方 evaluator 明细、镜像 ID、代码 commit、token 与资源指标'],['排除','首轮不选 Wikipedia / Map;不选无法由官方确定性 evaluator 验收的外部实时站点任务']])}

    “操作步数层级”只决定覆盖结构,不参与得分。环境准备失败应标成基础设施错误并修复后重跑同一预注册试次,不能计作 Agent 失败,也不能借机换题。官方 evaluator 读取 HAR;可视 trace 只用于调试,不参与评分。由于工具接口不同,本轮结果不能直接代表 Astra 发布页中原生 computer-use harness 的成绩。

    -{_result_section(attempts)}{_tier_section(attempts)}{_task_table(selection_path, attempts)} +{_result_section(attempts)}{_tier_section(attempts)}{_task_table(selection_path, attempts)}{_environment_section(evidence)}

    两条轨道必须分开读

    {_table(['轨道','Agent 是否自主','控制变量','主指标','回答的问题'], [['浏览器内核微基准(原 30 题)','否;重放固定 BrowserTool 等价调用','同一动作序列,只替换浏览器','严格输出正确率、动作执行、资源与耗时','相同操作交给两个内核,谁更兼容/省资源?'],['WebArena‑Verified Agent 评测(本报告)','是;CowAgent 自主观察、规划和调用 BrowserTool','同一 Agent、模型、提示、站点快照与任务,只替换浏览器','官方首轮 pass@1;失败重试恢复率另列','浏览器差异会怎样影响完整自主任务成功率?']])}

    固定动作轨道不是“有问题的假 Agent”,它是有价值的内核微基准;问题只是旧命名把它说成了自主 browser use。两条轨道互补,但分数不能相加,也不能用一条替代另一条。

    公开证据与复现边界

    采用统计只计公开一手材料。未找到 DeepSeek 或 Kimi 的官方浏览器交互 benchmark,不代表其内部没有评测;Kimi 公布的 BrowseComp 属于 agentic search,本报告不把它算成 UI browser use。

    """ diff --git a/harness/browser_agent/runner.py b/harness/browser_agent/runner.py index 2f218ff40..236ed1382 100644 --- a/harness/browser_agent/runner.py +++ b/harness/browser_agent/runner.py @@ -105,6 +105,18 @@ def _cdp_metadata(http_endpoint: str) -> dict[str, str]: } +def _docker_image_metadata(image: Any) -> dict[str, Any]: + attrs = image.attrs + return { + "id": image.id, + "tags": image.tags, + "repo_digests": attrs.get("RepoDigests") or [], + "architecture": attrs.get("Architecture"), + "os": attrs.get("Os"), + "size_bytes": attrs.get("Size"), + } + + def _configure_benchmark(hostname: str = "host.docker.internal") -> WebArenaVerified: environments = { site: EnvironmentConfig( @@ -527,6 +539,19 @@ def run(args: argparse.Namespace) -> Path: # noqa: PLR0915 run_config_file = output_dir / "run-config.json" completed = _load_completed_attempts(attempts_file) image_metadata: dict[str, dict[str, Any]] = {} + site_image_metadata = { + site.value: { + "reference": ContainerManager( + site=site, hostname="host.docker.internal" + ).config.docker_img, + **_docker_image_metadata( + client.images.get( + ContainerManager(site=site, hostname="host.docker.internal").config.docker_img + ) + ), + } + for site in SITE_PORTS + } browser_metadata: dict[str, dict[str, str]] = {} docker_info: dict[str, Any] = {} try: @@ -543,10 +568,7 @@ def run(args: argparse.Namespace) -> Path: # noqa: PLR0915 ) containers[engine] = container container.reload() - image_metadata[engine] = { - "id": container.image.id, - "tags": container.image.tags, - } + image_metadata[engine] = _docker_image_metadata(container.image) http_endpoint = _published_endpoint( container, engine=engine, cdp_port=ENGINE_PORTS[engine] ) @@ -562,6 +584,7 @@ def run(args: argparse.Namespace) -> Path: # noqa: PLR0915 "model": args.model, "reasoning_effort": "low", "browser_images": image_metadata, + "site_images": site_image_metadata, "browser_versions": browser_metadata, "docker_context": subprocess.run( ["docker", "context", "show"], @@ -636,6 +659,18 @@ def run(args: argparse.Namespace) -> Path: # noqa: PLR0915 except Exception: pass + evaluator_provenance: dict[str, Any] = {} + for row in completed.values(): + eval_path = Path(str((row.get("paths") or {}).get("evaluation") or "")) + if eval_path.is_file(): + eval_data = json.loads(eval_path.read_text(encoding="utf-8")) + evaluator_provenance = { + "version": eval_data.get("webarena_verified_version"), + "evaluator_checksum": eval_data.get("webarena_verified_evaluator_checksum"), + "data_checksum": eval_data.get("webarena_verified_data_checksum"), + } + break + manifest = { "schema": "lex_browser_agent_manifest/1", "run_id": run_id, @@ -665,6 +700,8 @@ def run(args: argparse.Namespace) -> Path: # noqa: PLR0915 "webarena_verified_version": importlib.metadata.version("webarena-verified"), "browser_images": image_metadata, "browser_versions": browser_metadata, + "site_images": site_image_metadata, + "official_evaluator": evaluator_provenance, }, } (output_dir / "manifest.json").write_text( diff --git a/tests/browser_agent/test_report.py b/tests/browser_agent/test_report.py index 45bf66339..fa0c61588 100644 --- a/tests/browser_agent/test_report.py +++ b/tests/browser_agent/test_report.py @@ -65,3 +65,65 @@ def test_report_keeps_primary_score_and_retry_recovery_separate(tmp_path: Path) data = json.loads((output / "report-data.json").read_text(encoding="utf-8")) assert data["summary"]["chromium"]["score"] == 0 assert data["summary"]["chromium"]["recovered"] == 1 + + +def test_report_renders_measured_runtime_identities(tmp_path: Path) -> None: + evidence = tmp_path / "evidence" + output = tmp_path / "report" + evidence.mkdir() + (evidence / "run-config.json").write_text( + json.dumps({"docker_context": "isolated-eval"}), encoding="utf-8" + ) + (evidence / "manifest.json").write_text( + json.dumps( + { + "protocol": { + "cowagent_commit": "abc123", + "model": "gpt-6-astra", + "reasoning_effort": "low", + }, + "environment": { + "docker_architecture": "aarch64", + "docker_server_version": "28.0", + "webarena_verified_version": "1.2.3", + "official_evaluator": { + "evaluator_checksum": "eval-checksum", + "data_checksum": "data-checksum", + }, + "browser_versions": { + "chromium": {"Browser": "Chrome/151"}, + "moli": {"Browser": "Moli/1.1.3"}, + }, + "browser_images": { + "chromium": {"architecture": "arm64", "id": "sha256:chrome"}, + "moli": {"architecture": "arm64", "id": "sha256:moli"}, + }, + "site_images": { + "reddit": { + "reference": "official/reddit:latest", + "repo_digests": ["official/reddit@sha256:site"], + } + }, + }, + } + ), + encoding="utf-8", + ) + + report = build_report( + evidence=evidence, + output=output, + selection_path=Path("evaluation/browser-agent/webarena-verified-pilot.json"), + ) + text = report.read_text(encoding="utf-8") + + for expected in ( + "abc123", + "gpt-6-astra · reasoning=low", + "eval-checksum", + "data-checksum", + "Chrome/151", + "Moli/1.1.3", + "official/reddit@sha256:site", + ): + assert expected in text From 254c73677bca762a294b35ece74ba47ddc4826c9 Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 8 Sep 2026 23:50:05 +0800 Subject: [PATCH 013/127] Reject malformed encoded HAR bodies --- harness/browser_agent/har.py | 1 + tests/browser_agent/test_har.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/harness/browser_agent/har.py b/harness/browser_agent/har.py index 2d6360b54..36d60adf5 100644 --- a/harness/browser_agent/har.py +++ b/harness/browser_agent/har.py @@ -123,6 +123,7 @@ def stop(self, context: Any, path: Path) -> None: self.errors.append( f"{request.get('url')}: response body unavailable: {exc}" ) + body = None entries.append( {"request": _har_request(request), "response": _har_response(response, body)} ) diff --git a/tests/browser_agent/test_har.py b/tests/browser_agent/test_har.py index d47f6f587..7b8168c61 100644 --- a/tests/browser_agent/test_har.py +++ b/tests/browser_agent/test_har.py @@ -106,3 +106,32 @@ def detach(self): content = json.loads(path.read_text())["log"]["entries"][0]["response"]["content"] assert content == {"mimeType": "application/json", "text": '{"items_qty":1}'} + + +def test_invalid_base64_is_not_written_as_plain_json(tmp_path) -> None: + class Session: + def send(self, _method, _params): + return {"body": "not valid base64!", "base64Encoded": True} + + def detach(self): + pass + + session = Session() + recorder = CdpHarRecorder() + recorder._sessions.append(session) + recorder._finished.add((id(session), "1")) + recorder._responses.append( + ( + session, + "1", + {"method": "GET", "url": "http://example.test/totals", "headers": {}}, + {"status": 200, "mimeType": "application/json", "headers": {}}, + ) + ) + path = tmp_path / "network.har" + + recorder.stop(object(), path) + + content = json.loads(path.read_text())["log"]["entries"][0]["response"]["content"] + assert content["text"] == "" + assert recorder.errors From b7a6204233181fc5640b12c6fafd0937463c5839 Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 8 Sep 2026 23:50:43 +0800 Subject: [PATCH 014/127] Expose pre-registered cohort hash --- harness/browser_agent/report.py | 1 + tests/browser_agent/test_report.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/harness/browser_agent/report.py b/harness/browser_agent/report.py index 6c7d33b77..6383a0e14 100644 --- a/harness/browser_agent/report.py +++ b/harness/browser_agent/report.py @@ -185,6 +185,7 @@ def _environment_section(evidence: Path) -> str: ["WebArena‑Verified", str(environment.get("webarena_verified_version") or "—")], ["官方 evaluator checksum", str(official.get("evaluator_checksum") or "—")], ["官方 data checksum", str(official.get("data_checksum") or "—")], + ["预注册选择集 SHA-256", str(run_config.get("selection_sha256") or "—")], ["Docker", f"{environment.get('docker_architecture', '—')} · {environment.get('docker_server_version', '—')} · context={run_config.get('docker_context', '—')}"], ] for engine, label in (("chromium", "Chromium"), ("moli", "Moli")): diff --git a/tests/browser_agent/test_report.py b/tests/browser_agent/test_report.py index fa0c61588..0e9a2473e 100644 --- a/tests/browser_agent/test_report.py +++ b/tests/browser_agent/test_report.py @@ -72,7 +72,8 @@ def test_report_renders_measured_runtime_identities(tmp_path: Path) -> None: output = tmp_path / "report" evidence.mkdir() (evidence / "run-config.json").write_text( - json.dumps({"docker_context": "isolated-eval"}), encoding="utf-8" + json.dumps({"docker_context": "isolated-eval", "selection_sha256": "selection-hash"}), + encoding="utf-8", ) (evidence / "manifest.json").write_text( json.dumps( @@ -122,6 +123,7 @@ def test_report_renders_measured_runtime_identities(tmp_path: Path) -> None: "gpt-6-astra · reasoning=low", "eval-checksum", "data-checksum", + "selection-hash", "Chrome/151", "Moli/1.1.3", "official/reddit@sha256:site", From a6a365685760e8f971cbc066c869e4025d3bfa88 Mon Sep 17 00:00:00 2001 From: kevin Date: Wed, 9 Sep 2026 12:14:40 +0800 Subject: [PATCH 015/127] Harden autonomous browser evaluation evidence --- Makefile | 6 +- README.md | 1 + examples/browser_agent/README.md | 15 +++ harness/browser_agent/model.py | 62 +++++++-- harness/browser_agent/report.py | 89 +++++++++++-- harness/browser_agent/review.py | 161 ++++++++++++++++++++++++ harness/browser_agent/runner.py | 144 +++++++++++++++++++-- scripts/check_browser_agent_report.py | 50 +++++++- scripts/review_browser_agent_results.py | 27 ++++ tests/browser_agent/test_model.py | 56 ++++++++- tests/browser_agent/test_report.py | 7 +- tests/browser_agent/test_review.py | 84 +++++++++++++ tests/browser_agent/test_runner.py | 87 +++++++++++++ 13 files changed, 747 insertions(+), 42 deletions(-) create mode 100644 harness/browser_agent/review.py create mode 100644 scripts/review_browser_agent_results.py create mode 100644 tests/browser_agent/test_review.py diff --git a/Makefile b/Makefile index e305103a8..c6cf6c59a 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ UV ?= uv PY := $(UV) run --frozen python -.PHONY: setup runtime-check test test-python test-ui fixture-smoke ci report report-check browser-agent-report browser-agent-report-check +.PHONY: setup runtime-check test test-python test-ui fixture-smoke ci report report-check browser-agent-review browser-agent-report browser-agent-report-check setup: $(UV) sync --frozen --all-extras @@ -40,6 +40,10 @@ browser-agent-report: runtime-check @test -n "$(EVIDENCE)" || (echo 'EVIDENCE=/absolute/path is required' >&2; exit 2) $(PY) scripts/build_browser_agent_report.py --evidence "$(EVIDENCE)" --output reports/browser-agent +browser-agent-review: runtime-check + @test -n "$(EVIDENCE)" || (echo 'EVIDENCE=/absolute/path is required' >&2; exit 2) + $(PY) scripts/review_browser_agent_results.py --evidence "$(EVIDENCE)" + browser-agent-report-check: runtime-check @test -n "$(EVIDENCE)" || (echo 'EVIDENCE=/absolute/path is required' >&2; exit 2) $(PY) scripts/check_browser_agent_report.py reports/browser-agent --evidence "$(EVIDENCE)" diff --git a/README.md b/README.md index a0931f22f..35d6b7251 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ one retry, reported separately from pass@1. ```bash ./examples/browser_agent/run.sh +make browser-agent-review EVIDENCE="$HOME/.browser-eval/browser-agent/current" make browser-agent-report EVIDENCE="$HOME/.browser-eval/browser-agent/current" make browser-agent-report-check EVIDENCE="$HOME/.browser-eval/browser-agent/current" ``` diff --git a/examples/browser_agent/README.md b/examples/browser_agent/README.md index 9db4fd0e3..4b9b2ca93 100644 --- a/examples/browser_agent/README.md +++ b/examples/browser_agent/README.md @@ -18,9 +18,24 @@ repository's `.env`, then run: ./examples/browser_agent/run.sh +To validate the pipeline with the currently available GPT-5.6 model before an +Astra-enabled project is available, keep the evidence cohort separate and run: + + BROWSER_AGENT_OUTPUT_DIR="${BROWSER_EVAL_DATA_HOME:-$HOME/.browser-eval}/browser-agent/gpt-5.6-sol" \ + ./examples/browser_agent/run.sh --model gpt-5.6-sol + +The selected model is immutable within one evidence directory. GPT-5.6 and +Astra attempts must never be resumed into or reported as the same cohort. + `COWAGENT_REPO` is a trusted code-execution boundary, not a data input. The runner refuses a dirty checkout and records its Git commit in the run config. Every task runs once per engine. Only a failed official evaluation receives one retry. The official benchmark score is calculated from primary attempts only; retry recovery is reported separately and never replaces the primary score. +After the run, create the deterministic per-attempt evidence review before +building or publishing the report: + + make browser-agent-review EVIDENCE="$BROWSER_AGENT_OUTPUT_DIR" + make browser-agent-report EVIDENCE="$BROWSER_AGENT_OUTPUT_DIR" + make browser-agent-report-check EVIDENCE="$BROWSER_AGENT_OUTPUT_DIR" diff --git a/harness/browser_agent/model.py b/harness/browser_agent/model.py index 93d524920..8fa71aa76 100644 --- a/harness/browser_agent/model.py +++ b/harness/browser_agent/model.py @@ -108,7 +108,7 @@ def response_to_chat_chunk(response: dict[str, Any]) -> dict[str, Any]: } -class AstraResponsesModel: +class OpenAIResponsesModel: """Minimal adapter: CowAgent loop and tools stay unchanged; only transport differs.""" def __init__( @@ -122,7 +122,7 @@ def __init__( session: requests.Session | None = None, ) -> None: if reasoning_effort != "low": - raise ValueError("pilot protocol fixes GPT-6 Astra reasoning effort to low") + raise ValueError("pilot protocol fixes OpenAI reasoning effort to low") self.model = model self.api_key = api_key self.base_url = base_url.rstrip("/") @@ -134,6 +134,8 @@ def __init__( self._seen_message_count = 0 self.usage: dict[str, int] = {} self.request_count = 0 + self.request_attempt_count = 0 + self.transport_errors: list[dict[str, str]] = [] def reset(self) -> None: """Start a fresh Responses conversation for the next benchmark attempt.""" @@ -142,6 +144,8 @@ def reset(self) -> None: self._seen_message_count = 0 self.usage = {} self.request_count = 0 + self.request_attempt_count = 0 + self.transport_errors = [] def _normalized_reasoning_effort(self) -> str: return self.reasoning_effort @@ -153,10 +157,30 @@ def verify_access(self) -> None: headers={"Authorization": f"Bearer {self.api_key}"}, timeout=30, ) - response.raise_for_status() - payload = response.json() - if payload.get("id") != self.model: - raise ValueError(f"model lookup returned an unexpected id: {payload.get('id')!r}") + if response.status_code < 400: + payload = response.json() + if payload.get("id") != self.model: + raise ValueError(f"model lookup returned an unexpected id: {payload.get('id')!r}") + return + if response.status_code not in {403, 404, 405}: + response.raise_for_status() + + # Some OpenAI-compatible gateways expose a team-filtered collection but + # disable the single-model route. The collection remains a read-only + # access preflight and avoids spending a benchmark model call. + collection = self.session.get( + f"{self.base_url}/models", + headers={"Authorization": f"Bearer {self.api_key}"}, + timeout=30, + ) + collection.raise_for_status() + ids = { + str(item.get("id")) + for item in collection.json().get("data") or [] + if isinstance(item, dict) and item.get("id") + } + if self.model not in ids: + raise PermissionError(f"configured API project does not expose model {self.model!r}") def call_stream(self, request: Any) -> Iterable[dict[str, Any]]: messages = list(request.messages or []) @@ -178,12 +202,22 @@ def call_stream(self, request: Any) -> Iterable[dict[str, Any]]: payload["previous_response_id"] = self._previous_response_id if getattr(request, "max_tokens", None): payload["max_output_tokens"] = int(request.max_tokens) - response = self.session.post( - f"{self.base_url}/responses", - headers={"Authorization": f"Bearer {self.api_key}", "Content-Type": "application/json"}, - json=payload, - timeout=self.timeout_seconds, - ) + self.request_attempt_count += 1 + try: + response = self.session.post( + f"{self.base_url}/responses", + headers={ + "Authorization": f"Bearer {self.api_key}", + "Content-Type": "application/json", + }, + json=payload, + timeout=self.timeout_seconds, + ) + except requests.RequestException as exc: + self.transport_errors.append( + {"type": type(exc).__name__, "message": str(exc)[:500]} + ) + raise response.raise_for_status() self.last_response = response.json() response_id = self.last_response.get("id") @@ -204,3 +238,7 @@ def call_stream(self, request: Any) -> Iterable[dict[str, Any]]: self._previous_response_id = response_id self._seen_message_count = len(messages) yield response_to_chat_chunk(self.last_response) + + +# Compatibility for callers created before the pilot allowed a non-Astra smoke model. +AstraResponsesModel = OpenAIResponsesModel diff --git a/harness/browser_agent/report.py b/harness/browser_agent/report.py index 6383a0e14..dc2f67703 100644 --- a/harness/browser_agent/report.py +++ b/harness/browser_agent/report.py @@ -3,6 +3,7 @@ from __future__ import annotations import argparse +import hashlib import html import json import math @@ -29,6 +30,10 @@ def _read_json(path: Path) -> dict[str, Any]: return json.loads(path.read_text(encoding="utf-8")) if path.is_file() else {} +def _sha256(path: Path) -> str | None: + return hashlib.sha256(path.read_bytes()).hexdigest() if path.is_file() else None + + def _primary_summary(rows: list[dict[str, Any]]) -> dict[str, dict[str, Any]]: result: dict[str, dict[str, Any]] = {} for engine in ("chromium", "moli"): @@ -180,6 +185,7 @@ def _environment_section(evidence: Path) -> str: images = environment.get("browser_images") or {} official = environment.get("official_evaluator") or {} rows = [ + ["browser-eval", str(protocol.get("browser_eval_commit") or "—")], ["CowAgent", str(protocol.get("cowagent_commit") or "—")], ["模型", f"{protocol.get('model', '—')} · reasoning={protocol.get('reasoning_effort', '—')}"], ["WebArena‑Verified", str(environment.get("webarena_verified_version") or "—")], @@ -205,14 +211,67 @@ def _environment_section(evidence: Path) -> str: if site_rows else '

    站点镜像身份尚未记录。

    ' ) - return f"""

    实测环境与证据身份

    {_table(['项目','记录值'], rows)}

    自托管站点镜像

    {sites}

    报告从运行 manifest 读取这些值,不按产品名推测版本。镜像 digest、浏览器 CDP 版本、代码 commit、数据 checksum 与选择集哈希共同限定本轮可复现边界。

    """ + return f"""

    本轮运行身份

    这些字段来自运行 manifest,用于限定复现边界,不参与评分。

    展开代码、镜像与校验身份{_table(['项目','记录值'], rows)}

    自托管站点镜像

    {sites}
    """ + + +def _review_page(evidence: Path, css: str) -> str: + review = _read_json(evidence / "result-review.json") + rows: list[list[Any]] = [] + if not review: + body = '

    复核尚未执行。正式发布要求逐试次证据审计覆盖全部实测记录,且没有未解决项。

    ' + else: + coverage = review.get("coverage") or {} + disagreements = review.get("disagreements") or [] + unresolved = review.get("unresolved") or [] + reviewer = review.get("reviewer") or {} + complete = ( + coverage.get("recorded_attempts") == coverage.get("reviewed_attempts") + and not unresolved + ) + state = "复核通过" if complete else "复核未通过" + klass = "good" if complete else "warn" + body = ( + f'

    {state}。' + f"已复核 {_e(coverage.get('reviewed_attempts', 0))}/{_e(coverage.get('recorded_attempts', 0))} 个试次;" + f"记分分歧 {_e(len(disagreements))},未解决 {_e(len(unresolved))}。

    " + f"

    复核者:{_e(reviewer.get('name', '—'))},{_e(reviewer.get('kind', '—'))}。" + "它检查证据文件是否完整、记录回答是否与落盘回答一致、主记录的分数和状态是否与官方 evaluator 输出一致、模型请求尝试是否完整入账,以及 evaluator/data checksum 是否统一。" + "它不重新判断任务成功与否,也不允许人工改分。

    " + ) + rows = [ + [ + item.get("engine", "—"), + item.get("task_id", "—"), + item.get("attempt", "—"), + "通过" if item.get("outcome") == "verified" else "未解决", + sum(not bool(value) for value in (item.get("checks") or {}).values()), + ] + for item in review.get("attempts") or [] + ] + coverage_table = ( + _table(["浏览器", "任务", "试次", "证据复核", "失败检查数"], rows) + if rows + else "

    尚无可列出的复核记录。

    " + ) + return f"""Browser Agent 结果复核

    Result review

    每个试次都要与官方评分证据对得上

    这是一项确定性证据审计,不是第二套 evaluator,也不是 LLM-as-a-judge。官方 WebArena-Verified 分数仍是唯一成绩来源。

    复核结论

    {body}

    判定协议:任何缺失文件、无法解析的 JSON、回答不一致、分数或状态不一致、官方版本身份缺失,以及 evaluator/data checksum 不一致,都会保留为未解决项并阻止发布。不存在用人工意见覆盖官方 evaluator 的裁决路径。

    逐试次覆盖

    {coverage_table}
    """ + + +def _reproduction_page(evidence: Path, css: str) -> str: + return f"""Browser Agent 复现说明

    Reproduction

    复现从仓库、预注册题集和固定运行配置开始

    代码、题集选择与运行入口以公开仓库为准;测量输出、登录 Cookie 和完整网络正文不进入 Git。

    唯一代码来源

    lexmount/browser-eval 保存评测代码、预注册的 30 题、依赖锁文件和报告生成器。官方 evaluator 由锁定的 webarena-verified 包提供,本项目不复制其判分逻辑。

    核心命令

    ./examples/browser_agent/run.sh
    +make browser-agent-review EVIDENCE="$BROWSER_AGENT_EVIDENCE"
    +make browser-agent-report EVIDENCE="$BROWSER_AGENT_EVIDENCE"
    +make browser-agent-report-check EVIDENCE="$BROWSER_AGENT_EVIDENCE"

    运行前需在仓库 .env 或显式进程环境配置目标模型可用的 API key、代理地址和 CowAgent checkout。Python 依赖只按 uv.lock 冻结安装。

    {_environment_section(evidence)}
    """ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: attempts = _read_attempts(evidence) + manifest = _read_json(evidence / "manifest.json") + protocol = manifest.get("protocol") or {} + run_model = str(protocol.get("model") or "gpt-5.6-sol") + run_date = str(manifest.get("created_at") or "2026-09-09")[:10] output.mkdir(parents=True, exist_ok=True) css = """ -:root{--ink:#17202a;--muted:#667085;--paper:#fbfaf7;--card:#fff;--line:#dedbd2;--accent:#155eef;--soft:#eef4ff;--good:#eaf8ef;--warn:#fff6df}*{box-sizing:border-box}html,body{overflow-x:hidden}body{margin:0;background:var(--paper);color:var(--ink);font:16px/1.72 ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}main{width:100%;max-width:1100px;margin:auto;padding:64px 28px 100px}header{border-bottom:1px solid var(--line);padding-bottom:42px;margin-bottom:42px}h1{font:700 clamp(38px,6vw,72px)/1.06 ui-serif,Georgia,serif;letter-spacing:-.035em;max-width:920px;margin:10px 0 22px}h2{font:700 30px/1.25 ui-serif,Georgia,serif;margin:0 0 16px}h1,h2,h3,p,li{overflow-wrap:anywhere}h3{margin:28px 0 8px}section{background:var(--card);border:1px solid var(--line);border-radius:18px;padding:30px;margin:22px 0;box-shadow:0 8px 30px rgba(31,41,55,.035)}p{max-width:88ch}.lede{font-size:20px;color:#344054}.eyebrow{color:var(--accent);font-weight:750;letter-spacing:.08em;text-transform:uppercase;font-size:13px}.meta,.fine{color:var(--muted);font-size:14px}.callout{border-left:4px solid var(--accent);padding:13px 16px;background:var(--soft);border-radius:6px}.callout.good{background:var(--good)}.callout.warn{background:var(--warn);border-color:#d68a00}.table-wrap{max-width:100%;overflow:auto;margin:20px 0}table{border-collapse:collapse;width:100%;font-size:14px}th,td{text-align:left;vertical-align:top;border-bottom:1px solid var(--line);padding:11px 12px}th{background:#f5f3ee;white-space:nowrap}a{color:var(--accent)}code{background:#f1f2f4;padding:2px 5px;border-radius:5px}.rank{display:grid;grid-template-columns:48px minmax(0,1fr);gap:14px;padding:18px 0;border-top:1px solid var(--line)}.rank>div{min-width:0}.rank b{font:700 30px/1 Georgia,serif;color:var(--accent)}nav{display:flex;gap:16px;flex-wrap:wrap;margin-top:24px}nav a{text-decoration:none;font-size:14px}@media(max-width:650px){main{padding:36px 14px}section{padding:21px}h1{font-size:34px}.lede{font-size:18px}h2{font-size:25px}}@media print{body{background:#fff}main{max-width:none;padding:0}section{break-inside:avoid;box-shadow:none}nav{display:none}a{color:inherit;text-decoration:none}} +:root{--ink:#17202a;--muted:#667085;--paper:#fbfaf7;--card:#fff;--line:#dedbd2;--accent:#155eef;--soft:#eef4ff;--good:#eaf8ef;--warn:#fff6df}*{box-sizing:border-box}html,body{overflow-x:hidden}body{margin:0;background:var(--paper);color:var(--ink);font:16px/1.72 ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}main{width:100%;max-width:1100px;margin:auto;padding:64px 28px 100px}header{border-bottom:1px solid var(--line);padding-bottom:42px;margin-bottom:42px}h1{font:700 clamp(38px,6vw,72px)/1.06 ui-serif,Georgia,serif;letter-spacing:-.035em;max-width:920px;margin:10px 0 22px}h2{font:700 30px/1.25 ui-serif,Georgia,serif;margin:0 0 16px}h1,h2,h3,p,li,summary{overflow-wrap:anywhere}h3{margin:28px 0 8px}section{background:var(--card);border:1px solid var(--line);border-radius:18px;padding:30px;margin:22px 0;box-shadow:0 8px 30px rgba(31,41,55,.035)}p{max-width:88ch}.lede{font-size:20px;color:#344054}.eyebrow{color:var(--accent);font-weight:750;letter-spacing:.08em;text-transform:uppercase;font-size:13px}.meta,.fine{color:var(--muted);font-size:14px}.callout{border-left:4px solid var(--accent);padding:13px 16px;background:var(--soft);border-radius:6px}.callout.good{background:var(--good)}.callout.warn{background:var(--warn);border-color:#d68a00}.table-wrap{max-width:100%;overflow:auto;margin:20px 0}table{border-collapse:collapse;width:100%;font-size:14px}th,td{text-align:left;vertical-align:top;border-bottom:1px solid var(--line);padding:11px 12px}th{background:#f5f3ee;white-space:nowrap}a{color:var(--accent)}code{background:#f1f2f4;padding:2px 5px;border-radius:5px}pre{overflow:auto;background:#171b24;color:#f7f7f5;border-radius:12px;padding:18px}pre code{background:transparent;padding:0}details{margin-top:18px}summary{cursor:pointer;font-weight:700}.rank{display:grid;grid-template-columns:48px minmax(0,1fr);gap:14px;padding:18px 0;border-top:1px solid var(--line)}.rank>div{min-width:0}.rank b{font:700 30px/1 Georgia,serif;color:var(--accent)}nav{display:flex;gap:16px;flex-wrap:wrap;margin-top:24px}nav a{text-decoration:none;font-size:14px}@media(max-width:650px){main{padding:36px 14px}section{padding:21px}h1{font-size:34px}.lede{font-size:18px}h2{font-size:25px}}@media print{body{background:#fff}main{max-width:none;padding:0}section{break-inside:avoid;box-shadow:none}nav{display:none}a{color:inherit;text-decoration:none}} """ methodology = _table( ["Benchmark", "评什么", "验收对象", "判分方式", "主要问题"], @@ -226,26 +285,42 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ["Odysseys", "200 个真实互联网长链、多站任务", "最终产物、轨迹与平均 6.1 个任务 rubric", "按细粒度 rubric 做模型评审;报告 partial、perfect 与每步效率", "实时网页漂移,运行与评审成本高;更适合第二阶段长链能力"], ], ) - html_text = f"""Browser Use 公开评测选型与 WebArena‑Verified 首轮对比

    Browser Agent Evaluation · 2026-09-08

    Browser Use 公开评测怎么选,以及为什么首轮用 WebArena‑Verified

    结论先说:原有 30 题应保留,但它测的是固定动作下浏览器内核能否正确、快速执行,不是 CowAgent 的自主规划能力。新增轨道用真实 CowAgent + GPT‑6 Astra(reasoning=low),在同一组 WebArena‑Verified 任务上只替换 Chromium/Moli,以官方程序化 evaluator 判最终结果。

    + html_text = f"""Browser Use 公开评测选型与 WebArena‑Verified 首轮对比

    Browser Agent Evaluation · {_e(run_date)}

    Browser Use 公开评测怎么选,以及为什么首轮用 WebArena‑Verified

    结论先说:原有 30 题应保留,但它测的是固定动作下浏览器内核能否正确、快速执行,不是 CowAgent 的自主规划能力。新增轨道用真实 CowAgent + {_e(run_model)}(reasoning=low),在同一组 WebArena‑Verified 任务上只替换 Chromium/Moli,以官方程序化 evaluator 判最终结果。GPT‑5.6 是流程验证模型,后续 Astra 结果必须作为新的独立运行发布,不能混入本轮分数。

    采用统计与决策

    公开采用次数 Top 3:Mind2Web、WebArena、WebVoyager

    统计单位是公开一手材料中采用过该家族的机构数,而不是论文引用量;同一机构重复发布只计一次。Mind2Web 家族为 5 家(OpenAI、Anthropic、Google DeepMind、THUDM / ChatGLM、Browser Use),WebArena 家族为 3 家(OpenAI、Anthropic、THUDM / ChatGLM),WebVoyager 为 2 家(OpenAI、Google DeepMind)。不同变体的成绩不能合并,这里的聚合只回答“有多少机构采用过这个家族”。

    1

    Mind2Web 家族 · 5 家

    采用最广,但家族内部差异最大:原版是离线动作预测,Online 版是真实网站 + WebJudge,Mind2Web 2 是长链研究任务 + Agent-as-a-Judge。适合作为第二阶段真实互联网泛化评测,不能把三者成绩合并。

    2

    WebArena 家族 · 3 家

    覆盖导航、检索和改写状态;自托管让 Chromium/Moli 面对同一份站点数据。首轮仍选它的 Verified 版本,因为它重新审计任务、答案和 evaluator,并使用确定性程序判分。这里是“实验适配性”胜过“采用数量”。

    3

    WebVoyager · 2 家

    经典视觉浏览器基准,优势是直观、接近用户网站;问题是实时网页和 LLM judge 同时引入波动,作为内核 A/B 首轮不如 WebArena‑Verified 稳定。

    为什么不是照着 Astra 发布页抄 benchmark:GPT‑6 Astra 的公开 computer-use 主表是 Agents’ Last Exam、OSWorld 2.0 和 ScreenSpot‑Pro;页面只给出 Mind2Web 的 1.9× 演示耗时对比,没有公开 WebArena‑Verified 分数。这些基准能说明模型的通用电脑操作能力,但 OSWorld/ScreenSpot 并不是纯 browser use,也不适合直接比较两个浏览器内核。

    Benchmark taxonomy

    它们评最终结果,还是评过程?

    {methodology}

    WebArena‑Verified 的精确判分链

    官方 1.2.3 数据共有 812 题,812 题都配置 AgentResponseEvaluator,其中 663 题还配置 NetworkEventEvaluator。前者解析 Agent 最终 JSON,核对任务类型、状态和带类型的返回字段;后者从 Playwright trace 或 HAR 中提取请求、响应、Cookie 等事件,核对导航或状态改变是否实际发生。字段先按官方 schema 做 URL、字符串、数字、布尔、数组和对象规范化,再做结构比较;需要有序时保留顺序,允许的备选答案由任务定义声明。没有模糊子串放行,也没有 LLM judge。

    单题是二元分:该题配置的每一个 evaluator 都得到 1.0 才通过,任何一项失败或 evaluator 报错都记 0。因此它评的是最终结果与必要的可观察副作用,不是“按参考路径逐步打分”。Agent 可以走不同点击路径,只要完成的结果和必要事件符合任务契约即可。

    Mind2Web 的 easy / medium / hard 要先纠正

    原始 Mind2Web 的官方拆分不是难度档,而是 cross-task、cross-website、cross-domain:测试任务是否见过同站任务、是否换了网站、是否换了整个领域。Online‑Mind2Web 官方也没有稳定的 easy/medium/hard 定义;有些第三方按参考动作长度再分档,那是第三方分析口径。Mind2Web 2 论文把任务 horizon 定义为 short(少于 10 次动作)、medium(10–50)、long(超过 50),仍不等同于 easy/medium/hard。

    所以本轮的 5/10/15 是我们自己的预注册操作长度层级。它只用于保证样本覆盖,不改写官方字段,也不把它当成跨论文可比较的难度标签。

    Browser Use 官方现在更偏向什么

    要区分三种信号。Browser Use 产品首页强调开放的 100 题 BU Bench V1,同时展示 Odysseys 的 200 题长链排行榜成绩;其 benchmark 仓库当前把 71 题 Stealth Bench V1 放在最前。Stealth Bench 测的是反机器人检测与浏览器基础设施,不是 CowAgent 能否自主完成业务任务,因此不能代替本报告的 agent 评测。

    BU Bench 由 Custom、WebBench、Mind2Web 2、GAIA、BrowseComp 各 20 题组成,另有独立的 300 题 Online‑Mind2Web 运行仓库。最值得借鉴的是:运行前确认任务可完成、保存逐题 trace、支持断点续跑、把“不可完成”与 Agent 真失败分开。Odysseys 还把长任务拆成平均 6.1 个可检查 rubric,并同时报告完整成功率、部分进度和每步效率。

    首轮不直接照搬的原因也很具体:这些工作流都包含模型评审,BU Bench 还混入搜索与问答任务,Odysseys 又是实时多站长链任务。它们适合第二阶段测真实互联网泛化和长链能力,但会把网页漂移、judge 波动和浏览器内核差异同时混进结果。

    -

    WebArena‑Verified 本地部署要不要账号付费

    四个首轮站点使用官方 Docker 镜像和内置测试账号,本地运行不需要向 GitLab、Reddit 或电商站点申请真实账号,也没有按题平台费用。成本来自本机 CPU、内存、磁盘和 GPT‑6 Astra API。

    Astra 通过 Responses API 调用,模型 ID 是 gpt-6-astra,本轮固定 reasoning.effort=low。OpenAI 官方模型页把 Free API tier 标为不支持,因此需要一个已开通 API 计费且有该模型权限的 OpenAI 账号和项目密钥;Codex 不能代替团队注册账号、开通计费或承担费用。

    “本地可部署”也不等于轻量。本轮核对的官方 GitLab 镜像 manifest 含一个约 22.0 GB 的压缩主层,官方文档建议至少 4 GB 内存;四站点与两浏览器因此放在独立的 100 GB Docker profile 中。Wikipedia 与 Map 还需要额外数据卷,所以首轮不纳入。自托管能消除网站内容漂移、封号和真实支付风险,但不会自动消除镜像下载、环境初始化、登录兼容、浏览器 CDP 实现差异或 evaluator 缺陷。

    -

    本轮怎样保证是可比较的自主 Agent 实验

    {_table(['项目','预注册规则'], [['样本','固定 30 题:简单 5、中等 10、困难 15;运行后不换题,不重复抽样'],['Agent 与模型','CowAgent 真实自主观察、规划和调用 BrowserTool;GPT‑6 Astra,reasoning=low;每个试次清空模型会话'],['工具接口','沿用 CowAgent 的页面快照与 browser 函数工具,不调用 Astra 原生 computer tool;测的是既有 CowAgent 接入方式下的自主 browser use'],['唯一变量','Agent、提示、模型、任务、站点数据和资源限额相同,只替换 Chromium / Moli'],['环境','每个试次都从官方镜像重置该题站点并等待应用连续稳定;Shopping、Shopping Admin、Reddit 使用官方头登录,GitLab 使用官方 UI 登录'],['顺序','逐题交替先跑 Chromium 或 Moli,减轻固定先后顺序偏差'],['主分','每个“题 × 浏览器”只跑一次,官方二元 pass@1;两边构成逐题配对'],['重试','只对首轮失败项再试一次,最多一次;重试仅报告恢复率,不回填或抬高主分'],['留证','保存最终 JSON、CowAgent 事件、标准网络 HAR、可视 Playwright trace、官方 evaluator 明细、镜像 ID、代码 commit、token 与资源指标'],['排除','首轮不选 Wikipedia / Map;不选无法由官方确定性 evaluator 验收的外部实时站点任务']])}

    “操作步数层级”只决定覆盖结构,不参与得分。环境准备失败应标成基础设施错误并修复后重跑同一预注册试次,不能计作 Agent 失败,也不能借机换题。官方 evaluator 读取 HAR;可视 trace 只用于调试,不参与评分。由于工具接口不同,本轮结果不能直接代表 Astra 发布页中原生 computer-use harness 的成绩。

    -{_result_section(attempts)}{_tier_section(attempts)}{_task_table(selection_path, attempts)}{_environment_section(evidence)} +

    WebArena‑Verified 本地部署要不要账号付费

    四个首轮站点使用官方 Docker 镜像和内置测试账号,本地运行不需要向 GitLab、Reddit 或电商站点申请真实账号,也没有按题平台费用。成本来自本机 CPU、内存、磁盘和模型 API。

    本轮模型是 {_e(run_model)},通过 Responses API 调用,并固定 reasoning.effort=low。当前 LiteLLM Team 没有 gpt-6-astra 权限,因此 GPT‑5.6 运行只能证明流程和 Chromium/Moli 在该模型条件下的差异,不能替代 Astra 成绩。

    “本地可部署”也不等于轻量。本轮核对的官方 GitLab 镜像 manifest 含一个约 22.0 GB 的压缩主层,官方文档建议至少 4 GB 内存;四站点与两浏览器因此放在独立的 100 GB Docker profile 中。Wikipedia 与 Map 还需要额外数据卷,所以首轮不纳入。自托管能消除网站内容漂移、封号和真实支付风险,但不会自动消除镜像下载、环境初始化、登录兼容、浏览器 CDP 实现差异或 evaluator 缺陷。

    +

    本轮怎样保证是可比较的自主 Agent 实验

    {_table(['项目','预注册规则'], [['样本','固定 30 题:简单 5、中等 10、困难 15;运行后不换题,不重复抽样'],['Agent 与模型',f'CowAgent 真实自主观察、规划和调用 BrowserTool;{run_model},reasoning=low;每个试次清空模型会话'],['工具接口','沿用 CowAgent 的页面快照与 browser 函数工具,不调用模型原生 computer tool;测的是既有 CowAgent 接入方式下的自主 browser use'],['唯一变量','Agent、提示、模型、任务、站点数据和资源限额相同,只替换 Chromium / Moli'],['环境','每个试次都从官方镜像重置该题站点并等待应用连续稳定;Shopping、Shopping Admin、Reddit 使用官方头登录,GitLab 使用官方 UI 登录'],['顺序','逐题交替先跑 Chromium 或 Moli,减轻固定先后顺序偏差'],['主分','每个“题 × 浏览器”只跑一次,官方二元 pass@1;两边构成逐题配对'],['重试','任务首轮失败后最多重跑一次,恢复只作诊断;每次模型请求遇到传输错误也最多重发一次,所有请求尝试与错误都入证'],['留证','保存最终 JSON、CowAgent 事件、标准网络 HAR、可视 Playwright trace、官方 evaluator 明细、镜像 ID、代码 commit、token 与资源指标'],['排除','首轮不选 Wikipedia / Map;不选无法由官方确定性 evaluator 验收的外部实时站点任务']])}

    “操作步数层级”只决定覆盖结构,不参与得分。环境准备失败应标成基础设施错误并修复后重跑同一预注册试次,不能计作 Agent 失败,也不能借机换题。官方 evaluator 读取 HAR;可视 trace 只用于调试,不参与评分。由于工具接口不同,本轮结果不能直接代表 Astra 发布页中原生 computer-use harness 的成绩。

    +{_result_section(attempts)}{_tier_section(attempts)}{_task_table(selection_path, attempts)}

    两条轨道必须分开读

    {_table(['轨道','Agent 是否自主','控制变量','主指标','回答的问题'], [['浏览器内核微基准(原 30 题)','否;重放固定 BrowserTool 等价调用','同一动作序列,只替换浏览器','严格输出正确率、动作执行、资源与耗时','相同操作交给两个内核,谁更兼容/省资源?'],['WebArena‑Verified Agent 评测(本报告)','是;CowAgent 自主观察、规划和调用 BrowserTool','同一 Agent、模型、提示、站点快照与任务,只替换浏览器','官方首轮 pass@1;失败重试恢复率另列','浏览器差异会怎样影响完整自主任务成功率?']])}

    固定动作轨道不是“有问题的假 Agent”,它是有价值的内核微基准;问题只是旧命名把它说成了自主 browser use。两条轨道互补,但分数不能相加,也不能用一条替代另一条。

    公开证据与复现边界

    采用统计只计公开一手材料。未找到 DeepSeek 或 Kimi 的官方浏览器交互 benchmark,不代表其内部没有评测;Kimi 公布的 BrowseComp 属于 agentic search,本报告不把它算成 UI browser use。

    """ report = output / "index.html" report.write_text(html_text, encoding="utf-8") + (output / "review.html").write_text(_review_page(evidence, css), encoding="utf-8") + (output / "reproduction.html").write_text( + _reproduction_page(evidence, css), encoding="utf-8" + ) + review = _read_json(evidence / "result-review.json") audit = { "schema": "lex_browser_agent_report/1", - "evidence": str(evidence), "attempt_count": len(attempts), "summary": _primary_summary(attempts), "paired_summary": _paired_summary(attempts), "selection_sha256": load_pilot(selection_path).sha256, + "evidence_bindings": { + "attempts_sha256": _sha256(evidence / "attempts.jsonl"), + "manifest_sha256": _sha256(evidence / "manifest.json"), + }, + "review": { + "coverage": review.get("coverage"), + "disagreement_count": len(review.get("disagreements") or []), + "unresolved_count": len(review.get("unresolved") or []), + "bindings": review.get("bindings"), + } + if review + else None, } (output / "report-data.json").write_text( json.dumps(audit, ensure_ascii=False, indent=2), encoding="utf-8" diff --git a/harness/browser_agent/review.py b/harness/browser_agent/review.py new file mode 100644 index 000000000..b8137af84 --- /dev/null +++ b/harness/browser_agent/review.py @@ -0,0 +1,161 @@ +"""Deterministic evidence review for autonomous browser-agent attempts. + +This review verifies evidence integrity. It does not replace or reinterpret the +official WebArena-Verified evaluator score. +""" + +from __future__ import annotations + +import hashlib +import json +from datetime import datetime, timezone +from pathlib import Path +from typing import Any + + +REQUIRED_ARTIFACTS = ("network_har", "agent_trace", "response", "evaluation", "events") + + +def _sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for block in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(block) + return digest.hexdigest() + + +def _attempts_sha256(path: Path) -> str: + return _sha256(path) + + +def _read_attempts(path: Path) -> list[dict[str, Any]]: + return [ + json.loads(line) + for line in path.read_text(encoding="utf-8").splitlines() + if line.strip() + ] + + +def review_results(*, evidence: Path, selection_sha256: str) -> dict[str, Any]: + """Review every recorded attempt and return a result bound to its input bytes.""" + attempts_path = evidence / "attempts.jsonl" + manifest_path = evidence / "manifest.json" + if not attempts_path.is_file() or not manifest_path.is_file(): + raise ValueError("result review requires attempts.jsonl and manifest.json") + + manifest = json.loads(manifest_path.read_text(encoding="utf-8")) + if manifest.get("selection_sha256") != selection_sha256: + raise ValueError("manifest does not match the pre-registered selection") + + rows = _read_attempts(attempts_path) + reviewed: list[dict[str, Any]] = [] + disagreements: list[dict[str, Any]] = [] + unresolved: list[dict[str, Any]] = [] + expected_provenance = manifest.get("environment", {}).get("official_evaluator", {}) + + for row in rows: + identity = { + "engine": str(row.get("engine")), + "task_id": int(row.get("task_id")), + "attempt": int(row.get("attempt")), + } + paths = row.get("paths") or {} + checks: dict[str, bool] = {} + artifacts: dict[str, dict[str, Any]] = {} + for name in REQUIRED_ARTIFACTS: + raw_path = paths.get(name) + path = Path(str(raw_path)) if raw_path else Path() + exists = bool(raw_path) and path.is_file() + checks[f"{name}_exists"] = exists + if exists: + artifacts[name] = {"sha256": _sha256(path), "bytes": path.stat().st_size} + + evaluation: dict[str, Any] = {} + response: Any = None + if checks["evaluation_exists"]: + try: + evaluation = json.loads(Path(str(paths["evaluation"])).read_text(encoding="utf-8")) + checks["evaluation_json"] = isinstance(evaluation, dict) + except (OSError, json.JSONDecodeError): + checks["evaluation_json"] = False + else: + checks["evaluation_json"] = False + if checks["response_exists"]: + try: + response = json.loads(Path(str(paths["response"])).read_text(encoding="utf-8")) + checks["response_json"] = True + except (OSError, json.JSONDecodeError): + checks["response_json"] = False + else: + checks["response_json"] = False + + checks["response_matches_record"] = checks["response_json"] and response == row.get( + "agent_response" + ) + checks["score_matches_evaluator"] = checks["evaluation_json"] and float( + evaluation.get("score") or 0.0 + ) == float(row.get("score") or 0.0) + checks["status_matches_evaluator"] = checks["evaluation_json"] and str( + evaluation.get("status") or "error" + ) == str(row.get("status") or "error") + checks["official_evaluator_version_present"] = bool( + evaluation.get("webarena_verified_version") + ) + checks["official_evaluator_checksum_matches"] = bool( + expected_provenance.get("evaluator_checksum") + ) and evaluation.get("webarena_verified_evaluator_checksum") == expected_provenance.get( + "evaluator_checksum" + ) + checks["official_data_checksum_matches"] = bool( + expected_provenance.get("data_checksum") + ) and evaluation.get("webarena_verified_data_checksum") == expected_provenance.get( + "data_checksum" + ) + usage = row.get("model_usage") or {} + transport_errors = row.get("model_transport_errors") or [] + checks["model_request_attempts_accounted"] = ( + isinstance(transport_errors, list) + and int(usage.get("request_attempt_count") or 0) + == int(usage.get("request_count") or 0) + len(transport_errors) + ) + + failed = sorted(name for name, passed in checks.items() if not passed) + item = {**identity, "outcome": "verified" if not failed else "unresolved", "checks": checks} + item["artifacts"] = artifacts + reviewed.append(item) + if not checks["score_matches_evaluator"] or not checks["status_matches_evaluator"]: + disagreements.append({**identity, "failed_checks": failed}) + if failed: + unresolved.append({**identity, "failed_checks": failed}) + + return { + "schema": "lex_browser_agent_result_review/1", + "reviewer": { + "name": "Browser-agent evidence auditor", + "kind": "deterministic programmatic review", + "version": 1, + }, + "reviewed_at": datetime.now(timezone.utc).isoformat(), + "bindings": { + "attempts_sha256": _attempts_sha256(attempts_path), + "manifest_sha256": _sha256(manifest_path), + "selection_sha256": selection_sha256, + }, + "protocol": { + "scope": "evidence completeness, byte identity, and recorded score/status agreement", + "score_authority": "unmodified WebArena-Verified evaluator", + "manual_score_override": "not permitted", + "adjudication": "any disagreement or missing artifact remains unresolved and blocks publication", + }, + "coverage": {"recorded_attempts": len(rows), "reviewed_attempts": len(reviewed)}, + "disagreements": disagreements, + "unresolved": unresolved, + "attempts": reviewed, + } + + +def write_review(*, evidence: Path, selection_sha256: str) -> Path: + review = review_results(evidence=evidence, selection_sha256=selection_sha256) + path = evidence / "result-review.json" + path.write_text(json.dumps(review, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + return path diff --git a/harness/browser_agent/runner.py b/harness/browser_agent/runner.py index 236ed1382..845b62b13 100644 --- a/harness/browser_agent/runner.py +++ b/harness/browser_agent/runner.py @@ -4,7 +4,9 @@ import argparse import importlib.metadata +import ipaddress import json +import os import platform import subprocess import sys @@ -13,6 +15,7 @@ import uuid from pathlib import Path from typing import Any +from urllib.parse import urlparse import docker import requests @@ -38,7 +41,7 @@ ) from .har import CdpHarRecorder -from .model import AstraResponsesModel +from .model import OpenAIResponsesModel from .selection import PilotTask, load_pilot, validate_against_official SCHEMA = "lex_browser_agent_attempt/1" @@ -63,11 +66,17 @@ def _load_repo_env(repo_root: Path) -> dict[str, str]: - """Credentials are intentionally read only from this repository's .env.""" + """Load repository defaults, with explicit process overrides for automation.""" env_file = repo_root / ".env" - if not env_file.exists(): - return {} - return {str(key): str(value) for key, value in dotenv_values(env_file).items() if value} + values = ( + {str(key): str(value) for key, value in dotenv_values(env_file).items() if value} + if env_file.exists() + else {} + ) + for key in ("OPENAI_API_KEY", "OPENAI_BASE_URL", "COWAGENT_REPO"): + if os.environ.get(key): + values[key] = os.environ[key] + return values def _git_head(path: Path) -> str: @@ -117,13 +126,69 @@ def _docker_image_metadata(image: Any) -> dict[str, Any]: } +def _container_host_gateway(container: Any) -> str: + """Return the literal host-gateway address visible to the browser process. + + CowAgent validates URLs in the host process before forwarding them over CDP. + On Linux/Colima, ``host.docker.internal`` can exist only in the container's + /etc/hosts, so a literal RFC1918 address is required on both sides. + """ + result = container.exec_run(["getent", "hosts", "host.docker.internal"]) + if int(result.exit_code) != 0: + raise RuntimeError("browser container cannot resolve host.docker.internal") + line = result.output.decode("utf-8", errors="replace").splitlines()[0] + address = line.split()[0] + ipaddress.ip_address(address) + return address + + +def _limit_cowagent_transport_retries(executor_class: Any) -> None: + """Limit CowAgent API errors to the initial request plus one retry.""" + if getattr(executor_class, "_lex_browser_eval_retry_limit", False): + return + original = executor_class._call_llm_stream + + def limited( + executor: Any, + retry_on_empty: bool = True, + retry_count: int = 0, + max_retries: int = 1, + _overflow_stage: int = 0, + ) -> Any: + return original( + executor, + retry_on_empty=retry_on_empty, + retry_count=retry_count, + max_retries=min(int(max_retries), 1), + _overflow_stage=_overflow_stage, + ) + + executor_class._call_llm_stream = limited + executor_class._lex_browser_eval_retry_limit = True + + +def _allow_official_container_hostname(tool: Any) -> None: + """Allow only the hostname emitted by the official self-hosted images.""" + original = tool._validate_url_safe + + def validate(url: str) -> None: + if urlparse(url).hostname == "host.docker.internal": + return + original(url) + + tool._validate_url_safe = validate + + def _configure_benchmark(hostname: str = "host.docker.internal") -> WebArenaVerified: + def site_url(site: WebArenaSite, host: str) -> str: + return f"http://{host}:{SITE_PORTS[site][0]}{SITE_URL_SUFFIX.get(site, '')}" + environments = { site: EnvironmentConfig( - urls=[ - f"http://{hostname}:{SITE_PORTS[site][0]}" - f"{SITE_URL_SUFFIX.get(site, '')}" - ], + # The gateway IP is active. The official images may redirect to + # host.docker.internal, so retain that as an equivalent URL for the + # package's own derendering and NetworkEventEvaluator logic. + urls=list(dict.fromkeys([site_url(site, hostname), site_url(site, "host.docker.internal")])), credentials=SITE_CREDENTIALS[site], use_header_login=site == WebArenaSite.SHOPPING_ADMIN, ) @@ -132,6 +197,24 @@ def _configure_benchmark(hostname: str = "host.docker.internal") -> WebArenaVeri return WebArenaVerified(config=WebArenaVerifiedConfig(environments=environments)) +def _wait_container_healthy(container: Any, *, timeout_seconds: float = 600) -> None: + """Wait past GitLab's delayed reconfigure/Puma restart before app probing.""" + deadline = time.monotonic() + timeout_seconds + last_status = "missing" + while time.monotonic() < deadline: + container.reload() + health = (container.attrs.get("State") or {}).get("Health") or {} + last_status = str(health.get("Status") or "missing") + if last_status == "healthy": + return + if last_status == "unhealthy": + logs = health.get("Log") or [] + detail = str(logs[-1].get("Output") or "")[-500:] if logs else "" + raise RuntimeError(f"GitLab container became unhealthy: {detail}") + time.sleep(2) + raise RuntimeError(f"GitLab container health timed out in state {last_status!r}") + + def _ensure_site(site: WebArenaSite, *, reset: bool) -> None: port, ctrl_port = SITE_PORTS[site] manager = ContainerManager(site=site, hostname="host.docker.internal") @@ -142,6 +225,9 @@ def _ensure_site(site: WebArenaSite, *, reset: bool) -> None: ) if not ready.success: raise RuntimeError(f"{site.value} did not become ready: {ready.message}") + if site == WebArenaSite.GITLAB: + container = docker.from_env().containers.get(manager.container_name) + _wait_container_healthy(container) _wait_application_ready(site, manager.config.health_check_path) @@ -166,7 +252,13 @@ def _wait_application_ready( last_detail = "no response" while time.monotonic() < deadline: try: - response = requests.get(url, timeout=15, allow_redirects=True) + # The official shopping images redirect to their configured + # host.docker.internal origin. That name is intentionally scoped to + # browser containers and may not resolve on the macOS host. A stable + # pre-redirect response proves the app port is serving; _login_context + # then validates the rendered authenticated page from inside the + # actual browser before tracing or model calls begin. + response = requests.get(url, timeout=15, allow_redirects=False) last_detail = f"HTTP {response.status_code}" consecutive = consecutive + 1 if response.status_code < 500 else 0 if consecutive >= stable_responses: @@ -315,7 +407,7 @@ def _run_attempt( # noqa: PLR0913 container: Any, cowagent_repo: Path, output_dir: Path, - model: AstraResponsesModel, + model: OpenAIResponsesModel, sample_interval_ms: int, ) -> dict[str, Any]: task = benchmark.get_task(selected.task_id) @@ -326,8 +418,11 @@ def _run_attempt( # noqa: PLR0913 if str(cowagent_repo) not in sys.path: sys.path.insert(0, str(cowagent_repo)) from agent.protocol.agent import Agent # noqa: PLC0415 + from agent.protocol.agent_stream import AgentStreamExecutor # noqa: PLC0415 from agent.tools.browser.browser_tool import BrowserTool # noqa: PLC0415 + _limit_cowagent_transport_retries(AgentStreamExecutor) + attempt_dir = output_dir / engine / str(selected.task_id) / f"attempt-{attempt}" attempt_dir.mkdir(parents=True, exist_ok=True) trace_path = attempt_dir / "agent_trace.zip" @@ -343,6 +438,7 @@ def _run_attempt( # noqa: PLR0913 "startup_timeout": 90, } ) + _allow_official_container_hostname(tool) events: list[dict[str, Any]] = [] resources = DockerStatsResources(container) before = resources.counters() @@ -461,7 +557,12 @@ def _run_attempt( # noqa: PLR0913 "cleanup_errors": cleanup_errors, "har_capture_errors": recorder.errors, "resource": resource, - "model_usage": {**model.usage, "request_count": model.request_count}, + "model_usage": { + **model.usage, + "request_count": model.request_count, + "request_attempt_count": model.request_attempt_count, + }, + "model_transport_errors": model.transport_errors, "paths": { "network_har": str(har_path), "agent_trace": str(trace_path), @@ -515,6 +616,7 @@ def run(args: argparse.Namespace) -> Path: # noqa: PLR0915 cowagent_repo = Path(args.cowagent_repo or env.get("COWAGENT_REPO", "")).resolve() if not (cowagent_repo / "agent/protocol/agent.py").exists(): raise RuntimeError("--cowagent-repo must point to a CowAgent checkout") + harness_commit = _require_clean_git_checkout(repo_root) cowagent_commit = _require_clean_git_checkout(cowagent_repo) output_dir = Path(args.output).expanduser().resolve() @@ -522,7 +624,7 @@ def run(args: argparse.Namespace) -> Path: # noqa: PLR0915 selection = load_pilot(args.selection) benchmark = _configure_benchmark() resolved = validate_against_official(selection, benchmark) - model = AstraResponsesModel( + model = OpenAIResponsesModel( api_key=api_key, base_url=env.get("OPENAI_BASE_URL", "https://api.openai.com/v1"), model=args.model, @@ -553,6 +655,7 @@ def run(args: argparse.Namespace) -> Path: # noqa: PLR0915 for site in SITE_PORTS } browser_metadata: dict[str, dict[str, str]] = {} + site_access_host = "" docker_info: dict[str, Any] = {} try: for engine in ENGINE_IMAGES: @@ -575,17 +678,27 @@ def run(args: argparse.Namespace) -> Path: # noqa: PLR0915 endpoints[engine] = _wait_cdp(http_endpoint, container=container) browser_metadata[engine] = _cdp_metadata(http_endpoint) + gateway_addresses = {_container_host_gateway(container) for container in containers.values()} + if len(gateway_addresses) != 1: + raise RuntimeError(f"browser containers disagree on host gateway: {gateway_addresses}") + site_access_host = gateway_addresses.pop() + benchmark = _configure_benchmark(hostname=site_access_host) + resolved = validate_against_official(selection, benchmark) + _write_or_validate_run_config( run_config_file, { "schema": "lex_browser_agent_run_config/1", "selection_sha256": selection.sha256, + "browser_eval_commit": harness_commit, "cowagent_commit": cowagent_commit, "model": args.model, "reasoning_effort": "low", + "model_transport_retries": 1, "browser_images": image_metadata, "site_images": site_image_metadata, "browser_versions": browser_metadata, + "site_access_host": site_access_host, "docker_context": subprocess.run( ["docker", "context", "show"], check=True, @@ -680,17 +793,21 @@ def run(args: argparse.Namespace) -> Path: # noqa: PLR0915 "selection": resolved, "protocol": { "agent": "CowAgent", + "browser_eval_commit": harness_commit, "cowagent_commit": cowagent_commit, "model": args.model, "reasoning_effort": "low", "primary_attempts_per_engine_task": 1, "failure_retries": 1, + "task_failure_retries": 1, + "model_transport_retries": 1, "official_score_uses": "primary attempt only", "retry_score_use": "reported separately as recovery diagnostic", "evaluator": "official webarena-verified package, unmodified", "environment_reset": "official site container reset before every attempt", "engine_order": "alternating by task", "sites": [site.value for site in SITE_PORTS], + "official_container_hostname_allowlist": ["host.docker.internal"], }, "environment": { "platform": platform.platform(), @@ -701,6 +818,7 @@ def run(args: argparse.Namespace) -> Path: # noqa: PLR0915 "browser_images": image_metadata, "browser_versions": browser_metadata, "site_images": site_image_metadata, + "site_access_host": site_access_host, "official_evaluator": evaluator_provenance, }, } diff --git a/scripts/check_browser_agent_report.py b/scripts/check_browser_agent_report.py index dd287df51..a654018e8 100644 --- a/scripts/check_browser_agent_report.py +++ b/scripts/check_browser_agent_report.py @@ -4,12 +4,17 @@ from __future__ import annotations import argparse +import hashlib import json from pathlib import Path from harness.browser_agent.selection import load_pilot +def _sha256(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + def check( report_dir: Path, evidence_dir: Path, @@ -18,12 +23,16 @@ def check( require_complete: bool, ) -> None: index = report_dir / "index.html" + review_page = report_dir / "review.html" + reproduction_page = report_dir / "reproduction.html" audit_path = report_dir / "report-data.json" attempts_path = evidence_dir / "attempts.jsonl" manifest_path = evidence_dir / "manifest.json" run_config_path = evidence_dir / "run-config.json" - if not index.is_file() or not audit_path.is_file(): - raise ValueError("report must contain index.html and report-data.json") + if not all(path.is_file() for path in (index, review_page, reproduction_page, audit_path)): + raise ValueError( + "report must contain index.html, review.html, reproduction.html, and report-data.json" + ) selection = load_pilot(selection_path) audit = json.loads(audit_path.read_text(encoding="utf-8")) if audit.get("selection_sha256") != selection.sha256: @@ -72,6 +81,14 @@ def check( raise ValueError("run configuration differs from the registered cohort") if run_config.get("reasoning_effort") != "low": raise ValueError("pilot reasoning effort must be low") + if not run_config.get("browser_eval_commit"): + raise ValueError("run configuration must identify the browser-eval commit") + if manifest.get("protocol", {}).get("browser_eval_commit") != run_config.get( + "browser_eval_commit" + ): + raise ValueError("manifest and run configuration disagree on browser-eval commit") + if manifest.get("protocol", {}).get("model_transport_retries") != 1: + raise ValueError("pilot allows exactly one model transport retry") missing = expected_primary - set(by_key) if missing: raise ValueError(f"missing {len(missing)} primary attempts") @@ -85,6 +102,35 @@ def check( raise ValueError(f"missing {len(missing_retries)} permitted failure retries") if "测量尚未完整" in html: raise ValueError("complete publication cannot contain an incomplete-results notice") + review_path = evidence_dir / "result-review.json" + if not review_path.is_file(): + raise ValueError("complete publication requires result-review.json") + review = json.loads(review_path.read_text(encoding="utf-8")) + bindings = review.get("bindings") or {} + if bindings.get("attempts_sha256") != _sha256(attempts_path): + raise ValueError("result review is not bound to the current attempts") + if bindings.get("manifest_sha256") != _sha256(manifest_path): + raise ValueError("result review is not bound to the current manifest") + if bindings.get("selection_sha256") != selection.sha256: + raise ValueError("result review is not bound to the registered selection") + coverage = review.get("coverage") or {} + if coverage != {"recorded_attempts": len(rows), "reviewed_attempts": len(rows)}: + raise ValueError("result review does not cover every recorded attempt") + if review.get("disagreements") or review.get("unresolved"): + raise ValueError("result review contains disagreements or unresolved checks") + reviewed_keys = { + (str(item["engine"]), int(item["task_id"]), int(item["attempt"])) + for item in review.get("attempts") or [] + } + if reviewed_keys != set(by_key): + raise ValueError("result review attempt inventory differs from measured evidence") + report_review = audit.get("review") or {} + if report_review.get("bindings") != bindings: + raise ValueError("rendered report is not bound to the current result review") + for page in (index, review_page, reproduction_page): + text = page.read_text(encoding="utf-8") + if str(evidence_dir.resolve()) in text: + raise ValueError("published pages must not contain machine-local evidence paths") def main() -> None: diff --git a/scripts/review_browser_agent_results.py b/scripts/review_browser_agent_results.py new file mode 100644 index 000000000..dc30e0c05 --- /dev/null +++ b/scripts/review_browser_agent_results.py @@ -0,0 +1,27 @@ +#!/usr/bin/env python3 +"""Create the deterministic per-attempt result review.""" + +from __future__ import annotations + +import argparse +from pathlib import Path + +from harness.browser_agent.review import write_review +from harness.browser_agent.selection import load_pilot + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--evidence", required=True, type=Path) + parser.add_argument( + "--selection", + type=Path, + default=Path("evaluation/browser-agent/webarena-verified-pilot.json"), + ) + args = parser.parse_args() + selection = load_pilot(args.selection) + print(write_review(evidence=args.evidence, selection_sha256=selection.sha256)) + + +if __name__ == "__main__": + main() diff --git a/tests/browser_agent/test_model.py b/tests/browser_agent/test_model.py index 8b48e5898..88f8af30f 100644 --- a/tests/browser_agent/test_model.py +++ b/tests/browser_agent/test_model.py @@ -1,7 +1,7 @@ import pytest from harness.browser_agent.model import ( - AstraResponsesModel, + OpenAIResponsesModel, cow_messages_to_responses, cow_tools_to_responses, response_to_chat_chunk, @@ -18,11 +18,13 @@ def __init__(self, messages): class _Response: - def __init__(self, payload): + def __init__(self, payload, status_code=200): self._payload = payload + self.status_code = status_code def raise_for_status(self): - return None + if self.status_code >= 400: + raise RuntimeError(f"HTTP {self.status_code}") def json(self): return self._payload @@ -110,7 +112,7 @@ def test_translates_tool_schema_and_response() -> None: def test_model_continues_responses_conversation_without_resending_assistant_call() -> None: session = _Session() - model = AstraResponsesModel(api_key="test", session=session) + model = OpenAIResponsesModel(api_key="test", session=session) list(model.call_stream(_Request([{"role": "user", "content": "open it"}]))) list( model.call_stream( @@ -153,13 +155,55 @@ class MalformedSession: def post(self, _url, **_kwargs): return _Response({"output": []}) - model = AstraResponsesModel(api_key="test", session=MalformedSession()) + model = OpenAIResponsesModel(api_key="test", session=MalformedSession()) with pytest.raises(ValueError, match="non-empty id"): list(model.call_stream(_Request([{"role": "user", "content": "open it"}]))) +def test_model_records_failed_transport_attempts() -> None: + class TimeoutSession: + def post(self, _url, **_kwargs): + raise __import__("requests").Timeout("timed out") + + model = OpenAIResponsesModel(api_key="test", session=TimeoutSession()) + + with pytest.raises(__import__("requests").Timeout): + list(model.call_stream(_Request([{"role": "user", "content": "open it"}]))) + + assert model.request_count == 0 + assert model.request_attempt_count == 1 + assert model.transport_errors == [{"type": "Timeout", "message": "timed out"}] + + def test_model_access_preflight_checks_exact_model() -> None: - model = AstraResponsesModel(api_key="test", session=_Session()) + model = OpenAIResponsesModel(api_key="test", session=_Session()) model.verify_access() + + +def test_model_access_preflight_supports_filtered_collection_gateway() -> None: + class GatewaySession: + def get(self, url, **_kwargs): + if url.endswith("/models/gpt-5.6-sol"): + return _Response({"error": "route disabled"}, status_code=403) + return _Response({"data": [{"id": "gpt-5.6-sol"}]}) + + model = OpenAIResponsesModel( + api_key="test", model="gpt-5.6-sol", session=GatewaySession() + ) + + model.verify_access() + + +def test_model_access_preflight_rejects_missing_gateway_model() -> None: + class GatewaySession: + def get(self, url, **_kwargs): + if url.endswith("/models/gpt-6-astra"): + return _Response({"error": "not allowed"}, status_code=403) + return _Response({"data": [{"id": "gpt-5.6-sol"}]}) + + model = OpenAIResponsesModel(api_key="test", session=GatewaySession()) + + with pytest.raises(PermissionError, match="does not expose"): + model.verify_access() diff --git a/tests/browser_agent/test_report.py b/tests/browser_agent/test_report.py index 0e9a2473e..dbe6434ed 100644 --- a/tests/browser_agent/test_report.py +++ b/tests/browser_agent/test_report.py @@ -116,7 +116,7 @@ def test_report_renders_measured_runtime_identities(tmp_path: Path) -> None: output=output, selection_path=Path("evaluation/browser-agent/webarena-verified-pilot.json"), ) - text = report.read_text(encoding="utf-8") + text = (output / "reproduction.html").read_text(encoding="utf-8") for expected in ( "abc123", @@ -129,3 +129,8 @@ def test_report_renders_measured_runtime_identities(tmp_path: Path) -> None: "official/reddit@sha256:site", ): assert expected in text + + assert report.is_file() + assert (output / "review.html").is_file() + report_data = json.loads((output / "report-data.json").read_text(encoding="utf-8")) + assert "evidence" not in report_data diff --git a/tests/browser_agent/test_review.py b/tests/browser_agent/test_review.py new file mode 100644 index 000000000..8f2fdd792 --- /dev/null +++ b/tests/browser_agent/test_review.py @@ -0,0 +1,84 @@ +import json +from pathlib import Path + +from harness.browser_agent.review import review_results, write_review + + +def _write_fixture(root: Path, *, row_score: float = 1.0) -> None: + attempt_dir = root / "chromium" / "44" / "attempt-1" + attempt_dir.mkdir(parents=True) + response = {"task_type": "NAVIGATE", "status": "SUCCESS", "retrieved_data": None} + evaluation = { + "status": "success", + "score": 1.0, + "webarena_verified_version": "1.2.3", + "webarena_verified_evaluator_checksum": "eval-sha", + "webarena_verified_data_checksum": "data-sha", + } + paths = { + "network_har": attempt_dir / "network.har", + "agent_trace": attempt_dir / "agent_trace.zip", + "response": attempt_dir / "agent_response.json", + "evaluation": attempt_dir / "eval_result.json", + "events": attempt_dir / "agent_events.json", + } + paths["network_har"].write_text("{}", encoding="utf-8") + paths["agent_trace"].write_bytes(b"trace") + paths["response"].write_text(json.dumps(response), encoding="utf-8") + paths["evaluation"].write_text(json.dumps(evaluation), encoding="utf-8") + paths["events"].write_text("[]", encoding="utf-8") + row = { + "engine": "chromium", + "task_id": 44, + "attempt": 1, + "score": row_score, + "status": "success", + "agent_response": response, + "model_usage": {"request_count": 2, "request_attempt_count": 2}, + "model_transport_errors": [], + "paths": {name: str(path) for name, path in paths.items()}, + } + (root / "attempts.jsonl").write_text(json.dumps(row) + "\n", encoding="utf-8") + (root / "manifest.json").write_text( + json.dumps( + { + "selection_sha256": "selection-sha", + "environment": { + "official_evaluator": { + "evaluator_checksum": "eval-sha", + "data_checksum": "data-sha", + } + }, + } + ), + encoding="utf-8", + ) + + +def test_review_verifies_every_artifact_and_binding(tmp_path: Path) -> None: + _write_fixture(tmp_path) + + path = write_review(evidence=tmp_path, selection_sha256="selection-sha") + review = json.loads(path.read_text(encoding="utf-8")) + + assert review["coverage"] == {"recorded_attempts": 1, "reviewed_attempts": 1} + assert review["disagreements"] == [] + assert review["unresolved"] == [] + assert review["attempts"][0]["outcome"] == "verified" + assert set(review["attempts"][0]["artifacts"]) == { + "network_har", + "agent_trace", + "response", + "evaluation", + "events", + } + + +def test_review_never_adjudicates_score_disagreement(tmp_path: Path) -> None: + _write_fixture(tmp_path, row_score=0.0) + + review = review_results(evidence=tmp_path, selection_sha256="selection-sha") + + assert review["attempts"][0]["outcome"] == "unresolved" + assert review["disagreements"][0]["failed_checks"] == ["score_matches_evaluator"] + assert review["unresolved"][0]["failed_checks"] == ["score_matches_evaluator"] diff --git a/tests/browser_agent/test_runner.py b/tests/browser_agent/test_runner.py index aef50760b..b8a373c58 100644 --- a/tests/browser_agent/test_runner.py +++ b/tests/browser_agent/test_runner.py @@ -3,10 +3,15 @@ from harness.browser_agent.runner import ( SITE_AUTH_HEADERS, + _allow_official_container_hostname, + _container_host_gateway, _configure_benchmark, _login_context, + _limit_cowagent_transport_retries, _scheduled_tasks, _user_prompt, + _wait_application_ready, + _wait_container_healthy, ) from harness.browser_agent.selection import load_pilot @@ -88,3 +93,85 @@ def _submit(self, callback): assert stale.closed is True assert primary.url == "http://official.test/start" + + +def test_container_host_gateway_returns_literal_ip() -> None: + class Result: + exit_code = 0 + output = b"192.168.5.2 host.docker.internal\n" + + class Container: + def exec_run(self, command): + assert command == ["getent", "hosts", "host.docker.internal"] + return Result() + + assert _container_host_gateway(Container()) == "192.168.5.2" + + +def test_wait_container_healthy_waits_past_starting(monkeypatch) -> None: + class Container: + attrs = {"State": {"Health": {"Status": "starting"}}} + + def reload(self): + self.attrs = {"State": {"Health": {"Status": "healthy"}}} + + monkeypatch.setattr("harness.browser_agent.runner.time.sleep", lambda _seconds: None) + _wait_container_healthy(Container(), timeout_seconds=1) + + +def test_application_readiness_does_not_follow_container_only_hostname(monkeypatch) -> None: + calls = [] + + class Response: + status_code = 302 + + def get(url, **kwargs): + calls.append((url, kwargs)) + return Response() + + monkeypatch.setattr("harness.browser_agent.runner.requests.get", get) + _wait_application_ready( + WebArenaSite.SHOPPING_ADMIN, + "/", + timeout_seconds=1, + stable_responses=1, + ) + + assert calls == [ + ( + "http://127.0.0.1:7780/", + {"timeout": 15, "allow_redirects": False}, + ) + ] + + +def test_cowagent_transport_retry_limit_is_exactly_one() -> None: + calls = [] + + class Executor: + def _call_llm_stream( + self, retry_on_empty=True, retry_count=0, max_retries=3, _overflow_stage=0 + ): + calls.append(max_retries) + return "ok" + + _limit_cowagent_transport_retries(Executor) + + assert Executor()._call_llm_stream(max_retries=9) == "ok" + assert calls == [1] + + +def test_only_official_container_hostname_bypasses_host_dns_validation() -> None: + checked = [] + + class Tool: + @staticmethod + def _validate_url_safe(url): + checked.append(url) + + tool = Tool() + _allow_official_container_hostname(tool) + + tool._validate_url_safe("http://host.docker.internal:7780/admin") + tool._validate_url_safe("https://example.com") + assert checked == ["https://example.com"] From 0979faafdeaebb67d37ef124a962372692e51bc0 Mon Sep 17 00:00:00 2001 From: kevin Date: Wed, 9 Sep 2026 13:03:43 +0800 Subject: [PATCH 016/127] Allow slow official environment restores --- harness/browser_agent/runner.py | 14 ++++++++++++-- tests/browser_agent/test_runner.py | 5 +++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/harness/browser_agent/runner.py b/harness/browser_agent/runner.py index 845b62b13..b45bc8671 100644 --- a/harness/browser_agent/runner.py +++ b/harness/browser_agent/runner.py @@ -63,6 +63,12 @@ WebArenaSite.SHOPPING_ADMIN: "X-M2-Admin-Auto-Login", WebArenaSite.REDDIT: "X-Postmill-Auto-Login", } +# The official all-in-one shopping image restores Magento, MariaDB and +# Elasticsearch before env-ctrl declares the fixture ready. On Colima/QEMU this +# repeatedly exceeded the package example's 180-second default despite becoming +# healthy shortly afterwards. A longer ceiling changes no measured task time; +# it only keeps environment preparation from aborting a formal cohort. +ENV_CTRL_READY_TIMEOUT_SECONDS = 600 def _load_repo_env(repo_root: Path) -> dict[str, str]: @@ -220,8 +226,12 @@ def _ensure_site(site: WebArenaSite, *, reset: bool) -> None: manager = ContainerManager(site=site, hostname="host.docker.internal") if reset or not manager.is_running(): manager.start(port=port, env_ctrl_port=ctrl_port, wait=False) - ready = EnvCtrlDockerClient.create(manager.container_name, timeout=180).wait_until_ready( - timeout=180, interval=2 + ready = EnvCtrlDockerClient.create( + manager.container_name, + timeout=ENV_CTRL_READY_TIMEOUT_SECONDS, + ).wait_until_ready( + timeout=ENV_CTRL_READY_TIMEOUT_SECONDS, + interval=2, ) if not ready.success: raise RuntimeError(f"{site.value} did not become ready: {ready.message}") diff --git a/tests/browser_agent/test_runner.py b/tests/browser_agent/test_runner.py index b8a373c58..27e31d151 100644 --- a/tests/browser_agent/test_runner.py +++ b/tests/browser_agent/test_runner.py @@ -2,6 +2,7 @@ from webarena_verified.types.task import WebArenaSite from harness.browser_agent.runner import ( + ENV_CTRL_READY_TIMEOUT_SECONDS, SITE_AUTH_HEADERS, _allow_official_container_hostname, _container_host_gateway, @@ -145,6 +146,10 @@ def get(url, **kwargs): ] +def test_environment_control_wait_covers_slow_official_image_restore() -> None: + assert ENV_CTRL_READY_TIMEOUT_SECONDS == 600 + + def test_cowagent_transport_retry_limit_is_exactly_one() -> None: calls = [] From 3b9a0566ffc108eb183e717cad550864ea968c65 Mon Sep 17 00:00:00 2001 From: kevin Date: Wed, 9 Sep 2026 14:28:31 +0800 Subject: [PATCH 017/127] Make official Magento images reliable on ARM --- examples/browser_agent/README.md | 10 +++ harness/browser_agent/runner.py | 88 ++++++++++++++++++- .../browser_agent/runtime/php-fpm-qemu.ini | 12 +++ tests/browser_agent/test_runner.py | 43 ++++++++- 4 files changed, 148 insertions(+), 5 deletions(-) create mode 100644 harness/browser_agent/runtime/php-fpm-qemu.ini diff --git a/examples/browser_agent/README.md b/examples/browser_agent/README.md index 4b9b2ca93..9574da912 100644 --- a/examples/browser_agent/README.md +++ b/examples/browser_agent/README.md @@ -13,6 +13,16 @@ label. The pilot uses the self-hosted shopping, shopping_admin, reddit, and gitlab sites. Wikipedia and Map are deferred because their official local setup requires large external data bundles. +On Apple Silicon, run the official amd64 site images in a dedicated Colima VZ +profile with Rosetta enabled and at least 140 GiB of disk. The four required +images occupy about 91 GB before container writable layers. The runner mounts +Colima's QEMU interpreter only for PHP-FPM in the two Magento images because the +legacy Alpine/musl PHP-FPM binary segfaults under Rosetta; Java, GitLab, and the +remaining official image processes stay on Rosetta. This is a runtime +compatibility shim, not a change to site data or evaluation logic. + + colima start browser-eval --vm-type vz --disk 140 --memory 8 --cpus 4 --vz-rosetta + Put `OPENAI_API_KEY`, optional `OPENAI_BASE_URL`, and `COWAGENT_REPO` in this repository's `.env`, then run: diff --git a/harness/browser_agent/runner.py b/harness/browser_agent/runner.py index b45bc8671..c60edca1f 100644 --- a/harness/browser_agent/runner.py +++ b/harness/browser_agent/runner.py @@ -3,6 +3,7 @@ from __future__ import annotations import argparse +import hashlib import importlib.metadata import ipaddress import json @@ -22,9 +23,16 @@ from dotenv import dotenv_values from json_repair import repair_json from webarena_verified import WebArenaVerified +from webarena_verified.environments.container.config import get_container_config from webarena_verified.environments.container.manager import ContainerManager from webarena_verified.environments.env_ctrl_client import EnvCtrlDockerClient -from webarena_verified.types.config import EnvironmentConfig, WebArenaVerifiedConfig +from webarena_verified.types.config import ( + ContainerConfig, + ContainerSetupConfig, + ContainerVolumeSpec, + EnvironmentConfig, + WebArenaVerifiedConfig, +) from webarena_verified.types.task import WebArenaSite from harness.browser_kernel.metrics import ( @@ -69,6 +77,10 @@ # healthy shortly afterwards. A longer ceiling changes no measured task time; # it only keeps environment preparation from aborting a formal cohort. ENV_CTRL_READY_TIMEOUT_SECONDS = 600 +AMD64_PHP_SITES = frozenset({WebArenaSite.SHOPPING, WebArenaSite.SHOPPING_ADMIN}) +PHP_FPM_COMPAT_CONFIG = Path(__file__).with_name("runtime") / "php-fpm-qemu.ini" +QEMU_X86_64_HOST_PATH = "/usr/bin/qemu-x86_64" +QEMU_X86_64_CONTAINER_PATH = "/usr/local/bin/qemu-x86_64" def _load_repo_env(repo_root: Path) -> dict[str, str]: @@ -221,9 +233,73 @@ def _wait_container_healthy(container: Any, *, timeout_seconds: float = 600) -> raise RuntimeError(f"GitLab container health timed out in state {last_status!r}") +def _site_container_manager(site: WebArenaSite) -> ContainerManager: + """Use QEMU only for PHP-FPM when amd64 site images run on an ARM host. + + Rosetta starts the official images' Java services reliably, but PHP-FPM in + both Magento images segfaults under Rosetta. Mounting Colima's static QEMU + interpreter and a one-line supervisor override keeps every other process on + Rosetta while preserving the official image and site data. + """ + config = get_container_config(site=site) + docker_arch = str(docker.from_env().info().get("Architecture") or "") + if site not in AMD64_PHP_SITES or docker_arch not in {"aarch64", "arm64"}: + return ContainerManager(site=site, hostname="host.docker.internal") + if not PHP_FPM_COMPAT_CONFIG.is_file(): + raise RuntimeError(f"missing PHP-FPM compatibility config: {PHP_FPM_COMPAT_CONFIG}") + compat = ContainerConfig( + **config.model_dump(exclude={"setup"}), + setup=ContainerSetupConfig( + volumes=( + ContainerVolumeSpec( + volume_name=QEMU_X86_64_HOST_PATH, + mount_path=QEMU_X86_64_CONTAINER_PATH, + ), + ContainerVolumeSpec( + volume_name=str(PHP_FPM_COMPAT_CONFIG.resolve()), + mount_path="/etc/supervisor.d/php-fpm.ini:ro", + ), + ) + ), + ) + return ContainerManager( + site=site, + config=compat, + hostname="host.docker.internal", + ) + + +def _verify_arm_php_compatibility(container_name: str) -> None: + container = docker.from_env().containers.get(container_name) + processes = container.exec_run(["ps", "-o", "pid,args"]) + process_table = processes.output.decode(errors="replace") + qemu = container.exec_run(["test", "-x", QEMU_X86_64_CONTAINER_PATH]) + if processes.exit_code != 0 or "/mnt/lima-rosetta/rosetta" not in process_table: + raise RuntimeError( + "ARM Docker host must enable Rosetta for official amd64 site images" + ) + if qemu.exit_code != 0: + raise RuntimeError("PHP-FPM QEMU compatibility interpreter was not mounted") + if f"{QEMU_X86_64_CONTAINER_PATH} /usr/local/sbin/php-fpm" not in process_table: + raise RuntimeError("PHP-FPM did not start through the mounted QEMU interpreter") + + +def _site_runtime_compatibility(docker_arch: str) -> dict[str, Any]: + if docker_arch not in {"aarch64", "arm64"}: + return {"mode": "host-default"} + return { + "mode": "rosetta-with-qemu-php-fpm", + "sites": sorted(site.value for site in AMD64_PHP_SITES), + "qemu_host_path": QEMU_X86_64_HOST_PATH, + "supervisor_config_sha256": hashlib.sha256( + PHP_FPM_COMPAT_CONFIG.read_bytes() + ).hexdigest(), + } + + def _ensure_site(site: WebArenaSite, *, reset: bool) -> None: port, ctrl_port = SITE_PORTS[site] - manager = ContainerManager(site=site, hostname="host.docker.internal") + manager = _site_container_manager(site) if reset or not manager.is_running(): manager.start(port=port, env_ctrl_port=ctrl_port, wait=False) ready = EnvCtrlDockerClient.create( @@ -239,6 +315,8 @@ def _ensure_site(site: WebArenaSite, *, reset: bool) -> None: container = docker.from_env().containers.get(manager.container_name) _wait_container_healthy(container) _wait_application_ready(site, manager.config.health_check_path) + if manager.config.setup is not None and site in AMD64_PHP_SITES: + _verify_arm_php_compatibility(manager.container_name) def _wait_application_ready( @@ -283,7 +361,7 @@ def _wait_application_ready( def _prepare_sites(sites: tuple[WebArenaSite, ...]) -> None: """Keep only task-required sites running, then reset those sites from official images.""" for site in SITE_PORTS: - manager = ContainerManager(site=site, hostname="host.docker.internal") + manager = _site_container_manager(site) if site not in sites and manager.is_running(): manager.stop() for site in sites: @@ -667,6 +745,8 @@ def run(args: argparse.Namespace) -> Path: # noqa: PLR0915 browser_metadata: dict[str, dict[str, str]] = {} site_access_host = "" docker_info: dict[str, Any] = {} + docker_architecture = str(client.info().get("Architecture") or "") + site_runtime_compatibility = _site_runtime_compatibility(docker_architecture) try: for engine in ENGINE_IMAGES: network = client.networks.create(f"lex-browser-agent-{run_id}-{engine}") @@ -715,6 +795,7 @@ def run(args: argparse.Namespace) -> Path: # noqa: PLR0915 capture_output=True, text=True, ).stdout.strip(), + "site_runtime_compatibility": site_runtime_compatibility, }, ) @@ -830,6 +911,7 @@ def run(args: argparse.Namespace) -> Path: # noqa: PLR0915 "site_images": site_image_metadata, "site_access_host": site_access_host, "official_evaluator": evaluator_provenance, + "site_runtime_compatibility": site_runtime_compatibility, }, } (output_dir / "manifest.json").write_text( diff --git a/harness/browser_agent/runtime/php-fpm-qemu.ini b/harness/browser_agent/runtime/php-fpm-qemu.ini new file mode 100644 index 000000000..8149b77f7 --- /dev/null +++ b/harness/browser_agent/runtime/php-fpm-qemu.ini @@ -0,0 +1,12 @@ +# ARM-host compatibility for the official amd64 Magento images. +[program:php-fpm] +command=/usr/local/bin/qemu-x86_64 /usr/local/sbin/php-fpm --nodaemonize --fpm-config /usr/local/etc/php-fpm.conf +autostart=true +autorestart=true +priority=2 +startretries=3 +stopwaitsecs=10 +stdout_logfile=/dev/stdout +stdout_logfile_maxbytes=0 +stderr_logfile=/dev/stderr +stderr_logfile_maxbytes=0 diff --git a/tests/browser_agent/test_runner.py b/tests/browser_agent/test_runner.py index 27e31d151..576941598 100644 --- a/tests/browser_agent/test_runner.py +++ b/tests/browser_agent/test_runner.py @@ -5,11 +5,13 @@ ENV_CTRL_READY_TIMEOUT_SECONDS, SITE_AUTH_HEADERS, _allow_official_container_hostname, - _container_host_gateway, _configure_benchmark, - _login_context, + _container_host_gateway, _limit_cowagent_transport_retries, + _login_context, _scheduled_tasks, + _site_container_manager, + _site_runtime_compatibility, _user_prompt, _wait_application_ready, _wait_container_healthy, @@ -150,6 +152,43 @@ def test_environment_control_wait_covers_slow_official_image_restore() -> None: assert ENV_CTRL_READY_TIMEOUT_SECONDS == 600 +def test_arm_php_sites_mount_targeted_qemu_compatibility(monkeypatch) -> None: + class Client: + @staticmethod + def info(): + return {"Architecture": "aarch64"} + + monkeypatch.setattr("harness.browser_agent.runner.docker.from_env", lambda: Client()) + manager = _site_container_manager(WebArenaSite.SHOPPING) + + assert manager.config.docker_img == "am1n3e/webarena-verified-shopping" + assert manager.config.volumes["/usr/bin/qemu-x86_64"] == "/usr/local/bin/qemu-x86_64" + assert manager.config.volumes[str(manager.config.setup.volumes[1].volume_name)].endswith( + "/etc/supervisor.d/php-fpm.ini:ro" + ) + + +def test_non_php_site_keeps_official_default_config(monkeypatch) -> None: + class Client: + @staticmethod + def info(): + return {"Architecture": "aarch64"} + + monkeypatch.setattr("harness.browser_agent.runner.docker.from_env", lambda: Client()) + manager = _site_container_manager(WebArenaSite.REDDIT) + + assert manager.config.docker_img == "am1n3e/webarena-verified-reddit" + assert manager.config.volumes == {} + + +def test_arm_runtime_compatibility_is_evidence_bound() -> None: + compatibility = _site_runtime_compatibility("aarch64") + + assert compatibility["mode"] == "rosetta-with-qemu-php-fpm" + assert compatibility["sites"] == ["shopping", "shopping_admin"] + assert len(compatibility["supervisor_config_sha256"]) == 64 + + def test_cowagent_transport_retry_limit_is_exactly_one() -> None: calls = [] From 538117428c346f4fd94d8807c8a632cd5cc11ebd Mon Sep 17 00:00:00 2001 From: kevin Date: Wed, 9 Sep 2026 14:31:40 +0800 Subject: [PATCH 018/127] Allow explicit provider credential file --- harness/browser_agent/runner.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/harness/browser_agent/runner.py b/harness/browser_agent/runner.py index c60edca1f..dc0814a94 100644 --- a/harness/browser_agent/runner.py +++ b/harness/browser_agent/runner.py @@ -697,11 +697,20 @@ def _scheduled_tasks(tasks: tuple[PilotTask, ...], max_tasks: int | None) -> tup def run(args: argparse.Namespace) -> Path: # noqa: PLR0915 repo_root = Path(args.repo_root).resolve() - env = _load_repo_env(repo_root) + repo_env = _load_repo_env(repo_root) + env_file = Path(args.env_file).expanduser().resolve() if args.env_file else None + env = ( + {key: value for key, value in dotenv_values(env_file).items() if value is not None} + if env_file + else repo_env + ) api_key = env.get("OPENAI_API_KEY") if not api_key: - raise RuntimeError(f"OPENAI_API_KEY is required in {repo_root / '.env'}") - cowagent_repo = Path(args.cowagent_repo or env.get("COWAGENT_REPO", "")).resolve() + source = env_file or repo_root / ".env" + raise RuntimeError(f"OPENAI_API_KEY is required in {source}") + cowagent_repo = Path( + args.cowagent_repo or repo_env.get("COWAGENT_REPO", "") + ).resolve() if not (cowagent_repo / "agent/protocol/agent.py").exists(): raise RuntimeError("--cowagent-repo must point to a CowAgent checkout") harness_commit = _require_clean_git_checkout(repo_root) @@ -785,6 +794,9 @@ def run(args: argparse.Namespace) -> Path: # noqa: PLR0915 "model": args.model, "reasoning_effort": "low", "model_transport_retries": 1, + "provider_base_url": env.get( + "OPENAI_BASE_URL", "https://api.openai.com/v1" + ), "browser_images": image_metadata, "site_images": site_image_metadata, "browser_versions": browser_metadata, @@ -927,6 +939,10 @@ def parse_args() -> argparse.Namespace: "--selection", default="evaluation/browser-agent/webarena-verified-pilot.json" ) parser.add_argument("--output", required=True) + parser.add_argument( + "--env-file", + help="explicit dotenv file for provider credentials; defaults to the repository .env", + ) parser.add_argument( "--cowagent-repo", help="trusted, clean CowAgent Git checkout; Python code is imported and executed from it", From 64fcbca5dfb35dddee108f94c4de747f73a87a0a Mon Sep 17 00:00:00 2001 From: kevin Date: Wed, 9 Sep 2026 14:34:43 +0800 Subject: [PATCH 019/127] Document and verify ARM report delivery --- harness/browser_agent/report.py | 2 +- scripts/publish_browser_agent_artifact_hub.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/harness/browser_agent/report.py b/harness/browser_agent/report.py index dc2f67703..b7e4dcb3e 100644 --- a/harness/browser_agent/report.py +++ b/harness/browser_agent/report.py @@ -290,7 +290,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path:

    Benchmark taxonomy

    它们评最终结果,还是评过程?

    {methodology}

    WebArena‑Verified 的精确判分链

    官方 1.2.3 数据共有 812 题,812 题都配置 AgentResponseEvaluator,其中 663 题还配置 NetworkEventEvaluator。前者解析 Agent 最终 JSON,核对任务类型、状态和带类型的返回字段;后者从 Playwright trace 或 HAR 中提取请求、响应、Cookie 等事件,核对导航或状态改变是否实际发生。字段先按官方 schema 做 URL、字符串、数字、布尔、数组和对象规范化,再做结构比较;需要有序时保留顺序,允许的备选答案由任务定义声明。没有模糊子串放行,也没有 LLM judge。

    单题是二元分:该题配置的每一个 evaluator 都得到 1.0 才通过,任何一项失败或 evaluator 报错都记 0。因此它评的是最终结果与必要的可观察副作用,不是“按参考路径逐步打分”。Agent 可以走不同点击路径,只要完成的结果和必要事件符合任务契约即可。

    Mind2Web 的 easy / medium / hard 要先纠正

    原始 Mind2Web 的官方拆分不是难度档,而是 cross-task、cross-website、cross-domain:测试任务是否见过同站任务、是否换了网站、是否换了整个领域。Online‑Mind2Web 官方也没有稳定的 easy/medium/hard 定义;有些第三方按参考动作长度再分档,那是第三方分析口径。Mind2Web 2 论文把任务 horizon 定义为 short(少于 10 次动作)、medium(10–50)、long(超过 50),仍不等同于 easy/medium/hard。

    所以本轮的 5/10/15 是我们自己的预注册操作长度层级。它只用于保证样本覆盖,不改写官方字段,也不把它当成跨论文可比较的难度标签。

    Browser Use 官方现在更偏向什么

    要区分三种信号。Browser Use 产品首页强调开放的 100 题 BU Bench V1,同时展示 Odysseys 的 200 题长链排行榜成绩;其 benchmark 仓库当前把 71 题 Stealth Bench V1 放在最前。Stealth Bench 测的是反机器人检测与浏览器基础设施,不是 CowAgent 能否自主完成业务任务,因此不能代替本报告的 agent 评测。

    BU Bench 由 Custom、WebBench、Mind2Web 2、GAIA、BrowseComp 各 20 题组成,另有独立的 300 题 Online‑Mind2Web 运行仓库。最值得借鉴的是:运行前确认任务可完成、保存逐题 trace、支持断点续跑、把“不可完成”与 Agent 真失败分开。Odysseys 还把长任务拆成平均 6.1 个可检查 rubric,并同时报告完整成功率、部分进度和每步效率。

    首轮不直接照搬的原因也很具体:这些工作流都包含模型评审,BU Bench 还混入搜索与问答任务,Odysseys 又是实时多站长链任务。它们适合第二阶段测真实互联网泛化和长链能力,但会把网页漂移、judge 波动和浏览器内核差异同时混进结果。

    -

    WebArena‑Verified 本地部署要不要账号付费

    四个首轮站点使用官方 Docker 镜像和内置测试账号,本地运行不需要向 GitLab、Reddit 或电商站点申请真实账号,也没有按题平台费用。成本来自本机 CPU、内存、磁盘和模型 API。

    本轮模型是 {_e(run_model)},通过 Responses API 调用,并固定 reasoning.effort=low。当前 LiteLLM Team 没有 gpt-6-astra 权限,因此 GPT‑5.6 运行只能证明流程和 Chromium/Moli 在该模型条件下的差异,不能替代 Astra 成绩。

    “本地可部署”也不等于轻量。本轮核对的官方 GitLab 镜像 manifest 含一个约 22.0 GB 的压缩主层,官方文档建议至少 4 GB 内存;四站点与两浏览器因此放在独立的 100 GB Docker profile 中。Wikipedia 与 Map 还需要额外数据卷,所以首轮不纳入。自托管能消除网站内容漂移、封号和真实支付风险,但不会自动消除镜像下载、环境初始化、登录兼容、浏览器 CDP 实现差异或 evaluator 缺陷。

    +

    WebArena‑Verified 本地部署要不要账号付费

    四个首轮站点使用官方 Docker 镜像和内置测试账号,本地运行不需要向 GitLab、Reddit 或电商站点申请真实账号,也没有按题平台费用。成本来自本机 CPU、内存、磁盘和模型 API。

    本轮模型是 {_e(run_model)},通过 Responses API 调用,并固定 reasoning.effort=low。当前 LiteLLM Team 没有 gpt-6-astra 权限,因此 GPT‑5.6 运行只能证明流程和 Chromium/Moli 在该模型条件下的差异,不能替代 Astra 成绩。

    “本地可部署”也不等于轻量。本轮核对的官方 GitLab 镜像 manifest 含一个约 22.0 GB 的压缩主层,四个站点镜像合计约 91 GB;正式环境因此使用独立的 140 GiB Docker profile。Apple Silicon 上,官方 amd64 Magento 镜像还需要兼容层:Java、数据库及其余进程通过 Rosetta,旧 Alpine/musl PHP‑FPM 单独通过挂载的 QEMU 运行。该适配不修改官方镜像、站点数据或 evaluator,并以配置哈希写入运行证据。Wikipedia 与 Map 还需要额外数据卷,所以首轮不纳入。自托管能消除网站内容漂移、封号和真实支付风险,但不会自动消除镜像下载、环境初始化、登录兼容、浏览器 CDP 实现差异或 evaluator 缺陷。

    本轮怎样保证是可比较的自主 Agent 实验

    {_table(['项目','预注册规则'], [['样本','固定 30 题:简单 5、中等 10、困难 15;运行后不换题,不重复抽样'],['Agent 与模型',f'CowAgent 真实自主观察、规划和调用 BrowserTool;{run_model},reasoning=low;每个试次清空模型会话'],['工具接口','沿用 CowAgent 的页面快照与 browser 函数工具,不调用模型原生 computer tool;测的是既有 CowAgent 接入方式下的自主 browser use'],['唯一变量','Agent、提示、模型、任务、站点数据和资源限额相同,只替换 Chromium / Moli'],['环境','每个试次都从官方镜像重置该题站点并等待应用连续稳定;Shopping、Shopping Admin、Reddit 使用官方头登录,GitLab 使用官方 UI 登录'],['顺序','逐题交替先跑 Chromium 或 Moli,减轻固定先后顺序偏差'],['主分','每个“题 × 浏览器”只跑一次,官方二元 pass@1;两边构成逐题配对'],['重试','任务首轮失败后最多重跑一次,恢复只作诊断;每次模型请求遇到传输错误也最多重发一次,所有请求尝试与错误都入证'],['留证','保存最终 JSON、CowAgent 事件、标准网络 HAR、可视 Playwright trace、官方 evaluator 明细、镜像 ID、代码 commit、token 与资源指标'],['排除','首轮不选 Wikipedia / Map;不选无法由官方确定性 evaluator 验收的外部实时站点任务']])}

    “操作步数层级”只决定覆盖结构,不参与得分。环境准备失败应标成基础设施错误并修复后重跑同一预注册试次,不能计作 Agent 失败,也不能借机换题。官方 evaluator 读取 HAR;可视 trace 只用于调试,不参与评分。由于工具接口不同,本轮结果不能直接代表 Astra 发布页中原生 computer-use harness 的成绩。

    {_result_section(attempts)}{_tier_section(attempts)}{_task_table(selection_path, attempts)}

    两条轨道必须分开读

    {_table(['轨道','Agent 是否自主','控制变量','主指标','回答的问题'], [['浏览器内核微基准(原 30 题)','否;重放固定 BrowserTool 等价调用','同一动作序列,只替换浏览器','严格输出正确率、动作执行、资源与耗时','相同操作交给两个内核,谁更兼容/省资源?'],['WebArena‑Verified Agent 评测(本报告)','是;CowAgent 自主观察、规划和调用 BrowserTool','同一 Agent、模型、提示、站点快照与任务,只替换浏览器','官方首轮 pass@1;失败重试恢复率另列','浏览器差异会怎样影响完整自主任务成功率?']])}

    固定动作轨道不是“有问题的假 Agent”,它是有价值的内核微基准;问题只是旧命名把它说成了自主 browser use。两条轨道互补,但分数不能相加,也不能用一条替代另一条。

    diff --git a/scripts/publish_browser_agent_artifact_hub.py b/scripts/publish_browser_agent_artifact_hub.py index 23a3f3a05..c9f9f69eb 100644 --- a/scripts/publish_browser_agent_artifact_hub.py +++ b/scripts/publish_browser_agent_artifact_hub.py @@ -83,6 +83,14 @@ def main() -> None: with urllib.request.urlopen(public, timeout=60) as response: if response.status != 200: raise RuntimeError(f"public report returned {response.status}") + preview = urllib.request.Request( + f"{BASE}/api/preview/{result['slug']}", + headers={"User-Agent": "Browser-Agent-Report-Publisher/1.0"}, + ) + with urllib.request.urlopen(preview, timeout=60) as response: + body = response.read().decode("utf-8", errors="replace") + if response.status != 200 or "Browser Use 公开评测怎么选" not in body: + raise RuntimeError("public preview is unavailable or missing the report heading") print(json.dumps({"slug": result["slug"], "url": result["share_url"]}, ensure_ascii=False)) From 1cb9c09fdb2063f2f2b5d9fc4ac473358072eabe Mon Sep 17 00:00:00 2001 From: kevin Date: Wed, 9 Sep 2026 14:50:35 +0800 Subject: [PATCH 020/127] Run official Reddit PHP reliably on ARM --- examples/browser_agent/README.md | 6 +++--- harness/browser_agent/report.py | 2 +- harness/browser_agent/runner.py | 4 +++- tests/browser_agent/test_runner.py | 6 +++--- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/examples/browser_agent/README.md b/examples/browser_agent/README.md index 9574da912..7e2d51046 100644 --- a/examples/browser_agent/README.md +++ b/examples/browser_agent/README.md @@ -16,9 +16,9 @@ requires large external data bundles. On Apple Silicon, run the official amd64 site images in a dedicated Colima VZ profile with Rosetta enabled and at least 140 GiB of disk. The four required images occupy about 91 GB before container writable layers. The runner mounts -Colima's QEMU interpreter only for PHP-FPM in the two Magento images because the -legacy Alpine/musl PHP-FPM binary segfaults under Rosetta; Java, GitLab, and the -remaining official image processes stay on Rosetta. This is a runtime +Colima's QEMU interpreter only for PHP-FPM in the Magento and Reddit images because +the legacy Alpine/musl PHP-FPM binary segfaults under Rosetta; Java, GitLab, and +the remaining official image processes stay on Rosetta. This is a runtime compatibility shim, not a change to site data or evaluation logic. colima start browser-eval --vm-type vz --disk 140 --memory 8 --cpus 4 --vz-rosetta diff --git a/harness/browser_agent/report.py b/harness/browser_agent/report.py index b7e4dcb3e..eeba189b2 100644 --- a/harness/browser_agent/report.py +++ b/harness/browser_agent/report.py @@ -290,7 +290,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path:

    Benchmark taxonomy

    它们评最终结果,还是评过程?

    {methodology}

    WebArena‑Verified 的精确判分链

    官方 1.2.3 数据共有 812 题,812 题都配置 AgentResponseEvaluator,其中 663 题还配置 NetworkEventEvaluator。前者解析 Agent 最终 JSON,核对任务类型、状态和带类型的返回字段;后者从 Playwright trace 或 HAR 中提取请求、响应、Cookie 等事件,核对导航或状态改变是否实际发生。字段先按官方 schema 做 URL、字符串、数字、布尔、数组和对象规范化,再做结构比较;需要有序时保留顺序,允许的备选答案由任务定义声明。没有模糊子串放行,也没有 LLM judge。

    单题是二元分:该题配置的每一个 evaluator 都得到 1.0 才通过,任何一项失败或 evaluator 报错都记 0。因此它评的是最终结果与必要的可观察副作用,不是“按参考路径逐步打分”。Agent 可以走不同点击路径,只要完成的结果和必要事件符合任务契约即可。

    Mind2Web 的 easy / medium / hard 要先纠正

    原始 Mind2Web 的官方拆分不是难度档,而是 cross-task、cross-website、cross-domain:测试任务是否见过同站任务、是否换了网站、是否换了整个领域。Online‑Mind2Web 官方也没有稳定的 easy/medium/hard 定义;有些第三方按参考动作长度再分档,那是第三方分析口径。Mind2Web 2 论文把任务 horizon 定义为 short(少于 10 次动作)、medium(10–50)、long(超过 50),仍不等同于 easy/medium/hard。

    所以本轮的 5/10/15 是我们自己的预注册操作长度层级。它只用于保证样本覆盖,不改写官方字段,也不把它当成跨论文可比较的难度标签。

    Browser Use 官方现在更偏向什么

    要区分三种信号。Browser Use 产品首页强调开放的 100 题 BU Bench V1,同时展示 Odysseys 的 200 题长链排行榜成绩;其 benchmark 仓库当前把 71 题 Stealth Bench V1 放在最前。Stealth Bench 测的是反机器人检测与浏览器基础设施,不是 CowAgent 能否自主完成业务任务,因此不能代替本报告的 agent 评测。

    BU Bench 由 Custom、WebBench、Mind2Web 2、GAIA、BrowseComp 各 20 题组成,另有独立的 300 题 Online‑Mind2Web 运行仓库。最值得借鉴的是:运行前确认任务可完成、保存逐题 trace、支持断点续跑、把“不可完成”与 Agent 真失败分开。Odysseys 还把长任务拆成平均 6.1 个可检查 rubric,并同时报告完整成功率、部分进度和每步效率。

    首轮不直接照搬的原因也很具体:这些工作流都包含模型评审,BU Bench 还混入搜索与问答任务,Odysseys 又是实时多站长链任务。它们适合第二阶段测真实互联网泛化和长链能力,但会把网页漂移、judge 波动和浏览器内核差异同时混进结果。

    -

    WebArena‑Verified 本地部署要不要账号付费

    四个首轮站点使用官方 Docker 镜像和内置测试账号,本地运行不需要向 GitLab、Reddit 或电商站点申请真实账号,也没有按题平台费用。成本来自本机 CPU、内存、磁盘和模型 API。

    本轮模型是 {_e(run_model)},通过 Responses API 调用,并固定 reasoning.effort=low。当前 LiteLLM Team 没有 gpt-6-astra 权限,因此 GPT‑5.6 运行只能证明流程和 Chromium/Moli 在该模型条件下的差异,不能替代 Astra 成绩。

    “本地可部署”也不等于轻量。本轮核对的官方 GitLab 镜像 manifest 含一个约 22.0 GB 的压缩主层,四个站点镜像合计约 91 GB;正式环境因此使用独立的 140 GiB Docker profile。Apple Silicon 上,官方 amd64 Magento 镜像还需要兼容层:Java、数据库及其余进程通过 Rosetta,旧 Alpine/musl PHP‑FPM 单独通过挂载的 QEMU 运行。该适配不修改官方镜像、站点数据或 evaluator,并以配置哈希写入运行证据。Wikipedia 与 Map 还需要额外数据卷,所以首轮不纳入。自托管能消除网站内容漂移、封号和真实支付风险,但不会自动消除镜像下载、环境初始化、登录兼容、浏览器 CDP 实现差异或 evaluator 缺陷。

    +

    WebArena‑Verified 本地部署要不要账号付费

    四个首轮站点使用官方 Docker 镜像和内置测试账号,本地运行不需要向 GitLab、Reddit 或电商站点申请真实账号,也没有按题平台费用。成本来自本机 CPU、内存、磁盘和模型 API。

    本轮模型是 {_e(run_model)},通过 Responses API 调用,并固定 reasoning.effort=low。当前 LiteLLM Team 没有 gpt-6-astra 权限,因此 GPT‑5.6 运行只能证明流程和 Chromium/Moli 在该模型条件下的差异,不能替代 Astra 成绩。

    “本地可部署”也不等于轻量。本轮核对的官方 GitLab 镜像 manifest 含一个约 22.0 GB 的压缩主层,四个站点镜像合计约 91 GB;正式环境因此使用独立的 140 GiB Docker profile。Apple Silicon 上,三个含旧 Alpine/musl PHP‑FPM 的官方 amd64 镜像需要兼容层:Java、数据库及其余进程通过 Rosetta,PHP‑FPM 单独通过挂载的 QEMU 运行。该适配不修改官方镜像、站点数据或 evaluator,并以配置哈希写入运行证据。Wikipedia 与 Map 还需要额外数据卷,所以首轮不纳入。自托管能消除网站内容漂移、封号和真实支付风险,但不会自动消除镜像下载、环境初始化、登录兼容、浏览器 CDP 实现差异或 evaluator 缺陷。

    本轮怎样保证是可比较的自主 Agent 实验

    {_table(['项目','预注册规则'], [['样本','固定 30 题:简单 5、中等 10、困难 15;运行后不换题,不重复抽样'],['Agent 与模型',f'CowAgent 真实自主观察、规划和调用 BrowserTool;{run_model},reasoning=low;每个试次清空模型会话'],['工具接口','沿用 CowAgent 的页面快照与 browser 函数工具,不调用模型原生 computer tool;测的是既有 CowAgent 接入方式下的自主 browser use'],['唯一变量','Agent、提示、模型、任务、站点数据和资源限额相同,只替换 Chromium / Moli'],['环境','每个试次都从官方镜像重置该题站点并等待应用连续稳定;Shopping、Shopping Admin、Reddit 使用官方头登录,GitLab 使用官方 UI 登录'],['顺序','逐题交替先跑 Chromium 或 Moli,减轻固定先后顺序偏差'],['主分','每个“题 × 浏览器”只跑一次,官方二元 pass@1;两边构成逐题配对'],['重试','任务首轮失败后最多重跑一次,恢复只作诊断;每次模型请求遇到传输错误也最多重发一次,所有请求尝试与错误都入证'],['留证','保存最终 JSON、CowAgent 事件、标准网络 HAR、可视 Playwright trace、官方 evaluator 明细、镜像 ID、代码 commit、token 与资源指标'],['排除','首轮不选 Wikipedia / Map;不选无法由官方确定性 evaluator 验收的外部实时站点任务']])}

    “操作步数层级”只决定覆盖结构,不参与得分。环境准备失败应标成基础设施错误并修复后重跑同一预注册试次,不能计作 Agent 失败,也不能借机换题。官方 evaluator 读取 HAR;可视 trace 只用于调试,不参与评分。由于工具接口不同,本轮结果不能直接代表 Astra 发布页中原生 computer-use harness 的成绩。

    {_result_section(attempts)}{_tier_section(attempts)}{_task_table(selection_path, attempts)}

    两条轨道必须分开读

    {_table(['轨道','Agent 是否自主','控制变量','主指标','回答的问题'], [['浏览器内核微基准(原 30 题)','否;重放固定 BrowserTool 等价调用','同一动作序列,只替换浏览器','严格输出正确率、动作执行、资源与耗时','相同操作交给两个内核,谁更兼容/省资源?'],['WebArena‑Verified Agent 评测(本报告)','是;CowAgent 自主观察、规划和调用 BrowserTool','同一 Agent、模型、提示、站点快照与任务,只替换浏览器','官方首轮 pass@1;失败重试恢复率另列','浏览器差异会怎样影响完整自主任务成功率?']])}

    固定动作轨道不是“有问题的假 Agent”,它是有价值的内核微基准;问题只是旧命名把它说成了自主 browser use。两条轨道互补,但分数不能相加,也不能用一条替代另一条。

    diff --git a/harness/browser_agent/runner.py b/harness/browser_agent/runner.py index dc0814a94..9b633063d 100644 --- a/harness/browser_agent/runner.py +++ b/harness/browser_agent/runner.py @@ -77,7 +77,9 @@ # healthy shortly afterwards. A longer ceiling changes no measured task time; # it only keeps environment preparation from aborting a formal cohort. ENV_CTRL_READY_TIMEOUT_SECONDS = 600 -AMD64_PHP_SITES = frozenset({WebArenaSite.SHOPPING, WebArenaSite.SHOPPING_ADMIN}) +AMD64_PHP_SITES = frozenset( + {WebArenaSite.SHOPPING, WebArenaSite.SHOPPING_ADMIN, WebArenaSite.REDDIT} +) PHP_FPM_COMPAT_CONFIG = Path(__file__).with_name("runtime") / "php-fpm-qemu.ini" QEMU_X86_64_HOST_PATH = "/usr/bin/qemu-x86_64" QEMU_X86_64_CONTAINER_PATH = "/usr/local/bin/qemu-x86_64" diff --git a/tests/browser_agent/test_runner.py b/tests/browser_agent/test_runner.py index 576941598..2625420de 100644 --- a/tests/browser_agent/test_runner.py +++ b/tests/browser_agent/test_runner.py @@ -175,9 +175,9 @@ def info(): return {"Architecture": "aarch64"} monkeypatch.setattr("harness.browser_agent.runner.docker.from_env", lambda: Client()) - manager = _site_container_manager(WebArenaSite.REDDIT) + manager = _site_container_manager(WebArenaSite.GITLAB) - assert manager.config.docker_img == "am1n3e/webarena-verified-reddit" + assert manager.config.docker_img == "am1n3e/webarena-verified-gitlab" assert manager.config.volumes == {} @@ -185,7 +185,7 @@ def test_arm_runtime_compatibility_is_evidence_bound() -> None: compatibility = _site_runtime_compatibility("aarch64") assert compatibility["mode"] == "rosetta-with-qemu-php-fpm" - assert compatibility["sites"] == ["shopping", "shopping_admin"] + assert compatibility["sites"] == ["reddit", "shopping", "shopping_admin"] assert len(compatibility["supervisor_config_sha256"]) == 64 From d02085416e9c4a8a3cdda10d7588d1d9e6820fec Mon Sep 17 00:00:00 2001 From: kevin Date: Wed, 9 Sep 2026 16:57:06 +0800 Subject: [PATCH 021/127] Publish verified browser agent pilot report --- harness/browser_agent/report.py | 41 ++++++++++++++++++- research/report-changes.json | 17 ++++++++ scripts/package_browser_agent_artifact_hub.py | 2 +- scripts/publish_browser_agent_artifact_hub.py | 2 +- 4 files changed, 59 insertions(+), 3 deletions(-) diff --git a/harness/browser_agent/report.py b/harness/browser_agent/report.py index eeba189b2..ffb112973 100644 --- a/harness/browser_agent/report.py +++ b/harness/browser_agent/report.py @@ -155,6 +155,44 @@ def _tier_section(attempts: list[dict[str, Any]]) -> str: return f"""

    难度不是官方标签,是预注册的操作长度分层

    WebArena‑Verified 只发布一个 258 题 hard subset,并没有 easy/medium/hard 三档。为了让首轮同时覆盖简单、中等和困难任务,本项目在模型运行前按“完成目标所需的最少可见 UI 操作数”预注册三档:1–4、5–7、8+。这衡量的是操作链长度,不等于推理难度;涉及跨页聚合、条件筛选或提交修改的任务即使点击少,也可能更难。

    {_table(['本项目分层','Chromium 首轮','Moli 首轮'], rows)}

    30 题配额为 5/10/15;其中个别本项目“简单”题也属于官方 hard subset,两种分类回答的是不同问题,报告保留两者而不互相覆盖。

    """ +def _failure_analysis(attempts: list[dict[str, Any]]) -> str: + primary_failures = { + int(row["task_id"]) + for row in attempts + if int(row["attempt"]) == 1 and float(row["score"]) == 0.0 + } + transport_rows = [row for row in attempts if row.get("model_transport_errors")] + transport_count = sum(len(row["model_transport_errors"]) for row in transport_rows) + if not primary_failures: + return "" + failure_rows = [ + [168, "项目筛选", "两种浏览器的主试与重试都返回空列表;官方答案非空。"], + [68, "多帖聚合", "四次执行都只返回一本书,遗漏其余合格帖子中的结果。"], + [124, "条件范围", "四次执行都返回 0.01–17774.32,取成了错误的全局价格范围。"], + [142, "跨订单汇总", "四次执行都返回 80.50,未完成目标月份与品类的完整汇总。"], + [506, "购物车最终状态", "主试虽到达结账或成功页,但官方网络事件检查发现购物车最终内容不符合唯一目标商品;重试又在 30 步上限前未完成。"], + ] + visible_rows = [row for row in failure_rows if row[0] in primary_failures] + transport_text = ( + f"另有 {transport_count} 次模型请求发生代理断连或超时,均只做一次同请求传输重试并恢复;它们分布在 " + + "、".join(f"{row['engine']} #{row['task_id']}" for row in transport_rows) + + "。这些事件保留在证据中,不改分,但会拉长对应试次耗时。" + if transport_rows + else "本轮没有模型传输重试。" + ) + return f"""

    五个共同失败首先指向 Agent,而不是某个浏览器

    两种浏览器在同五题失败,且十次失败诊断重试没有一次恢复。四个检索题的主试和重试还给出了跨浏览器一致的错误答案;购买题则由官方网络事件 evaluator 拒绝了不正确的购物车最终状态。这是“共同失败模式”的证据,不足以证明根因只在模型:同一 Agent、提示和 30 步上限也是共同变量。

    {_table(['任务','失败类型','证据中的表现'], visible_rows)}

    {_e(transport_text)}

    本轮没有逐题胜负差,因此不能估计内核对成功率的方向性效应。Moli 在个别长任务上出现更长的交互等待,Chromium 也有一次 180 秒模型代理超时;端到端耗时混合了模型、站点重置、页面等待和浏览器执行,不应当解释成纯内核基准。

    """ + + +def _release_notes() -> str: + changes = _read_json(Path("research/report-changes.json")) + releases = changes.get("browser_agent_releases") or [] + if not releases: + return "" + release = releases[0] + rows = release.get("changes") or [] + return f"""

    本报告发布记录

    {_e(release.get('date', '—'))} · {_e(release.get('title', '—'))}

    {_e(release.get('summary', '—'))}

    {_table(['变更','原因','本次处理','结果与边界'], rows)}
    展开发布证据绑定

    attempts SHA-256:{_e(release.get('attempts_sha256', '—'))}
    selection SHA-256:{_e(release.get('selection_sha256', '—'))}

    """ + + def _task_table(selection_path: Path, attempts: list[dict[str, Any]]) -> str: selection = load_pilot(selection_path) score = {(row["engine"], row["task_id"]): row for row in attempts if row["attempt"] == 1} @@ -292,9 +330,10 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path:

    Browser Use 官方现在更偏向什么

    要区分三种信号。Browser Use 产品首页强调开放的 100 题 BU Bench V1,同时展示 Odysseys 的 200 题长链排行榜成绩;其 benchmark 仓库当前把 71 题 Stealth Bench V1 放在最前。Stealth Bench 测的是反机器人检测与浏览器基础设施,不是 CowAgent 能否自主完成业务任务,因此不能代替本报告的 agent 评测。

    BU Bench 由 Custom、WebBench、Mind2Web 2、GAIA、BrowseComp 各 20 题组成,另有独立的 300 题 Online‑Mind2Web 运行仓库。最值得借鉴的是:运行前确认任务可完成、保存逐题 trace、支持断点续跑、把“不可完成”与 Agent 真失败分开。Odysseys 还把长任务拆成平均 6.1 个可检查 rubric,并同时报告完整成功率、部分进度和每步效率。

    首轮不直接照搬的原因也很具体:这些工作流都包含模型评审,BU Bench 还混入搜索与问答任务,Odysseys 又是实时多站长链任务。它们适合第二阶段测真实互联网泛化和长链能力,但会把网页漂移、judge 波动和浏览器内核差异同时混进结果。

    WebArena‑Verified 本地部署要不要账号付费

    四个首轮站点使用官方 Docker 镜像和内置测试账号,本地运行不需要向 GitLab、Reddit 或电商站点申请真实账号,也没有按题平台费用。成本来自本机 CPU、内存、磁盘和模型 API。

    本轮模型是 {_e(run_model)},通过 Responses API 调用,并固定 reasoning.effort=low。当前 LiteLLM Team 没有 gpt-6-astra 权限,因此 GPT‑5.6 运行只能证明流程和 Chromium/Moli 在该模型条件下的差异,不能替代 Astra 成绩。

    “本地可部署”也不等于轻量。本轮核对的官方 GitLab 镜像 manifest 含一个约 22.0 GB 的压缩主层,四个站点镜像合计约 91 GB;正式环境因此使用独立的 140 GiB Docker profile。Apple Silicon 上,三个含旧 Alpine/musl PHP‑FPM 的官方 amd64 镜像需要兼容层:Java、数据库及其余进程通过 Rosetta,PHP‑FPM 单独通过挂载的 QEMU 运行。该适配不修改官方镜像、站点数据或 evaluator,并以配置哈希写入运行证据。Wikipedia 与 Map 还需要额外数据卷,所以首轮不纳入。自托管能消除网站内容漂移、封号和真实支付风险,但不会自动消除镜像下载、环境初始化、登录兼容、浏览器 CDP 实现差异或 evaluator 缺陷。

    本轮怎样保证是可比较的自主 Agent 实验

    {_table(['项目','预注册规则'], [['样本','固定 30 题:简单 5、中等 10、困难 15;运行后不换题,不重复抽样'],['Agent 与模型',f'CowAgent 真实自主观察、规划和调用 BrowserTool;{run_model},reasoning=low;每个试次清空模型会话'],['工具接口','沿用 CowAgent 的页面快照与 browser 函数工具,不调用模型原生 computer tool;测的是既有 CowAgent 接入方式下的自主 browser use'],['唯一变量','Agent、提示、模型、任务、站点数据和资源限额相同,只替换 Chromium / Moli'],['环境','每个试次都从官方镜像重置该题站点并等待应用连续稳定;Shopping、Shopping Admin、Reddit 使用官方头登录,GitLab 使用官方 UI 登录'],['顺序','逐题交替先跑 Chromium 或 Moli,减轻固定先后顺序偏差'],['主分','每个“题 × 浏览器”只跑一次,官方二元 pass@1;两边构成逐题配对'],['重试','任务首轮失败后最多重跑一次,恢复只作诊断;每次模型请求遇到传输错误也最多重发一次,所有请求尝试与错误都入证'],['留证','保存最终 JSON、CowAgent 事件、标准网络 HAR、可视 Playwright trace、官方 evaluator 明细、镜像 ID、代码 commit、token 与资源指标'],['排除','首轮不选 Wikipedia / Map;不选无法由官方确定性 evaluator 验收的外部实时站点任务']])}

    “操作步数层级”只决定覆盖结构,不参与得分。环境准备失败应标成基础设施错误并修复后重跑同一预注册试次,不能计作 Agent 失败,也不能借机换题。官方 evaluator 读取 HAR;可视 trace 只用于调试,不参与评分。由于工具接口不同,本轮结果不能直接代表 Astra 发布页中原生 computer-use harness 的成绩。

    -{_result_section(attempts)}{_tier_section(attempts)}{_task_table(selection_path, attempts)} +{_result_section(attempts)}{_tier_section(attempts)}{_failure_analysis(attempts)}{_task_table(selection_path, attempts)}

    两条轨道必须分开读

    {_table(['轨道','Agent 是否自主','控制变量','主指标','回答的问题'], [['浏览器内核微基准(原 30 题)','否;重放固定 BrowserTool 等价调用','同一动作序列,只替换浏览器','严格输出正确率、动作执行、资源与耗时','相同操作交给两个内核,谁更兼容/省资源?'],['WebArena‑Verified Agent 评测(本报告)','是;CowAgent 自主观察、规划和调用 BrowserTool','同一 Agent、模型、提示、站点快照与任务,只替换浏览器','官方首轮 pass@1;失败重试恢复率另列','浏览器差异会怎样影响完整自主任务成功率?']])}

    固定动作轨道不是“有问题的假 Agent”,它是有价值的内核微基准;问题只是旧命名把它说成了自主 browser use。两条轨道互补,但分数不能相加,也不能用一条替代另一条。

    公开证据与复现边界

    采用统计只计公开一手材料。未找到 DeepSeek 或 Kimi 的官方浏览器交互 benchmark,不代表其内部没有评测;Kimi 公布的 BrowseComp 属于 agentic search,本报告不把它算成 UI browser use。

    +{_release_notes()}
    """ report = output / "index.html" report.write_text(html_text, encoding="utf-8") diff --git a/research/report-changes.json b/research/report-changes.json index 31481b2c9..e8d313018 100644 --- a/research/report-changes.json +++ b/research/report-changes.json @@ -1,6 +1,23 @@ { "schema_version": 1, "history_scope": "本页从有完整审计依据的修订开始记录。更早的站点版本仍保留在 Artifact Hub,但没有逐项变更说明,故不补编历史。", + "browser_agent_releases": [ + { + "id": "2026-09-09-webarena-verified-pilot", + "date": "2026-09-09", + "title": "完成 WebArena-Verified 自主 Agent 双浏览器首轮对比", + "summary": "CowAgent 与 gpt-5.6-sol(reasoning=low)在预注册的 30 题上分别以 Chromium 和 Moli 完成 60 次主试;双方均通过 25 题,主分 83.3%,失败后的 10 次唯一重试均未恢复。70/70 个试次证据通过确定性复核,分歧与未决均为 0。", + "attempts_sha256": "517b613adcc40fc0ebe41af2ab2115d76f49faaba5378b426cc196b5b633e9c7", + "selection_sha256": "ba3c344df003dd4eba3a5d1ef97162ee98df2ed728f7c29c4188bc4247982d38", + "changes": [ + ["新增自主 Agent 轨道", "原 30 题是固定 BrowserTool 动作重放,只能回答内核兼容与执行问题。", "新增 CowAgent 自主观察、规划和操作的 WebArena-Verified 轨道;同题配对且只替换浏览器。", "保留原轨道并正名为浏览器内核微基准;两条轨道分开计分,不能相加。"], + ["公开 benchmark 选型", "模型厂商使用的 browser-use、computer-use、离线动作预测和 agentic search 口径混杂。", "按一手机构采用情况梳理 Mind2Web、WebArena、WebVoyager,并比较 Online-Mind2Web、BU Bench、Odysseys 等评测对象和 judge。", "首轮选择可自托管、确定性结果验收的 WebArena-Verified;实时网站和 LLM judge 留到后续泛化阶段。"], + ["完整正式运行", "需要同时覆盖短、中、长操作链,且避免运行后换题或挑结果。", "预注册 5/10/15 题操作长度分层;每个题×浏览器只以第一次正式执行计 pass@1,失败最多重试一次且只作诊断。", "Chromium 与 Moli 均为 25/30;共同失败 5 题、单边胜出 0 题,30 题 pilot 未观察到成功率差异。"], + ["证据与复核", "最终分数必须能回到模型回答、网络副作用和官方 evaluator,不能人工修分。", "保存回答、事件、HAR、Playwright trace 和 evaluator 明细;逐试次核对文件、哈希、分数、状态与模型请求尝试。", "70/70 个试次复核通过,记分分歧 0、未决 0;完整网络正文和登录凭据不公开。"], + ["Astra 可用性边界", "用户代理公开的模型列表不等于当前团队有调用权限。", "直接调用代理验证 gpt-6-astra,服务返回 team_model_access_denied;随后只用可用的 gpt-5.6-sol 完成流程。", "本轮是 GPT-5.6 条件下的正式对比,不能写成 Astra 成绩;Astra 获权后必须新建独立运行。"] + ] + } + ], "releases": [ { "id": "2026-09-08-seven-method-final-audit", diff --git a/scripts/package_browser_agent_artifact_hub.py b/scripts/package_browser_agent_artifact_hub.py index a58562290..b27c5d30a 100644 --- a/scripts/package_browser_agent_artifact_hub.py +++ b/scripts/package_browser_agent_artifact_hub.py @@ -7,7 +7,7 @@ import zipfile from pathlib import Path -from scripts.check_browser_agent_report import check +from check_browser_agent_report import check def main() -> None: diff --git a/scripts/publish_browser_agent_artifact_hub.py b/scripts/publish_browser_agent_artifact_hub.py index c9f9f69eb..ad700f310 100644 --- a/scripts/publish_browser_agent_artifact_hub.py +++ b/scripts/publish_browser_agent_artifact_hub.py @@ -72,7 +72,7 @@ def main() -> None: json.dumps({"policy": "public"}).encode(), "application/json", ) - if status != 200: + if not 200 <= status < 300: raise RuntimeError(raw.decode(errors="replace")) result["share_url"] = json.loads(raw)["url"] RECEIPT.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") From e4feb281d6f9b06300a441d01fd056748cfc46bb Mon Sep 17 00:00:00 2001 From: kevin Date: Wed, 9 Sep 2026 17:09:33 +0800 Subject: [PATCH 022/127] Fix browser agent request and runtime guards --- harness/browser_agent/model.py | 2 +- harness/browser_agent/runner.py | 2 +- tests/browser_agent/test_model.py | 23 +++++++++++++++++++++++ tests/browser_agent/test_runner.py | 13 +++++++++++++ 4 files changed, 38 insertions(+), 2 deletions(-) diff --git a/harness/browser_agent/model.py b/harness/browser_agent/model.py index 8fa71aa76..4aa48c3b8 100644 --- a/harness/browser_agent/model.py +++ b/harness/browser_agent/model.py @@ -213,12 +213,12 @@ def call_stream(self, request: Any) -> Iterable[dict[str, Any]]: json=payload, timeout=self.timeout_seconds, ) + response.raise_for_status() except requests.RequestException as exc: self.transport_errors.append( {"type": type(exc).__name__, "message": str(exc)[:500]} ) raise - response.raise_for_status() self.last_response = response.json() response_id = self.last_response.get("id") if not isinstance(response_id, str) or not response_id: diff --git a/harness/browser_agent/runner.py b/harness/browser_agent/runner.py index 9b633063d..b80bea925 100644 --- a/harness/browser_agent/runner.py +++ b/harness/browser_agent/runner.py @@ -317,7 +317,7 @@ def _ensure_site(site: WebArenaSite, *, reset: bool) -> None: container = docker.from_env().containers.get(manager.container_name) _wait_container_healthy(container) _wait_application_ready(site, manager.config.health_check_path) - if manager.config.setup is not None and site in AMD64_PHP_SITES: + if QEMU_X86_64_HOST_PATH in manager.config.volumes: _verify_arm_php_compatibility(manager.container_name) diff --git a/tests/browser_agent/test_model.py b/tests/browser_agent/test_model.py index 88f8af30f..2a95804f3 100644 --- a/tests/browser_agent/test_model.py +++ b/tests/browser_agent/test_model.py @@ -176,6 +176,29 @@ def post(self, _url, **_kwargs): assert model.transport_errors == [{"type": "Timeout", "message": "timed out"}] +def test_model_records_http_error_attempts() -> None: + requests = __import__("requests") + + class ErrorResponse: + def raise_for_status(self): + raise requests.HTTPError("503 Service Unavailable") + + class HttpErrorSession: + def post(self, _url, **_kwargs): + return ErrorResponse() + + model = OpenAIResponsesModel(api_key="test", session=HttpErrorSession()) + + with pytest.raises(requests.HTTPError): + list(model.call_stream(_Request([{"role": "user", "content": "open it"}]))) + + assert model.request_count == 0 + assert model.request_attempt_count == 1 + assert model.transport_errors == [ + {"type": "HTTPError", "message": "503 Service Unavailable"} + ] + + def test_model_access_preflight_checks_exact_model() -> None: model = OpenAIResponsesModel(api_key="test", session=_Session()) diff --git a/tests/browser_agent/test_runner.py b/tests/browser_agent/test_runner.py index 2625420de..ecdf7c2bb 100644 --- a/tests/browser_agent/test_runner.py +++ b/tests/browser_agent/test_runner.py @@ -3,6 +3,7 @@ from harness.browser_agent.runner import ( ENV_CTRL_READY_TIMEOUT_SECONDS, + QEMU_X86_64_HOST_PATH, SITE_AUTH_HEADERS, _allow_official_container_hostname, _configure_benchmark, @@ -181,6 +182,18 @@ def info(): assert manager.config.volumes == {} +def test_php_site_on_x86_keeps_official_default_config(monkeypatch) -> None: + class Client: + @staticmethod + def info(): + return {"Architecture": "x86_64"} + + monkeypatch.setattr("harness.browser_agent.runner.docker.from_env", lambda: Client()) + manager = _site_container_manager(WebArenaSite.SHOPPING) + + assert QEMU_X86_64_HOST_PATH not in manager.config.volumes + + def test_arm_runtime_compatibility_is_evidence_bound() -> None: compatibility = _site_runtime_compatibility("aarch64") From 703d8179bf4d61f431401cdff2b6322cc5c734b9 Mon Sep 17 00:00:00 2001 From: kevin Date: Wed, 9 Sep 2026 18:20:13 +0800 Subject: [PATCH 023/127] Restructure browser agent report --- harness/browser_agent/report.py | 224 +++++++++++++++--- research/report-changes.json | 1 + scripts/check_browser_agent_report.py | 34 ++- scripts/publish_browser_agent_artifact_hub.py | 94 +++++--- tests/browser_agent/test_report.py | 15 +- 5 files changed, 294 insertions(+), 74 deletions(-) diff --git a/harness/browser_agent/report.py b/harness/browser_agent/report.py index ffb112973..c6a3d3eb0 100644 --- a/harness/browser_agent/report.py +++ b/harness/browser_agent/report.py @@ -12,6 +12,8 @@ from pathlib import Path from typing import Any +from harness.web_fetch.report_style import STYLE as SHARED_STYLE + from .selection import load_pilot @@ -109,6 +111,71 @@ class Safe(str): pass +REPORT_JS = r""" +function theme(){ + const root=document.documentElement; + const next=root.dataset.theme==='dark'?'light':'dark'; + root.dataset.theme=next; + try{localStorage.setItem('browser-agent-theme',next)}catch(_error){} +} +document.addEventListener('DOMContentLoaded',()=>{ + let saved=''; + try{saved=localStorage.getItem('browser-agent-theme')||''}catch(_error){} + document.documentElement.dataset.theme=saved||'light'; +}); +""" + + +REPORT_STYLE = SHARED_STYLE + r""" +.top nav{gap:19px;flex-wrap:nowrap}.top nav a[aria-current="page"]{color:var(--ink);font-weight:800;text-decoration:underline;text-decoration-thickness:2px;text-underline-offset:8px}.top .theme-toggle{margin-left:auto} +.page-main{min-height:78vh}.hero.compact{padding-bottom:22px}.hero.compact h1{font-size:clamp(34px,4.5vw,52px)} +.prose{max-width:900px;font-size:16px;line-height:1.82}.prose p{margin:14px 0}.section>.wrap>p{max-width:920px}.section>.wrap>ul{max-width:920px} +main>section:not(.hero):not(.section){width:min(var(--max),calc(100% - 40px));margin:22px auto;padding:28px;background:var(--panel);border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow)} +.decision-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.decision-grid .card{min-height:190px}.decision-grid h3{margin:8px 0} +.rank-list{counter-reset:rank}.rank-item{counter-increment:rank;display:grid;grid-template-columns:54px minmax(0,1fr);gap:18px;padding:22px 0;border-top:1px solid var(--line)}.rank-item:before{content:counter(rank);font-size:34px;font-weight:850;color:var(--accent)}.rank-item h3{margin:4px 0 8px} +.result-hero{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px}.result-hero .card{border-top:3px solid var(--accent)}.result-hero .card:nth-child(2){border-color:var(--violet)}.result-hero .card:nth-child(3){border-color:var(--lime)}.result-hero .card:nth-child(4){border-color:var(--coral)} +.reading-path .card{grid-column:span 4}.reading-path h3{margin-top:0}.reading-path a{font-weight:750}.callout.good{border-left-color:var(--good);background:var(--success-bg)}.callout.warn{border-left-color:var(--warn);background:var(--callout)} +.fine{font-size:13px;color:var(--muted)}details{margin:16px 0}summary{cursor:pointer;font-weight:700;color:var(--accent)} +.table-wrap{margin:18px 0}.table-wrap table{min-width:760px}.table-wrap th{white-space:nowrap}.source-list li{margin:9px 0}.footer{border-top:1px solid var(--line)} +@media(max-width:900px){.decision-grid{grid-template-columns:1fr}.result-hero{grid-template-columns:repeat(2,minmax(0,1fr))}.reading-path .card{grid-column:span 6}.top nav{gap:15px}} +@media(max-width:650px){.wrap{width:calc(100% - 28px)}.top nav{padding:9px 0}.brand{margin-right:4px}.result-hero{grid-template-columns:1fr}.reading-path .card{grid-column:span 12}.rank-item{grid-template-columns:38px minmax(0,1fr)}.hero{padding-top:30px}} +@media print{.top{display:none!important}.page-main{min-height:0}.section{break-inside:auto}.card,.table-wrap{box-shadow:none}.hero{padding-top:0}} +""" + + +NAVIGATION = [ + ("index.html", "首页", "home"), + ("interpretation.html", "报告解读", "interpretation"), + ("changes.html", "变更记录", "changes"), + ("dataset.html", "评测集", "dataset"), + ("methodology.html", "评测方法", "methodology"), + ("results.html", "结果分析", "results"), + ("review.html", "核验记录", "review"), + ("methods.html", "方法介绍", "methods"), + ("reproduction.html", "环境与复现", "reproduction"), + ("accounts.html", "账号", "accounts"), +] + + +def _page(*, title: str, active: str, body: str) -> str: + links = "".join( + f'{_e(label)}' + for href, label, key in NAVIGATION + ) + return f"""{_e(title)} · Browser Use 评测
    {body}

    结论只适用于本轮预注册样本、Agent、模型与运行配置;展示调整不改变测量结果。

    """ + + +def _hero(eyebrow: str, title: str, lead: str) -> str: + return f'
    {_e(eyebrow)}

    {_e(title)}

    {_e(lead)}

    ' + + +def _section(title: str, body: str, *, section_id: str = "") -> str: + marker = f' id="{_e(section_id)}"' if section_id else "" + return f'

    {_e(title)}

    {body}
    ' + + def _result_section(attempts: list[dict[str, Any]]) -> str: summary = _primary_summary(attempts) paired = _paired_summary(attempts) @@ -216,7 +283,7 @@ def _environment_section(evidence: Path) -> str: manifest = _read_json(evidence / "manifest.json") run_config = _read_json(evidence / "run-config.json") if not manifest or not run_config: - return """

    实测环境与证据身份

    正式运行尚未形成 manifest;发布门槛会阻止这份草稿作为完整结果上线。

    """ + return """

    正式运行尚未形成 manifest;发布门槛会阻止这份草稿作为完整结果上线。

    """ protocol = manifest.get("protocol") or {} environment = manifest.get("environment") or {} versions = environment.get("browser_versions") or {} @@ -249,10 +316,10 @@ def _environment_section(evidence: Path) -> str: if site_rows else '

    站点镜像身份尚未记录。

    ' ) - return f"""

    本轮运行身份

    这些字段来自运行 manifest,用于限定复现边界,不参与评分。

    展开代码、镜像与校验身份{_table(['项目','记录值'], rows)}

    自托管站点镜像

    {sites}
    """ + return f"""

    这些字段来自运行 manifest,用于限定复现边界,不参与评分。

    展开代码、镜像与校验身份{_table(['项目','记录值'], rows)}

    自托管站点镜像

    {sites}
    """ -def _review_page(evidence: Path, css: str) -> str: +def _review_page(evidence: Path) -> str: review = _read_json(evidence / "result-review.json") rows: list[list[Any]] = [] if not review: @@ -291,14 +358,41 @@ def _review_page(evidence: Path, css: str) -> str: if rows else "

    尚无可列出的复核记录。

    " ) - return f"""Browser Agent 结果复核

    Result review

    每个试次都要与官方评分证据对得上

    这是一项确定性证据审计,不是第二套 evaluator,也不是 LLM-as-a-judge。官方 WebArena-Verified 分数仍是唯一成绩来源。

    复核结论

    {body}

    判定协议:任何缺失文件、无法解析的 JSON、回答不一致、分数或状态不一致、官方版本身份缺失,以及 evaluator/data checksum 不一致,都会保留为未解决项并阻止发布。不存在用人工意见覆盖官方 evaluator 的裁决路径。

    逐试次覆盖

    {coverage_table}
    """ + page_body = ( + _hero( + "Result review", + "每个试次都要与官方评分证据对得上", + "这是一项确定性证据审计,不是第二套 evaluator,也不是 LLM-as-a-judge。官方 WebArena-Verified 分数仍是唯一成绩来源。", + ) + + _section( + "复核结论", + body + + "

    判定协议:任何缺失文件、无法解析的 JSON、回答不一致、分数或状态不一致、官方版本身份缺失,以及 evaluator/data checksum 不一致,都会保留为未解决项并阻止发布。人工意见不能覆盖官方 evaluator。

    ", + ) + + _section("逐试次覆盖", coverage_table) + ) + return _page(title="核验记录", active="review", body=page_body) -def _reproduction_page(evidence: Path, css: str) -> str: - return f"""Browser Agent 复现说明

    Reproduction

    复现从仓库、预注册题集和固定运行配置开始

    代码、题集选择与运行入口以公开仓库为准;测量输出、登录 Cookie 和完整网络正文不进入 Git。

    唯一代码来源

    lexmount/browser-eval 保存评测代码、预注册的 30 题、依赖锁文件和报告生成器。官方 evaluator 由锁定的 webarena-verified 包提供,本项目不复制其判分逻辑。

    核心命令

    ./examples/browser_agent/run.sh
    +def _reproduction_page(evidence: Path) -> str:
    +    body = _hero(
    +        "Reproduction",
    +        "复现从仓库、预注册题集和固定运行配置开始",
    +        "代码、题集选择与运行入口以公开仓库为准;测量输出、登录 Cookie 和完整网络正文不进入 Git。",
    +    )
    +    body += _section(
    +        "唯一代码来源",
    +        '

    lexmount/browser-eval 保存评测代码、预注册的 30 题、依赖锁文件和报告生成器。官方 evaluator 由锁定的 webarena-verified 包提供,本项目不复制其判分逻辑。

    ', + ) + body += _section( + "核心命令", + """
    ./examples/browser_agent/run.sh
     make browser-agent-review EVIDENCE="$BROWSER_AGENT_EVIDENCE"
     make browser-agent-report EVIDENCE="$BROWSER_AGENT_EVIDENCE"
    -make browser-agent-report-check EVIDENCE="$BROWSER_AGENT_EVIDENCE"

    运行前需在仓库 .env 或显式进程环境配置目标模型可用的 API key、代理地址和 CowAgent checkout。Python 依赖只按 uv.lock 冻结安装。

    {_environment_section(evidence)}
    """ +make browser-agent-report-check EVIDENCE="$BROWSER_AGENT_EVIDENCE"

    运行前需在仓库 .env 或显式进程环境配置目标模型可用的 API key、代理地址和 CowAgent checkout。Python 依赖只按 uv.lock 冻结安装。

    """, + ) + body += _section("本轮运行身份", _environment_section(evidence)) + return _page(title="环境与复现", active="reproduction", body=body) def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: @@ -308,9 +402,6 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: run_model = str(protocol.get("model") or "gpt-5.6-sol") run_date = str(manifest.get("created_at") or "2026-09-09")[:10] output.mkdir(parents=True, exist_ok=True) - css = """ -:root{--ink:#17202a;--muted:#667085;--paper:#fbfaf7;--card:#fff;--line:#dedbd2;--accent:#155eef;--soft:#eef4ff;--good:#eaf8ef;--warn:#fff6df}*{box-sizing:border-box}html,body{overflow-x:hidden}body{margin:0;background:var(--paper);color:var(--ink);font:16px/1.72 ui-sans-serif,-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}main{width:100%;max-width:1100px;margin:auto;padding:64px 28px 100px}header{border-bottom:1px solid var(--line);padding-bottom:42px;margin-bottom:42px}h1{font:700 clamp(38px,6vw,72px)/1.06 ui-serif,Georgia,serif;letter-spacing:-.035em;max-width:920px;margin:10px 0 22px}h2{font:700 30px/1.25 ui-serif,Georgia,serif;margin:0 0 16px}h1,h2,h3,p,li,summary{overflow-wrap:anywhere}h3{margin:28px 0 8px}section{background:var(--card);border:1px solid var(--line);border-radius:18px;padding:30px;margin:22px 0;box-shadow:0 8px 30px rgba(31,41,55,.035)}p{max-width:88ch}.lede{font-size:20px;color:#344054}.eyebrow{color:var(--accent);font-weight:750;letter-spacing:.08em;text-transform:uppercase;font-size:13px}.meta,.fine{color:var(--muted);font-size:14px}.callout{border-left:4px solid var(--accent);padding:13px 16px;background:var(--soft);border-radius:6px}.callout.good{background:var(--good)}.callout.warn{background:var(--warn);border-color:#d68a00}.table-wrap{max-width:100%;overflow:auto;margin:20px 0}table{border-collapse:collapse;width:100%;font-size:14px}th,td{text-align:left;vertical-align:top;border-bottom:1px solid var(--line);padding:11px 12px}th{background:#f5f3ee;white-space:nowrap}a{color:var(--accent)}code{background:#f1f2f4;padding:2px 5px;border-radius:5px}pre{overflow:auto;background:#171b24;color:#f7f7f5;border-radius:12px;padding:18px}pre code{background:transparent;padding:0}details{margin-top:18px}summary{cursor:pointer;font-weight:700}.rank{display:grid;grid-template-columns:48px minmax(0,1fr);gap:14px;padding:18px 0;border-top:1px solid var(--line)}.rank>div{min-width:0}.rank b{font:700 30px/1 Georgia,serif;color:var(--accent)}nav{display:flex;gap:16px;flex-wrap:wrap;margin-top:24px}nav a{text-decoration:none;font-size:14px}@media(max-width:650px){main{padding:36px 14px}section{padding:21px}h1{font-size:34px}.lede{font-size:18px}h2{font-size:25px}}@media print{body{background:#fff}main{max-width:none;padding:0}section{break-inside:avoid;box-shadow:none}nav{display:none}a{color:inherit;text-decoration:none}} -""" methodology = _table( ["Benchmark", "评什么", "验收对象", "判分方式", "主要问题"], [ @@ -323,24 +414,103 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ["Odysseys", "200 个真实互联网长链、多站任务", "最终产物、轨迹与平均 6.1 个任务 rubric", "按细粒度 rubric 做模型评审;报告 partial、perfect 与每步效率", "实时网页漂移,运行与评审成本高;更适合第二阶段长链能力"], ], ) - html_text = f"""Browser Use 公开评测选型与 WebArena‑Verified 首轮对比

    Browser Agent Evaluation · {_e(run_date)}

    Browser Use 公开评测怎么选,以及为什么首轮用 WebArena‑Verified

    结论先说:原有 30 题应保留,但它测的是固定动作下浏览器内核能否正确、快速执行,不是 CowAgent 的自主规划能力。新增轨道用真实 CowAgent + {_e(run_model)}(reasoning=low),在同一组 WebArena‑Verified 任务上只替换 Chromium/Moli,以官方程序化 evaluator 判最终结果。GPT‑5.6 是流程验证模型,后续 Astra 结果必须作为新的独立运行发布,不能混入本轮分数。

    -

    采用统计与决策

    公开采用次数 Top 3:Mind2Web、WebArena、WebVoyager

    统计单位是公开一手材料中采用过该家族的机构数,而不是论文引用量;同一机构重复发布只计一次。Mind2Web 家族为 5 家(OpenAI、Anthropic、Google DeepMind、THUDM / ChatGLM、Browser Use),WebArena 家族为 3 家(OpenAI、Anthropic、THUDM / ChatGLM),WebVoyager 为 2 家(OpenAI、Google DeepMind)。不同变体的成绩不能合并,这里的聚合只回答“有多少机构采用过这个家族”。

    1

    Mind2Web 家族 · 5 家

    采用最广,但家族内部差异最大:原版是离线动作预测,Online 版是真实网站 + WebJudge,Mind2Web 2 是长链研究任务 + Agent-as-a-Judge。适合作为第二阶段真实互联网泛化评测,不能把三者成绩合并。

    2

    WebArena 家族 · 3 家

    覆盖导航、检索和改写状态;自托管让 Chromium/Moli 面对同一份站点数据。首轮仍选它的 Verified 版本,因为它重新审计任务、答案和 evaluator,并使用确定性程序判分。这里是“实验适配性”胜过“采用数量”。

    3

    WebVoyager · 2 家

    经典视觉浏览器基准,优势是直观、接近用户网站;问题是实时网页和 LLM judge 同时引入波动,作为内核 A/B 首轮不如 WebArena‑Verified 稳定。

    为什么不是照着 Astra 发布页抄 benchmark:GPT‑6 Astra 的公开 computer-use 主表是 Agents’ Last Exam、OSWorld 2.0 和 ScreenSpot‑Pro;页面只给出 Mind2Web 的 1.9× 演示耗时对比,没有公开 WebArena‑Verified 分数。这些基准能说明模型的通用电脑操作能力,但 OSWorld/ScreenSpot 并不是纯 browser use,也不适合直接比较两个浏览器内核。

    -

    Benchmark taxonomy

    它们评最终结果,还是评过程?

    {methodology}

    WebArena‑Verified 的精确判分链

    官方 1.2.3 数据共有 812 题,812 题都配置 AgentResponseEvaluator,其中 663 题还配置 NetworkEventEvaluator。前者解析 Agent 最终 JSON,核对任务类型、状态和带类型的返回字段;后者从 Playwright trace 或 HAR 中提取请求、响应、Cookie 等事件,核对导航或状态改变是否实际发生。字段先按官方 schema 做 URL、字符串、数字、布尔、数组和对象规范化,再做结构比较;需要有序时保留顺序,允许的备选答案由任务定义声明。没有模糊子串放行,也没有 LLM judge。

    单题是二元分:该题配置的每一个 evaluator 都得到 1.0 才通过,任何一项失败或 evaluator 报错都记 0。因此它评的是最终结果与必要的可观察副作用,不是“按参考路径逐步打分”。Agent 可以走不同点击路径,只要完成的结果和必要事件符合任务契约即可。

    -

    Mind2Web 的 easy / medium / hard 要先纠正

    原始 Mind2Web 的官方拆分不是难度档,而是 cross-task、cross-website、cross-domain:测试任务是否见过同站任务、是否换了网站、是否换了整个领域。Online‑Mind2Web 官方也没有稳定的 easy/medium/hard 定义;有些第三方按参考动作长度再分档,那是第三方分析口径。Mind2Web 2 论文把任务 horizon 定义为 short(少于 10 次动作)、medium(10–50)、long(超过 50),仍不等同于 easy/medium/hard。

    所以本轮的 5/10/15 是我们自己的预注册操作长度层级。它只用于保证样本覆盖,不改写官方字段,也不把它当成跨论文可比较的难度标签。

    -

    Browser Use 官方现在更偏向什么

    要区分三种信号。Browser Use 产品首页强调开放的 100 题 BU Bench V1,同时展示 Odysseys 的 200 题长链排行榜成绩;其 benchmark 仓库当前把 71 题 Stealth Bench V1 放在最前。Stealth Bench 测的是反机器人检测与浏览器基础设施,不是 CowAgent 能否自主完成业务任务,因此不能代替本报告的 agent 评测。

    BU Bench 由 Custom、WebBench、Mind2Web 2、GAIA、BrowseComp 各 20 题组成,另有独立的 300 题 Online‑Mind2Web 运行仓库。最值得借鉴的是:运行前确认任务可完成、保存逐题 trace、支持断点续跑、把“不可完成”与 Agent 真失败分开。Odysseys 还把长任务拆成平均 6.1 个可检查 rubric,并同时报告完整成功率、部分进度和每步效率。

    首轮不直接照搬的原因也很具体:这些工作流都包含模型评审,BU Bench 还混入搜索与问答任务,Odysseys 又是实时多站长链任务。它们适合第二阶段测真实互联网泛化和长链能力,但会把网页漂移、judge 波动和浏览器内核差异同时混进结果。

    -

    WebArena‑Verified 本地部署要不要账号付费

    四个首轮站点使用官方 Docker 镜像和内置测试账号,本地运行不需要向 GitLab、Reddit 或电商站点申请真实账号,也没有按题平台费用。成本来自本机 CPU、内存、磁盘和模型 API。

    本轮模型是 {_e(run_model)},通过 Responses API 调用,并固定 reasoning.effort=low。当前 LiteLLM Team 没有 gpt-6-astra 权限,因此 GPT‑5.6 运行只能证明流程和 Chromium/Moli 在该模型条件下的差异,不能替代 Astra 成绩。

    “本地可部署”也不等于轻量。本轮核对的官方 GitLab 镜像 manifest 含一个约 22.0 GB 的压缩主层,四个站点镜像合计约 91 GB;正式环境因此使用独立的 140 GiB Docker profile。Apple Silicon 上,三个含旧 Alpine/musl PHP‑FPM 的官方 amd64 镜像需要兼容层:Java、数据库及其余进程通过 Rosetta,PHP‑FPM 单独通过挂载的 QEMU 运行。该适配不修改官方镜像、站点数据或 evaluator,并以配置哈希写入运行证据。Wikipedia 与 Map 还需要额外数据卷,所以首轮不纳入。自托管能消除网站内容漂移、封号和真实支付风险,但不会自动消除镜像下载、环境初始化、登录兼容、浏览器 CDP 实现差异或 evaluator 缺陷。

    -

    本轮怎样保证是可比较的自主 Agent 实验

    {_table(['项目','预注册规则'], [['样本','固定 30 题:简单 5、中等 10、困难 15;运行后不换题,不重复抽样'],['Agent 与模型',f'CowAgent 真实自主观察、规划和调用 BrowserTool;{run_model},reasoning=low;每个试次清空模型会话'],['工具接口','沿用 CowAgent 的页面快照与 browser 函数工具,不调用模型原生 computer tool;测的是既有 CowAgent 接入方式下的自主 browser use'],['唯一变量','Agent、提示、模型、任务、站点数据和资源限额相同,只替换 Chromium / Moli'],['环境','每个试次都从官方镜像重置该题站点并等待应用连续稳定;Shopping、Shopping Admin、Reddit 使用官方头登录,GitLab 使用官方 UI 登录'],['顺序','逐题交替先跑 Chromium 或 Moli,减轻固定先后顺序偏差'],['主分','每个“题 × 浏览器”只跑一次,官方二元 pass@1;两边构成逐题配对'],['重试','任务首轮失败后最多重跑一次,恢复只作诊断;每次模型请求遇到传输错误也最多重发一次,所有请求尝试与错误都入证'],['留证','保存最终 JSON、CowAgent 事件、标准网络 HAR、可视 Playwright trace、官方 evaluator 明细、镜像 ID、代码 commit、token 与资源指标'],['排除','首轮不选 Wikipedia / Map;不选无法由官方确定性 evaluator 验收的外部实时站点任务']])}

    “操作步数层级”只决定覆盖结构,不参与得分。环境准备失败应标成基础设施错误并修复后重跑同一预注册试次,不能计作 Agent 失败,也不能借机换题。官方 evaluator 读取 HAR;可视 trace 只用于调试,不参与评分。由于工具接口不同,本轮结果不能直接代表 Astra 发布页中原生 computer-use harness 的成绩。

    -{_result_section(attempts)}{_tier_section(attempts)}{_failure_analysis(attempts)}{_task_table(selection_path, attempts)} -

    两条轨道必须分开读

    {_table(['轨道','Agent 是否自主','控制变量','主指标','回答的问题'], [['浏览器内核微基准(原 30 题)','否;重放固定 BrowserTool 等价调用','同一动作序列,只替换浏览器','严格输出正确率、动作执行、资源与耗时','相同操作交给两个内核,谁更兼容/省资源?'],['WebArena‑Verified Agent 评测(本报告)','是;CowAgent 自主观察、规划和调用 BrowserTool','同一 Agent、模型、提示、站点快照与任务,只替换浏览器','官方首轮 pass@1;失败重试恢复率另列','浏览器差异会怎样影响完整自主任务成功率?']])}

    固定动作轨道不是“有问题的假 Agent”,它是有价值的内核微基准;问题只是旧命名把它说成了自主 browser use。两条轨道互补,但分数不能相加,也不能用一条替代另一条。

    -

    公开证据与复现边界

    采用统计只计公开一手材料。未找到 DeepSeek 或 Kimi 的官方浏览器交互 benchmark,不代表其内部没有评测;Kimi 公布的 BrowseComp 属于 agentic search,本报告不把它算成 UI browser use。

    -{_release_notes()} -
    """ - report = output / "index.html" - report.write_text(html_text, encoding="utf-8") - (output / "review.html").write_text(_review_page(evidence, css), encoding="utf-8") - (output / "reproduction.html").write_text( - _reproduction_page(evidence, css), encoding="utf-8" + summary = _primary_summary(attempts) + paired = _paired_summary(attempts) + chromium = summary["chromium"] + moli = summary["moli"] + home = _hero( + f"Browser Agent Evaluation · {run_date}", + "同一自主 Agent、同一模型,只替换浏览器", + f"CowAgent + {run_model}(reasoning=low)在预注册的 30 题 WebArena-Verified 样本上分别运行 Chromium 与 Moli;主分只使用第一次正式执行。", + ) + home += _section( + "先看结论", + f'
    Chromium 首轮
    {chromium["passed"]}/{chromium["completed"]}

    官方 pass@1 · {_pct(chromium["score"])}

    Moli 首轮
    {moli["passed"]}/{moli["completed"]}

    官方 pass@1 · {_pct(moli["score"])}

    逐题不一致
    {paired["chromium_only"] + paired["moli_only"]}

    {paired["paired"]} 个完整配对中的单边胜出

    失败重试恢复
    {chromium["recovered"] + moli["recovered"]}/{chromium["retry_count"] + moli["retry_count"]}

    只作诊断,不回写主分

    当前最小结论:这个 30 题 pilot 没有观察到 Chromium 与 Moli 的任务成功率差异。双方均通过 25 题,并在同 5 题失败。共同失败更像 Agent、模型、提示或步数上限的共同瓶颈,不能据此断言某个浏览器更强。

    ', + ) + home += _section( + "怎么读这份报告", + '

    先理解结论

    为什么没有差异仍然是有效结果,以及它能支持什么决策。

    读报告解读 →

    再核对口径

    公开 benchmark 的采用情况、验收对象和本轮筛选规则。

    看评测集 →
    ', + ) + + interpretation = _hero( + "Report interpretation", + "这轮结果证明流程可比,但还不能证明内核等价", + "零个不一致配对意味着本样本没有给出成功率方向;它缩小了下一轮该查什么,而不是结束评测。", + ) + interpretation += _section( + "30 题回答了什么", + '

    两种浏览器面对完全相同的任务、站点快照、Agent、模型和资源上限,各完成 30 次首轮执行。25 题共同通过,5 题共同失败,失败后的唯一一次重试也没有恢复。这个模式支持一个克制的判断:在当前样本里,浏览器替换没有改变最终任务成功率。

    它没有证明 Chromium 与 Moli 在所有网站、长链任务、延迟或资源使用上等价。样本只有 30 题,且端到端耗时混合了模型响应、站点重置、页面等待和浏览器执行。成功率没有差异时,继续用同一批题反复重跑的收益有限;下一轮更应该扩大可稳定验收的题量,并补充能区分内核的协议兼容与长页面交互案例。

    ', ) + interpretation += _section( + "为什么首轮选 WebArena-Verified", + '

    选择它不是因为采用次数最多,而是因为它最适合做浏览器 A/B:站点和数据可自托管,任务状态可以逐次重置,官方 evaluator 以结构化答案和必要网络副作用做确定性判分,不依赖 LLM judge,也不要求 Agent 重放参考点击路径。

    Mind2Web 家族采用更广,但原版主要评离线动作预测;Online-Mind2Web 和 WebVoyager 接近真实网站,却同时引入网页漂移与模型裁判。它们适合第二阶段测泛化,不适合作为第一组内核差异归因实验。

    ', + ) + interpretation += _section( + "下一轮决策", + '

    先扩大确定性样本,再引入实时网站。保持 CowAgent 和 pass@1 主分不变,把稳定可验收的 WebArena-Verified 题目扩到更大配对样本;另开 Online-Mind2Web / WebVoyager 泛化轨道,并把 LLM judge 与人工抽检结果单独报告。Astra 获权后新建独立运行,不能与本轮 GPT-5.6 成绩混合。

    ', + ) + + dataset = _hero("Dataset", "公开 benchmark 怎样选", "先区分真实交互、离线动作预测和 agentic search,再看采用次数;不能把同一家族的不同变体当成同一成绩。") + dataset += _section( + "公开采用次数 Top 3", + '

    统计单位是公开一手材料中采用过该家族的机构数,同一机构重复发布只计一次。它衡量行业采用信号,不是 benchmark 质量分。

    Mind2Web 家族 · 5 家

    OpenAI、Anthropic、Google DeepMind、THUDM / ChatGLM、Browser Use。原版、Online 版与 Mind2Web 2 的任务和评分不同,成绩不能合并。

    WebArena 家族 · 3 家

    OpenAI、Anthropic、THUDM / ChatGLM。自托管、可重置、结果验收,最适合本轮浏览器内核 A/B。

    WebVoyager · 2 家

    OpenAI、Google DeepMind。真实网站和视觉交互直观,但网页漂移与 LLM judge 会降低精确复现性。

    ', + ) + dataset += _section( + "Mind2Web 没有统一的 easy / medium / hard", + '

    原始 Mind2Web 的官方拆分是 cross-task、cross-website、cross-domain,衡量从见过同站任务到跨领域的泛化,不是难度。Online-Mind2Web 也没有稳定的三档定义。Mind2Web 2 用 short(少于 10 次动作)、medium(10–50)、long(超过 50)描述任务跨度,仍不等于 easy / medium / hard。

    本轮的简单 5 题、中等 10 题、困难 15 题是运行前固定的操作长度层级:1–4、5–7、8+ 次最少可见 UI 操作。它只保证覆盖结构,不参与评分,也不冒充官方标签。

    ', + ) + dataset += _task_table(selection_path, attempts) + + methodology_page = _hero("Methodology", "到底评最终结果,还是评过程", "不同 benchmark 的验收对象差别很大;本轮只把官方确定性最终结果作为主分。") + methodology_page += _section("七个候选 benchmark 的判分边界", methodology) + methodology_page += _section( + "WebArena-Verified 的精确判分链", + '

    官方 1.2.3 数据共有 812 题,全部配置 AgentResponseEvaluator,其中 663 题还配置 NetworkEventEvaluator。前者解析 Agent 最终 JSON,核对任务类型、状态和带类型字段;后者从 trace 或 HAR 读取请求、响应、Cookie 等事件,验证导航或状态改变是否真的发生。

    单题为二元分:所有已配置 evaluator 都返回 1.0 才通过。它检查最终结果和必要副作用,不检查是否复刻参考路径;本轮也没有 LLM-as-a-judge。

    ', + ) + methodology_page += _section( + "本轮预注册协议", + _table(['项目','规则'], [['样本','固定 30 题:简单 5、中等 10、困难 15;运行后不换题'],['Agent 与模型',f'CowAgent 真实自主观察、规划和调用 BrowserTool;{run_model},reasoning=low'],['唯一变量','Agent、提示、模型、站点数据和资源限额相同,只替换 Chromium / Moli'],['主分','每个题×浏览器只跑一次,官方二元 pass@1;逐题配对'],['重试','首轮失败后最多重跑一次,只报告恢复诊断;模型传输错误同样最多一次重发并完整入证'],['排除','不纳入无法稳定验收的实时任务;首轮排除需要额外大型数据卷的 Wikipedia / Map']]) + '

    基础设施失败应修复后重跑同一预注册试次,不能记作 Agent 失败,也不能借机换题。Playwright trace 用于调试,官方 evaluator 读取最终回答和 HAR。

    ', + ) + + results = _hero("Results", "成功率相同,五个失败也完全重合", "结果页只展示由当前持久证据计算的首轮成绩、配对关系和失败诊断。") + results += _result_section(attempts) + _tier_section(attempts) + _failure_analysis(attempts) + + methods = _hero("Methods", "两条轨道回答不同问题", "固定动作重放和自主 Agent 都有价值;错误在于混用名称或合并分数。") + methods += _section( + "浏览器内核微基准与自主 Agent 评测", + _table(['轨道','Agent 是否自主','控制变量','主指标','回答的问题'], [['浏览器内核微基准(原 30 题)','否;重放固定 BrowserTool 等价调用','同一动作序列,只替换浏览器','严格输出正确率、动作执行、资源与耗时','相同操作交给两个内核,谁更兼容、资源更省?'],['WebArena-Verified Agent 评测(本报告)','是;CowAgent 自主观察、规划和调用 BrowserTool','同一 Agent、模型、提示、站点快照与任务,只替换浏览器','官方首轮 pass@1;失败重试恢复率另列','浏览器差异会怎样影响完整自主任务成功率?']]) + '

    原 30 题确实由 CowAgent 真实执行 BrowserTool,但任务动作是预先固定的,因此它测执行层,不测自主规划。把它正名为微基准不会降低其价值,也不应把它冒充完整 Agent benchmark。

    ', + ) + methods += _section( + "Browser Use 官方实践里可借鉴什么", + '

    Browser Use 同时使用 BU Bench V1、Odysseys、Online-Mind2Web 和 Stealth Bench。最值得复用的是运行前确认任务可完成、保存逐题 trace、支持断点续跑,并把不可完成与 Agent 真失败分开。Stealth Bench 评反机器人检测和浏览器基础设施,不能替代自主任务成功率评测。

    BU Bench 和 Odysseys 使用模型评审,且包含实时网站、搜索或长链任务。它们适合第二阶段,而不是当前需要清晰归因的最小对照。

    ', + ) + + accounts = _hero("Accounts", "本地站点不需要真实业务账号", "账号页只解释运行权限和费用边界,不展示任何密钥、Cookie 或个人信息。") + accounts += _section( + "站点、模型与费用", + _table(['范围','本轮做法','是否需要外部付费账号'], [['Shopping / Admin / Reddit / GitLab','官方 Docker 镜像与内置测试账号;每题前重置状态','不需要'],['模型 API',f'{run_model},Responses API,reasoning=low','需要有权调用该模型的 API 账号'],['GPT-6 Astra','已直接验证当前 LiteLLM Team;返回 team_model_access_denied','当前团队权限不足,未运行'],['Artifact Hub','只发布脱敏后的静态报告;令牌不进报告和 Git','发布者需已有站点权限']]) + '

    本地可部署不等于轻量:四个首轮站点镜像约 91 GB,正式环境使用独立 140 GiB Docker profile。Apple Silicon 对部分旧 amd64 PHP-FPM 镜像还需要受控兼容层。

    ', + ) + + sources = '

    采用统计只计公开一手材料。未找到 DeepSeek 或 Kimi 的官方 UI browser-use benchmark,不代表其内部没有评测;BrowseComp 属于 agentic search,本报告不把它算作浏览器 UI 操作基准。

    ' + methods += _section("公开依据", sources) + + changes = _hero("Release notes", "变更记录从可审计版本开始", "这里只写会改变读者理解、分母或复现边界的正式变更,不记录制作过程。") + _release_notes() + + pages = { + "index.html": _page(title="首页", active="home", body=home), + "interpretation.html": _page(title="报告解读", active="interpretation", body=interpretation), + "changes.html": _page(title="变更记录", active="changes", body=changes), + "dataset.html": _page(title="评测集", active="dataset", body=dataset), + "methodology.html": _page(title="评测方法", active="methodology", body=methodology_page), + "results.html": _page(title="结果分析", active="results", body=results), + "review.html": _review_page(evidence), + "methods.html": _page(title="方法介绍", active="methods", body=methods), + "reproduction.html": _reproduction_page(evidence), + "accounts.html": _page(title="账号", active="accounts", body=accounts), + } + for filename, text in pages.items(): + (output / filename).write_text(text, encoding="utf-8") + report = output / "index.html" review = _read_json(evidence / "result-review.json") audit = { "schema": "lex_browser_agent_report/1", diff --git a/research/report-changes.json b/research/report-changes.json index e8d313018..d9a33123a 100644 --- a/research/report-changes.json +++ b/research/report-changes.json @@ -10,6 +10,7 @@ "attempts_sha256": "517b613adcc40fc0ebe41af2ab2115d76f49faaba5378b426cc196b5b633e9c7", "selection_sha256": "ba3c344df003dd4eba3a5d1ef97162ee98df2ed728f7c29c4188bc4247982d38", "changes": [ + ["多页阅读结构", "一页版把选型、方法、结果、复核和复现连续堆叠,首页无法承担快速决策入口。", "按首页、报告解读、变更记录、评测集、评测方法、结果分析、核验记录、方法介绍、环境与复现、账号拆成十页,并提供统一导航与明暗主题。", "只改变信息架构和文字组织;任务、分母、原始输出、分数与证据绑定均未修改。"], ["新增自主 Agent 轨道", "原 30 题是固定 BrowserTool 动作重放,只能回答内核兼容与执行问题。", "新增 CowAgent 自主观察、规划和操作的 WebArena-Verified 轨道;同题配对且只替换浏览器。", "保留原轨道并正名为浏览器内核微基准;两条轨道分开计分,不能相加。"], ["公开 benchmark 选型", "模型厂商使用的 browser-use、computer-use、离线动作预测和 agentic search 口径混杂。", "按一手机构采用情况梳理 Mind2Web、WebArena、WebVoyager,并比较 Online-Mind2Web、BU Bench、Odysseys 等评测对象和 judge。", "首轮选择可自托管、确定性结果验收的 WebArena-Verified;实时网站和 LLM judge 留到后续泛化阶段。"], ["完整正式运行", "需要同时覆盖短、中、长操作链,且避免运行后换题或挑结果。", "预注册 5/10/15 题操作长度分层;每个题×浏览器只以第一次正式执行计 pass@1,失败最多重试一次且只作诊断。", "Chromium 与 Moli 均为 25/30;共同失败 5 题、单边胜出 0 题,30 题 pilot 未观察到成功率差异。"], diff --git a/scripts/check_browser_agent_report.py b/scripts/check_browser_agent_report.py index a654018e8..e5351dbd5 100644 --- a/scripts/check_browser_agent_report.py +++ b/scripts/check_browser_agent_report.py @@ -22,17 +22,26 @@ def check( *, require_complete: bool, ) -> None: - index = report_dir / "index.html" - review_page = report_dir / "review.html" - reproduction_page = report_dir / "reproduction.html" + page_names = ( + "index.html", + "interpretation.html", + "changes.html", + "dataset.html", + "methodology.html", + "results.html", + "review.html", + "methods.html", + "reproduction.html", + "accounts.html", + ) + pages = tuple(report_dir / name for name in page_names) + index = pages[0] audit_path = report_dir / "report-data.json" attempts_path = evidence_dir / "attempts.jsonl" manifest_path = evidence_dir / "manifest.json" run_config_path = evidence_dir / "run-config.json" - if not all(path.is_file() for path in (index, review_page, reproduction_page, audit_path)): - raise ValueError( - "report must contain index.html, review.html, reproduction.html, and report-data.json" - ) + if not all(path.is_file() for path in (*pages, audit_path)): + raise ValueError("report is missing one or more required report pages") selection = load_pilot(selection_path) audit = json.loads(audit_path.read_text(encoding="utf-8")) if audit.get("selection_sha256") != selection.sha256: @@ -67,9 +76,13 @@ def check( raise ValueError( f"retry is allowed only after a failed primary: {(engine, task_id)}" ) - html = index.read_text(encoding="utf-8") - if "expected_response" in html or "reference_answer" in html: + rendered_pages = {page: page.read_text(encoding="utf-8") for page in pages} + if any( + "expected_response" in text or "reference_answer" in text + for text in rendered_pages.values() + ): raise ValueError("private evaluator answers must not be published") + html = rendered_pages[index] if require_complete: if not manifest_path.is_file() or not run_config_path.is_file(): raise ValueError("complete evidence requires manifest.json and run-config.json") @@ -127,8 +140,7 @@ def check( report_review = audit.get("review") or {} if report_review.get("bindings") != bindings: raise ValueError("rendered report is not bound to the current result review") - for page in (index, review_page, reproduction_page): - text = page.read_text(encoding="utf-8") + for page, text in rendered_pages.items(): if str(evidence_dir.resolve()) in text: raise ValueError("published pages must not contain machine-local evidence paths") diff --git a/scripts/publish_browser_agent_artifact_hub.py b/scripts/publish_browser_agent_artifact_hub.py index ad700f310..5dcbafcf1 100644 --- a/scripts/publish_browser_agent_artifact_hub.py +++ b/scripts/publish_browser_agent_artifact_hub.py @@ -1,11 +1,12 @@ #!/usr/bin/env python3 -"""Publish the validated browser-agent report as a new Artifact Hub site.""" +"""Create or atomically update the browser-agent report on Artifact Hub.""" from __future__ import annotations import json import os import urllib.request +import urllib.parse import uuid from pathlib import Path @@ -16,56 +17,80 @@ RECEIPT = Path("publish/browser-agent-artifact-hub-receipt.json") -def request(path: str, data: bytes | None = None, content_type: str | None = None): +def request( + path: str, + data: bytes | None = None, + content_type: str | None = None, + *, + method: str | None = None, +): headers = { "Authorization": "Bearer " + load_token(), "User-Agent": "Browser-Agent-Report-Publisher/1.0", } if content_type: headers["Content-Type"] = content_type - req = urllib.request.Request(BASE + path, data=data, headers=headers) + req = urllib.request.Request(BASE + path, data=data, headers=headers, method=method) with urllib.request.urlopen(req, timeout=240) as response: return response.status, response.read() -def main() -> None: - if RECEIPT.exists(): - result = json.loads(RECEIPT.read_text(encoding="utf-8")) - else: - if not ARCHIVE.is_file(): - raise ValueError(f"missing validated package: {ARCHIVE}") - boundary = "----browseragent" + uuid.uuid4().hex - parts = [] - for key, value in ( - ("mode", "zip"), - ("title", "Browser Use 公开评测与 WebArena-Verified 对比"), - ): - parts.append( - ( - f'--{boundary}\r\nContent-Disposition: form-data; name="{key}"' - f"\r\n\r\n{value}\r\n" - ).encode() - ) +def multipart_archive() -> tuple[bytes, str]: + if not ARCHIVE.is_file(): + raise ValueError(f"missing validated package: {ARCHIVE}") + boundary = "----browseragent" + uuid.uuid4().hex + parts = [] + for key, value in ( + ("mode", "zip"), + ("title", "Browser Use 公开评测与 WebArena-Verified 对比"), + ): parts.append( ( - f'--{boundary}\r\nContent-Disposition: form-data; name="file"; ' - 'filename="browser-agent.zip"\r\nContent-Type: application/zip\r\n\r\n' + f'--{boundary}\r\nContent-Disposition: form-data; name="{key}"' + f"\r\n\r\n{value}\r\n" ).encode() - + ARCHIVE.read_bytes() - + b"\r\n" ) - parts.append(f"--{boundary}--\r\n".encode()) + parts.append( + ( + f'--{boundary}\r\nContent-Disposition: form-data; name="file"; ' + 'filename="browser-agent.zip"\r\nContent-Type: application/zip\r\n\r\n' + ).encode() + + ARCHIVE.read_bytes() + + b"\r\n" + ) + parts.append(f"--{boundary}--\r\n".encode()) + return b"".join(parts), "multipart/form-data; boundary=" + boundary + + +def save_receipt(result: dict) -> None: + safe = {key: value for key, value in result.items() if key not in {"editToken", "claimToken"}} + RECEIPT.parent.mkdir(parents=True, exist_ok=True) + RECEIPT.write_text(json.dumps(safe, ensure_ascii=False, indent=2), encoding="utf-8") + os.chmod(RECEIPT, 0o600) + + +def main() -> None: + payload, content_type = multipart_archive() + if RECEIPT.exists(): + result = json.loads(RECEIPT.read_text(encoding="utf-8")) + status, raw = request(f"/api/sites/{result['slug']}") + current = json.loads(raw) + version_id = (current.get("site") or {}).get("currentVersionId") + if status != 200 or not version_id: + raise RuntimeError("could not resolve the current Artifact Hub version") + query = urllib.parse.urlencode({"expected_version": version_id}) status, raw = request( - "/api/sites", - b"".join(parts), - "multipart/form-data; boundary=" + boundary, + f"/api/sites/{result['slug']}/versions?{query}", payload, content_type ) + updated = json.loads(raw) + if not 200 <= status < 300: + raise RuntimeError(updated) + result.update({key: value for key, value in updated.items() if key not in {"editToken", "claimToken"}}) + else: + status, raw = request("/api/sites", payload, content_type) result = json.loads(raw) if status != 200 or not result.get("slug"): raise RuntimeError(result) - RECEIPT.parent.mkdir(parents=True, exist_ok=True) - RECEIPT.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") - os.chmod(RECEIPT, 0o600) if not result.get("share_url"): status, raw = request( f"/api/sites/{result['slug']}/shares", @@ -75,8 +100,7 @@ def main() -> None: if not 200 <= status < 300: raise RuntimeError(raw.decode(errors="replace")) result["share_url"] = json.loads(raw)["url"] - RECEIPT.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8") - os.chmod(RECEIPT, 0o600) + save_receipt(result) public = urllib.request.Request( result["share_url"], headers={"User-Agent": "Browser-Agent-Report-Publisher/1.0"} ) @@ -89,7 +113,7 @@ def main() -> None: ) with urllib.request.urlopen(preview, timeout=60) as response: body = response.read().decode("utf-8", errors="replace") - if response.status != 200 or "Browser Use 公开评测怎么选" not in body: + if response.status != 200 or "同一自主 Agent、同一模型,只替换浏览器" not in body: raise RuntimeError("public preview is unavailable or missing the report heading") print(json.dumps({"slug": result["slug"], "url": result["share_url"]}, ensure_ascii=False)) diff --git a/tests/browser_agent/test_report.py b/tests/browser_agent/test_report.py index dbe6434ed..60bbcd129 100644 --- a/tests/browser_agent/test_report.py +++ b/tests/browser_agent/test_report.py @@ -131,6 +131,19 @@ def test_report_renders_measured_runtime_identities(tmp_path: Path) -> None: assert expected in text assert report.is_file() - assert (output / "review.html").is_file() + for name in ( + "interpretation.html", + "changes.html", + "dataset.html", + "methodology.html", + "results.html", + "review.html", + "methods.html", + "reproduction.html", + "accounts.html", + ): + page = output / name + assert page.is_file() + assert "切换主题" in page.read_text(encoding="utf-8") report_data = json.loads((output / "report-data.json").read_text(encoding="utf-8")) assert "evidence" not in report_data From b92aa967faea3ca6676603d71296506e0d5896fd Mon Sep 17 00:00:00 2001 From: kevin Date: Wed, 9 Sep 2026 18:26:35 +0800 Subject: [PATCH 024/127] Preserve Artifact Hub site identity --- scripts/publish_browser_agent_artifact_hub.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/scripts/publish_browser_agent_artifact_hub.py b/scripts/publish_browser_agent_artifact_hub.py index 5dcbafcf1..cf12fe262 100644 --- a/scripts/publish_browser_agent_artifact_hub.py +++ b/scripts/publish_browser_agent_artifact_hub.py @@ -91,6 +91,10 @@ def main() -> None: result = json.loads(raw) if status != 200 or not result.get("slug"): raise RuntimeError(result) + # Persist the remote identity before the separate share request. If that + # request fails, the next run must update this site rather than create + # an orphaned duplicate. + save_receipt(result) if not result.get("share_url"): status, raw = request( f"/api/sites/{result['slug']}/shares", From 1a358cd2031fd9629e85e0e0327ccd0c984b428d Mon Sep 17 00:00:00 2001 From: kevin Date: Wed, 9 Sep 2026 19:58:19 +0800 Subject: [PATCH 025/127] Rebuild browser agent report as evidence application --- .../claim-ledger.md | 13 + harness/browser_agent/report.py | 1324 +++++++++++------ research/report-changes.json | 4 +- scripts/check_browser_agent_report.py | 89 +- tests/browser_agent/test_report.py | 30 +- 5 files changed, 995 insertions(+), 465 deletions(-) create mode 100644 .ai/topics/browser-agent-report-rewrite/claim-ledger.md diff --git a/.ai/topics/browser-agent-report-rewrite/claim-ledger.md b/.ai/topics/browser-agent-report-rewrite/claim-ledger.md new file mode 100644 index 000000000..c07b12d9e --- /dev/null +++ b/.ai/topics/browser-agent-report-rewrite/claim-ledger.md @@ -0,0 +1,13 @@ +# Browser-agent report claim ledger + +This file is an editorial input, not part of the published report bundle. + +| Claim | Status | Evidence | Boundary | +|---|---|---|---| +| Chromium and Moli each passed 25/30 primary attempts. | measured | `attempts.jsonl`, primary attempts only | Applies to this 30-task cohort and fixed run configuration. | +| There were zero discordant primary pairs. | derived | paired join on task ID and engine | Does not establish browser equivalence. | +| The five primary failures were shared: 68, 124, 142, 168, 506. | derived | primary attempt score pairs | Shared variables include agent, model, prompt, step limit, and evaluator interface. | +| Ten permitted failure retries produced zero recoveries. | measured | attempt 2 records | Retry results are diagnostic and never replace pass@1. | +| All 70 recorded attempts passed deterministic evidence review. | measured | `result-review.json` | Review checks integrity and agreement; it is not a second evaluator. | +| WebArena-Verified is the best first benchmark for this browser A/B. | interpretation | deterministic result evaluation, resettable self-hosted sites | It is operationally heavy and does not cover open-web drift. | +| Next expansion should seek kernel-sensitive, deterministically verifiable tasks. | proposal | zero discordance plus shared failure pattern | Must remain a new pre-registered run; do not alter this cohort. | diff --git a/harness/browser_agent/report.py b/harness/browser_agent/report.py index c6a3d3eb0..665126ad4 100644 --- a/harness/browser_agent/report.py +++ b/harness/browser_agent/report.py @@ -1,4 +1,4 @@ -"""Publication-ready Chinese report for the autonomous browser-agent pilot.""" +"""Build the public, drill-down browser-agent evidence report.""" from __future__ import annotations @@ -7,49 +7,159 @@ import html import json import math +import shutil import statistics +import tempfile from collections import defaultdict +from collections.abc import Iterable from pathlib import Path from typing import Any from harness.web_fetch.report_style import STYLE as SHARED_STYLE -from .selection import load_pilot +from .selection import PilotSelection, load_pilot, validate_against_official + +ENGINE_LABELS = {"chromium": "Chromium", "moli": "Moli 1.1.3"} +TIER_LABELS = {"easy": "简单", "medium": "中等", "hard": "困难"} +SITE_LABELS = { + "shopping": "Shopping", + "shopping_admin": "Shopping Admin", + "reddit": "Reddit", + "gitlab": "GitLab", +} +MUTATE_IDS = {399, 404, 441, 506} +NAVIGATE_IDS = {44, 45, 118} +FAILURE_NOTES = { + 68: "多帖聚合不完整:四次执行都只返回一本书。", + 124: "把全局价格范围当成目标商品范围。", + 142: "未完成目标月份与品类的完整订单汇总。", + 168: "个人项目筛选返回空列表,官方答案非空。", + 506: "购物车最终状态不满足唯一目标商品;重试也未在步数上限内完成。", +} def _e(value: Any) -> str: return html.escape(str(value), quote=True) -def _read_attempts(evidence: Path) -> list[dict[str, Any]]: - path = evidence / "attempts.jsonl" - if not path.exists(): - return [] - return [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line] +class Safe(str): + pass def _read_json(path: Path) -> dict[str, Any]: return json.loads(path.read_text(encoding="utf-8")) if path.is_file() else {} +def _read_attempts(evidence: Path) -> list[dict[str, Any]]: + path = evidence / "attempts.jsonl" + return ( + [json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line] + if path.is_file() + else [] + ) + + def _sha256(path: Path) -> str | None: return hashlib.sha256(path.read_bytes()).hexdigest() if path.is_file() else None +def _pct(value: float | None) -> str: + return "—" if value is None else f"{100 * value:.1f}%" + + +def _seconds(value: float | None) -> str: + return "—" if value is None else f"{value / 1000:.1f} 秒" + + +def _number(value: float | int | None) -> str: + return "—" if value is None else f"{value:,.0f}" + + +def _table(headers: list[str], rows: Iterable[Iterable[Any]], *, caption: str = "") -> str: + head = "".join(f"{_e(x)}" for x in headers) + body = "".join( + "" + "".join(f"{x if isinstance(x, Safe) else _e(x)}" for x in row) + "" + for row in rows + ) + cap = f"{_e(caption)}" if caption else "" + return f'
    {cap}{head}{body}
    ' + + +def _task_type(task_id: int) -> str: + return ( + "MUTATE" if task_id in MUTATE_IDS else "NAVIGATE" if task_id in NAVIGATE_IDS else "RETRIEVE" + ) + + +def _attempt_id(row: dict[str, Any]) -> str: + return f"{row['engine']}-{int(row['task_id'])}-a{int(row['attempt'])}" + + +def _public_attempt(row: dict[str, Any]) -> dict[str, Any]: + usage, resource = row.get("model_usage") or {}, row.get("resource") or {} + return { + "id": _attempt_id(row), + "engine": str(row["engine"]), + "task_id": int(row["task_id"]), + "tier": str(row["tier"]), + "attempt": int(row["attempt"]), + "is_retry": bool(row.get("is_retry") or int(row["attempt"]) == 2), + "score": float(row["score"]), + "status": str(row.get("status") or ("success" if float(row["score"]) == 1 else "failure")), + "duration_ms": round(float(row.get("duration_ms") or 0), 3), + "model_usage": { + key: int(usage.get(key) or 0) + for key in ( + "input_tokens", + "output_tokens", + "reasoning_tokens", + "total_tokens", + "request_count", + "request_attempt_count", + ) + }, + "transport_error_count": len(row.get("model_transport_errors") or []), + "resource": { + key: round(float(resource[key]), 3) + for key in ( + "cpu_ms", + "network_rx_bytes", + "network_tx_bytes", + "memory_peak_bytes", + "memory_retained_bytes", + "pids_current_peak", + "cpu_average_utilization_pct", + ) + if resource.get(key) is not None + }, + } + + +def _percentile(values: list[float], q: float) -> float | None: + if not values: + return None + values = sorted(values) + index = (len(values) - 1) * q + lo, hi = math.floor(index), math.ceil(index) + return values[lo] if lo == hi else values[lo] + (values[hi] - values[lo]) * (index - lo) + + def _primary_summary(rows: list[dict[str, Any]]) -> dict[str, dict[str, Any]]: - result: dict[str, dict[str, Any]] = {} - for engine in ("chromium", "moli"): - primary = [row for row in rows if row["engine"] == engine and row["attempt"] == 1] - retries = [row for row in rows if row["engine"] == engine and row["attempt"] == 2] - durations = [float(row["duration_ms"]) for row in primary] - tokens = [int((row.get("model_usage") or {}).get("total_tokens") or 0) for row in primary] + result = {} + for engine in ENGINE_LABELS: + primary = [r for r in rows if r["engine"] == engine and int(r["attempt"]) == 1] + retries = [r for r in rows if r["engine"] == engine and int(r["attempt"]) == 2] + durations = [float(r.get("duration_ms") or 0) for r in primary] + tokens = [int((r.get("model_usage") or {}).get("total_tokens") or 0) for r in primary] + passed = sum(float(r["score"]) == 1 for r in primary) result[engine] = { "completed": len(primary), - "passed": sum(float(row["score"]) == 1.0 for row in primary), - "score": sum(float(row["score"]) for row in primary) / len(primary) if primary else None, + "passed": passed, + "score": passed / len(primary) if primary else None, "retry_count": len(retries), - "recovered": sum(float(row["score"]) == 1.0 for row in retries), - "median_seconds": statistics.median(durations) / 1000 if durations else None, + "recovered": sum(float(r["score"]) == 1 for r in retries), + "median_duration_ms": statistics.median(durations) if durations else None, + "p95_duration_ms": _percentile(durations, 0.95), "median_tokens": statistics.median(tokens) if tokens else None, } return result @@ -57,484 +167,828 @@ def _primary_summary(rows: list[dict[str, Any]]) -> dict[str, dict[str, Any]]: def _paired_summary(rows: list[dict[str, Any]]) -> dict[str, Any]: primary = { - (str(row["engine"]), int(row["task_id"])): float(row["score"]) == 1.0 - for row in rows - if int(row["attempt"]) == 1 + (str(r["engine"]), int(r["task_id"])): float(r["score"]) == 1 + for r in rows + if int(r["attempt"]) == 1 } - paired_ids = sorted( + ids = sorted( task_id - for task_id in {task_id for _, task_id in primary} + for task_id in {key[1] for key in primary} if ("chromium", task_id) in primary and ("moli", task_id) in primary ) counts = {"both_pass": 0, "chromium_only": 0, "moli_only": 0, "both_fail": 0} - for task_id in paired_ids: - chromium = primary[("chromium", task_id)] - moli = primary[("moli", task_id)] - key = ( - "both_pass" - if chromium and moli - else "chromium_only" - if chromium - else "moli_only" - if moli - else "both_fail" - ) - counts[key] += 1 + for task_id in ids: + c, m = primary[("chromium", task_id)], primary[("moli", task_id)] + counts[ + "both_pass" if c and m else "chromium_only" if c else "moli_only" if m else "both_fail" + ] += 1 discordant = counts["chromium_only"] + counts["moli_only"] - if discordant: - smaller = min(counts["chromium_only"], counts["moli_only"]) - tail = sum(math.comb(discordant, k) for k in range(smaller + 1)) / 2**discordant - p_value = min(1.0, 2 * tail) - else: - p_value = 1.0 if paired_ids else None - return {"paired": len(paired_ids), **counts, "mcnemar_exact_p": p_value} - - -def _pct(value: float | None) -> str: - return "—" if value is None else f"{value * 100:.1f}%" - - -def _seconds(value: float | None) -> str: - return "—" if value is None else f"{value:.1f} s" - - -def _table(headers: list[str], rows: list[list[Any]]) -> str: - head = "".join(f"{_e(item)}" for item in headers) - body = "".join( - "" + "".join(f"{item if isinstance(item, Safe) else _e(item)}" for item in row) + "" - for row in rows + p = ( + min( + 1.0, + 2 + * sum( + math.comb(discordant, k) + for k in range(min(counts["chromium_only"], counts["moli_only"]) + 1) + ) + / 2**discordant, + ) + if discordant + else (1.0 if ids else None) ) - return f'
    {head}{body}
    ' - - -class Safe(str): - pass + return {"paired": len(ids), **counts, "mcnemar_exact_p": p} + + +def _resolve_tasks(selection: PilotSelection) -> list[dict[str, Any]]: + try: + from .runner import _configure_benchmark + + tasks = validate_against_official(selection, _configure_benchmark()) + except Exception: + tasks = [ + { + "task_id": t.task_id, + "tier": t.tier, + "estimated_min_steps": t.estimated_min_steps, + "rationale": t.rationale, + "official_hard": t.official_hard, + "sites": ["unknown"], + "intent": t.rationale, + "intent_template_id": None, + "revision": None, + "start_urls": [], + } + for t in selection.tasks + ] + for task in tasks: + task["task_type"] = _task_type(int(task["task_id"])) + return tasks -REPORT_JS = r""" -function theme(){ - const root=document.documentElement; - const next=root.dataset.theme==='dark'?'light':'dark'; - root.dataset.theme=next; - try{localStorage.setItem('browser-agent-theme',next)}catch(_error){} -} -document.addEventListener('DOMContentLoaded',()=>{ - let saved=''; - try{saved=localStorage.getItem('browser-agent-theme')||''}catch(_error){} - document.documentElement.dataset.theme=saved||'light'; -}); -""" +def _group_rows( + attempts: list[dict[str, Any]], tasks: list[dict[str, Any]], field: str +) -> list[list[Any]]: + meta = {int(t["task_id"]): t for t in tasks} + grouped: dict[tuple[str, str], list[dict[str, Any]]] = defaultdict(list) + for row in attempts: + if int(row["attempt"]) == 1: + value = meta[int(row["task_id"])][field] + value = value[0] if isinstance(value, list) else value + grouped[(str(value), str(row["engine"]))].append(row) + result = [] + for value in sorted({key[0] for key in grouped}): + line: list[Any] = [SITE_LABELS.get(value, TIER_LABELS.get(value, value))] + for engine in ENGINE_LABELS: + items = grouped[(value, engine)] + line.append( + f"{sum(float(x['score']) == 1 for x in items)}/{len(items)}" if items else "—" + ) + result.append(line) + return result -REPORT_STYLE = SHARED_STYLE + r""" -.top nav{gap:19px;flex-wrap:nowrap}.top nav a[aria-current="page"]{color:var(--ink);font-weight:800;text-decoration:underline;text-decoration-thickness:2px;text-underline-offset:8px}.top .theme-toggle{margin-left:auto} -.page-main{min-height:78vh}.hero.compact{padding-bottom:22px}.hero.compact h1{font-size:clamp(34px,4.5vw,52px)} -.prose{max-width:900px;font-size:16px;line-height:1.82}.prose p{margin:14px 0}.section>.wrap>p{max-width:920px}.section>.wrap>ul{max-width:920px} -main>section:not(.hero):not(.section){width:min(var(--max),calc(100% - 40px));margin:22px auto;padding:28px;background:var(--panel);border:1px solid var(--line);border-radius:14px;box-shadow:var(--shadow)} -.decision-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.decision-grid .card{min-height:190px}.decision-grid h3{margin:8px 0} -.rank-list{counter-reset:rank}.rank-item{counter-increment:rank;display:grid;grid-template-columns:54px minmax(0,1fr);gap:18px;padding:22px 0;border-top:1px solid var(--line)}.rank-item:before{content:counter(rank);font-size:34px;font-weight:850;color:var(--accent)}.rank-item h3{margin:4px 0 8px} -.result-hero{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px}.result-hero .card{border-top:3px solid var(--accent)}.result-hero .card:nth-child(2){border-color:var(--violet)}.result-hero .card:nth-child(3){border-color:var(--lime)}.result-hero .card:nth-child(4){border-color:var(--coral)} -.reading-path .card{grid-column:span 4}.reading-path h3{margin-top:0}.reading-path a{font-weight:750}.callout.good{border-left-color:var(--good);background:var(--success-bg)}.callout.warn{border-left-color:var(--warn);background:var(--callout)} -.fine{font-size:13px;color:var(--muted)}details{margin:16px 0}summary{cursor:pointer;font-weight:700;color:var(--accent)} -.table-wrap{margin:18px 0}.table-wrap table{min-width:760px}.table-wrap th{white-space:nowrap}.source-list li{margin:9px 0}.footer{border-top:1px solid var(--line)} -@media(max-width:900px){.decision-grid{grid-template-columns:1fr}.result-hero{grid-template-columns:repeat(2,minmax(0,1fr))}.reading-path .card{grid-column:span 6}.top nav{gap:15px}} -@media(max-width:650px){.wrap{width:calc(100% - 28px)}.top nav{padding:9px 0}.brand{margin-right:4px}.result-hero{grid-template-columns:1fr}.reading-path .card{grid-column:span 12}.rank-item{grid-template-columns:38px minmax(0,1fr)}.hero{padding-top:30px}} -@media print{.top{display:none!important}.page-main{min-height:0}.section{break-inside:auto}.card,.table-wrap{box-shadow:none}.hero{padding-top:0}} +REPORT_JS = """function theme(){const r=document.documentElement,n=r.dataset.theme==='dark'?'light':'dark';r.dataset.theme=n;try{localStorage.setItem('browser-agent-theme',n)}catch(e){}}function filterRows(){const f={tier:document.querySelector('[data-filter=tier]')?.value||'',site:document.querySelector('[data-filter=site]')?.value||'',kind:document.querySelector('[data-filter=kind]')?.value||'',result:document.querySelector('[data-filter=result]')?.value||''};document.querySelectorAll('tbody tr[data-tier]').forEach(r=>r.hidden=Object.entries(f).some(([k,v])=>v&&r.dataset[k]!==v));}document.addEventListener('DOMContentLoaded',()=>{try{document.documentElement.dataset.theme=localStorage.getItem('browser-agent-theme')||'light'}catch(e){}document.querySelectorAll('[data-filter]').forEach(x=>x.addEventListener('change',filterRows));filterRows();});""" +REPORT_STYLE = ( + SHARED_STYLE + + """ +body{overflow-x:hidden}.hero h1,.publication-article h1,.publication-article h2{overflow-wrap:anywhere;word-break:break-word} +.top nav{gap:18px;flex-wrap:nowrap}.top nav a[aria-current=page]{color:var(--ink);font-weight:800;text-decoration:underline;text-decoration-thickness:2px;text-underline-offset:8px}.top .theme-toggle{margin-left:auto}.page-main{min-height:78vh}.hero.compact{padding-bottom:22px}.hero.compact h1{font-size:clamp(34px,4.5vw,52px)}.fine{font-size:13px;color:var(--muted)}.result-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px}.result-grid .card{border-top:3px solid var(--accent)}.filters{display:flex;gap:10px;flex-wrap:wrap;margin:18px 0}.filters select{padding:9px 12px;border:1px solid var(--line);border-radius:9px;background:var(--panel);color:var(--ink)}.status-pass{color:var(--good);font-weight:750}.status-fail{color:var(--bad);font-weight:750}.flow{display:grid;grid-template-columns:repeat(6,1fr);gap:9px;list-style:none;padding:0}.flow li{padding:14px;border:1px solid var(--line);border-radius:11px;background:var(--panel);font-size:13px}.flow li+li:before{content:'→';color:var(--accent);margin-right:7px}.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:12px}.detail-grid .kv{min-height:92px}.page-links{display:flex;gap:10px;flex-wrap:wrap}.page-links a{border:1px solid var(--line);background:var(--panel);padding:8px 12px;border-radius:9px}.rank-list{counter-reset:rank}.rank-item{counter-increment:rank;display:grid;grid-template-columns:54px 1fr;gap:16px;padding:20px 0;border-top:1px solid var(--line)}.rank-item:before{content:counter(rank);font-size:34px;font-weight:850;color:var(--accent)}.rank-item h3{margin:4px 0}.article-entry{border-left:4px solid var(--accent)}.source-list li{margin:9px 0}.table-wrap{margin:18px 0}.table-wrap table{min-width:760px}.table-wrap caption{text-align:left;padding:10px 12px;color:var(--muted);caption-side:bottom}.footer{border-top:1px solid var(--line)}@media(max-width:900px){.result-grid{grid-template-columns:repeat(2,1fr)}.flow{grid-template-columns:repeat(3,1fr)}}@media(max-width:600px){.wrap{width:calc(100% - 28px)}.result-grid,.flow{grid-template-columns:1fr}.flow li+li:before{content:'↓';display:block}.hero{padding-top:28px}.hero.compact h1{font-size:30px;line-height:1.18}}@media print{.top{display:none!important}.card,.table-wrap{box-shadow:none}} """ - - -NAVIGATION = [ - ("index.html", "首页", "home"), - ("interpretation.html", "报告解读", "interpretation"), - ("changes.html", "变更记录", "changes"), - ("dataset.html", "评测集", "dataset"), - ("methodology.html", "评测方法", "methodology"), - ("results.html", "结果分析", "results"), - ("review.html", "核验记录", "review"), - ("methods.html", "方法介绍", "methods"), - ("reproduction.html", "环境与复现", "reproduction"), - ("accounts.html", "账号", "accounts"), +) +NAV = [ + ("", "首页", "home"), + ("guide/", "报告解读", "guide"), + ("changes/", "变更记录", "changes"), + ("datasets/", "评测集", "datasets"), + ("methodology/", "评测方法", "methodology"), + ("analysis/", "结果分析", "analysis"), + ("review/", "核验记录", "review"), + ("adoption/", "方法介绍", "adoption"), + ("reproduce/", "环境与复现", "reproduce"), + ("accounts/", "账号", "accounts"), ] -def _page(*, title: str, active: str, body: str) -> str: +def _page(*, title: str, active: str, body: str, depth: int = 0) -> str: + base = "../" * depth links = "".join( - f'{_e(label)}' - for href, label, key in NAVIGATION + + f">{_e(label)}" + for href, label, key in NAV ) - return f"""{_e(title)} · Browser Use 评测
    {body}

    结论只适用于本轮预注册样本、Agent、模型与运行配置;展示调整不改变测量结果。

    """ + return f'{_e(title)} · Browser Use 评测
    {body}

    结论只适用于本轮预注册样本、Agent、模型与运行配置;展示调整不改变测量结果。

    ' def _hero(eyebrow: str, title: str, lead: str) -> str: return f'
    {_e(eyebrow)}

    {_e(title)}

    {_e(lead)}

    ' -def _section(title: str, body: str, *, section_id: str = "") -> str: - marker = f' id="{_e(section_id)}"' if section_id else "" - return f'

    {_e(title)}

    {body}
    ' +def _section(title: str, body: str) -> str: + return f'

    {_e(title)}

    {body}
    ' -def _result_section(attempts: list[dict[str, Any]]) -> str: - summary = _primary_summary(attempts) - paired = _paired_summary(attempts) - rows = [] - for engine, label in (("chromium", "Chromium"), ("moli", "Moli 1.1.3")): - item = summary[engine] - rows.append( +def _status(row: dict[str, Any]) -> Safe: + passed = float(row["score"]) == 1 + return Safe( + f'{"通过" if passed else "失败"}' + ) + + +def _article_body( + paired: dict[str, Any], attempts: list[dict[str, Any]], tasks: list[dict[str, Any]] +) -> str: + failures = [[f"#{task_id}", FAILURE_NOTES[task_id]] for task_id in sorted(FAILURE_NOTES)] + return ( + '
    Browser Agent 实测解读

    25 比 25 不是“内核等价”:它真正暴露的是共同瓶颈

    同一 CowAgent 与同一模型替换浏览器后,30 个配对任务没有一个出现单边胜出。这个结果证明对照流程跑通,却没有为内核优劣提供方向性证据。

    测量口径:WebArena‑Verified 1.2.3;首轮 pass@1;失败最多一次诊断重试;官方确定性 evaluator;无 LLM judge。

    一、最重要的结果不是平分,而是零个不一致配对

    Chromium 与 Moli 都通过 25/30,首轮成功率都是 83.3%。逐题配对后,25 题共同通过、5 题共同失败,两个浏览器单边通过都是 0。精确 McNemar 双侧 p=1.000,但这里最有解释力的不是 p 值,而是检验没有拿到任何不一致样本。

    ' + + _table( + ["配对结果", "任务数", "可支持的判断"], [ - label, - f"{item['completed']}/30", - f"{item['passed']}/{item['completed']}" if item["completed"] else "—", - _pct(item["score"]), - f"{item['recovered']}/{item['retry_count']}" if item["retry_count"] else "—", - _seconds(item["median_seconds"]), - f"{item['median_tokens']:,.0f}" if item["median_tokens"] is not None else "—", - ] + ["两者都通过", paired["both_pass"], "当前组合可完成"], + ["仅 Chromium", paired["chromium_only"], "本轮未观察到"], + ["仅 Moli", paired["moli_only"], "本轮未观察到"], + ["两者都失败", paired["both_fail"], "优先排查共同链路"], + ], + caption="表 1|30 个首轮任务的逐题配对;重试不并入主分。", ) - complete = all(item["completed"] == 30 for item in summary.values()) - notice = ( - '

    测量完整。主分只使用每个浏览器每题的第一次正式执行;失败后的唯一一次重试只进入恢复诊断。

    ' - if complete - else '

    测量尚未完整。本页可以先审阅选型与方法,但在两种浏览器各完成 30 个首轮样本前,不发布比较结论。

    ' - ) - paired_text = ( - f"完整配对 {paired['paired']}/30 题:两者都通过 {paired['both_pass']},仅 Chromium 通过 {paired['chromium_only']},仅 Moli 通过 {paired['moli_only']},两者都失败 {paired['both_fail']};精确 McNemar 双侧 p={paired['mcnemar_exact_p']:.3f}。" - if paired["paired"] == 30 - else f"当前完整配对 {paired['paired']}/30 题;未完成前不做显著性解释。" + + "

    严谨表述只能是“本轮没有观察到成功率差异”,不能写成“两种内核等价”。30 题对小效应检出力有限;更关键的是,样本没有诱发内核分歧。

    二、五个共同失败指向检索聚合与最终状态,不是重试次数

    失败集中在四个信息聚合任务和一个购物车状态任务。十个失败试次各获得一次重试,恢复数仍为 0。继续增加重试只会提高成本并模糊 pass@1。

    " + + _table(["任务", "公开诊断"], failures, caption="表 2|不公开参考答案或 Agent 原始回答。") + + "

    共同失败不能单独证明模型是唯一根因,因为 CowAgent 提示、工具抽象和步数上限也是共同变量;但它足以改变下一步优先级:先检查跨页收集、去重、范围约束和提交后状态验证。

    三、简单、中等、困难都覆盖了,但“难度”只是操作长度

    WebArena‑Verified 没有官方 easy/medium/hard 三档。本项目在运行前按最少可见 UI 操作数注册为 1–4、5–7、8+ 三档,配额 5/10/15。它控制操作链覆盖,不等同于推理难度。

    " + + _table( + ["本项目分层", "Chromium", "Moli"], + _group_rows(attempts, tasks, "tier"), + caption="表 3|各层级首轮通过数/任务数。", + ) + + _table( + ["站点", "Chromium", "Moli"], + _group_rows(attempts, tasks, "sites"), + caption="表 4|四个可重置自托管站点。", + ) + + '

    四、选择 WebArena‑Verified 是为了归因,不是因为它最流行

    Mind2Web 家族采用面更广,但原版主要评离线动作预测;Online‑Mind2Web 和 WebVoyager贴近实时网页,却引入网页漂移和模型裁判。WebArena‑Verified 可以自托管、逐题重置,并用结构化最终答案和必要网络副作用做程序化验收,更适合“只替换浏览器”的 A/B。

    代价也明确:部署重,环境兼容本身会成为工程变量;官方 evaluator 不衡量路径效率,也不能覆盖开放互联网泛化。

    五、下一轮应主动寻找内核敏感样本

    保留 CowAgent、模型、reasoning=low、pass@1 和一次失败诊断重试,扩大稳定可验收的配对样本;新增长页面、上传下载、复杂编辑器、弹窗、多标签页等更可能诱发内核差异的任务。实时网站另开 Online‑Mind2Web / WebVoyager 泛化轨道,LLM judge 与人工抽检单列。

    决策边界

    本轮支持“流程可比、当前样本无方向性差异”;不支持“内核等价”“Moli 已全面替代 Chromium”或“端到端耗时就是纯内核速度”。Astra 获权后必须形成独立运行。

    ' ) - return f"""

    实测结果

    同一 Agent、同一模型,只替换浏览器

    {notice}{_table(['浏览器','首轮覆盖','首轮通过','官方主分','失败重试恢复','首轮耗时中位数','首轮 tokens 中位数'], rows)}

    {paired_text}

    官方分数来自未修改的 WebArena‑Verified 1.2.3 evaluator。它是结果验收,不要求 Agent 复刻某条参考点击路径。McNemar 检验只比较同一题上的不一致结果;30 题仍是 pilot,p 值不能替代效应大小或更大样本复验。执行轨迹、网络 trace、最终 JSON、模型 token 用量和 evaluator 明细保存在仓库外的持久证据目录;报告不公开登录 Cookie 或完整网络正文。

    """ -def _tier_section(attempts: list[dict[str, Any]]) -> str: - grouped: dict[tuple[str, str], list[dict[str, Any]]] = defaultdict(list) - for row in attempts: - if row["attempt"] == 1: - grouped[(row["engine"], row["tier"])].append(row) - rows: list[list[Any]] = [] - for tier, label in (("easy", "简单:1–4 步"), ("medium", "中等:5–7 步"), ("hard", "困难:8 步以上")): - line: list[Any] = [label] - for engine in ("chromium", "moli"): - items = grouped[(engine, tier)] - line.append(f"{sum(float(x['score']) == 1 for x in items)}/{len(items)}" if items else "—") - rows.append(line) - return f"""

    难度不是官方标签,是预注册的操作长度分层

    WebArena‑Verified 只发布一个 258 题 hard subset,并没有 easy/medium/hard 三档。为了让首轮同时覆盖简单、中等和困难任务,本项目在模型运行前按“完成目标所需的最少可见 UI 操作数”预注册三档:1–4、5–7、8+。这衡量的是操作链长度,不等于推理难度;涉及跨页聚合、条件筛选或提交修改的任务即使点击少,也可能更难。

    {_table(['本项目分层','Chromium 首轮','Moli 首轮'], rows)}

    30 题配额为 5/10/15;其中个别本项目“简单”题也属于官方 hard subset,两种分类回答的是不同问题,报告保留两者而不互相覆盖。

    """ - - -def _failure_analysis(attempts: list[dict[str, Any]]) -> str: - primary_failures = { - int(row["task_id"]) - for row in attempts - if int(row["attempt"]) == 1 and float(row["score"]) == 0.0 - } - transport_rows = [row for row in attempts if row.get("model_transport_errors")] - transport_count = sum(len(row["model_transport_errors"]) for row in transport_rows) - if not primary_failures: - return "" - failure_rows = [ - [168, "项目筛选", "两种浏览器的主试与重试都返回空列表;官方答案非空。"], - [68, "多帖聚合", "四次执行都只返回一本书,遗漏其余合格帖子中的结果。"], - [124, "条件范围", "四次执行都返回 0.01–17774.32,取成了错误的全局价格范围。"], - [142, "跨订单汇总", "四次执行都返回 80.50,未完成目标月份与品类的完整汇总。"], - [506, "购物车最终状态", "主试虽到达结账或成功页,但官方网络事件检查发现购物车最终内容不符合唯一目标商品;重试又在 30 步上限前未完成。"], - ] - visible_rows = [row for row in failure_rows if row[0] in primary_failures] - transport_text = ( - f"另有 {transport_count} 次模型请求发生代理断连或超时,均只做一次同请求传输重试并恢复;它们分布在 " - + "、".join(f"{row['engine']} #{row['task_id']}" for row in transport_rows) - + "。这些事件保留在证据中,不改分,但会拉长对应试次耗时。" - if transport_rows - else "本轮没有模型传输重试。" - ) - return f"""

    五个共同失败首先指向 Agent,而不是某个浏览器

    两种浏览器在同五题失败,且十次失败诊断重试没有一次恢复。四个检索题的主试和重试还给出了跨浏览器一致的错误答案;购买题则由官方网络事件 evaluator 拒绝了不正确的购物车最终状态。这是“共同失败模式”的证据,不足以证明根因只在模型:同一 Agent、提示和 30 步上限也是共同变量。

    {_table(['任务','失败类型','证据中的表现'], visible_rows)}

    {_e(transport_text)}

    本轮没有逐题胜负差,因此不能估计内核对成功率的方向性效应。Moli 在个别长任务上出现更长的交互等待,Chromium 也有一次 180 秒模型代理超时;端到端耗时混合了模型、站点重置、页面等待和浏览器执行,不应当解释成纯内核基准。

    """ +def _standalone_article(body: str) -> str: + return f'Browser Agent 实测解读
    {body}
    ' -def _release_notes() -> str: - changes = _read_json(Path("research/report-changes.json")) - releases = changes.get("browser_agent_releases") or [] - if not releases: - return "" - release = releases[0] - rows = release.get("changes") or [] - return f"""

    本报告发布记录

    {_e(release.get('date', '—'))} · {_e(release.get('title', '—'))}

    {_e(release.get('summary', '—'))}

    {_table(['变更','原因','本次处理','结果与边界'], rows)}
    展开发布证据绑定

    attempts SHA-256:{_e(release.get('attempts_sha256', '—'))}
    selection SHA-256:{_e(release.get('selection_sha256', '—'))}

    """ +def _filters() -> str: + return '
    ' -def _task_table(selection_path: Path, attempts: list[dict[str, Any]]) -> str: - selection = load_pilot(selection_path) - score = {(row["engine"], row["task_id"]): row for row in attempts if row["attempt"] == 1} +def _task_table(tasks: list[dict[str, Any]], attempts: list[dict[str, Any]]) -> str: + primary = {(r["engine"], int(r["task_id"])): r for r in attempts if int(r["attempt"]) == 1} rows = [] - for task in selection.tasks: - rows.append( - [ - task.task_id, - {"easy": "简单", "medium": "中等", "hard": "困难"}[task.tier], - task.estimated_min_steps, - "是" if task.official_hard else "否", - task.rationale, - _pct(float(score[("chromium", task.task_id)]["score"])) if ("chromium", task.task_id) in score else "—", - _pct(float(score[("moli", task.task_id)]["score"])) if ("moli", task.task_id) in score else "—", - ] - ) - return f"""

    预注册的 30 题

    选择文件在任何模型调用前固定,失败后不换题。样本含 23 个 RETRIEVE、4 个 MUTATE 和 3 个 NAVIGATE;按任务涉及站点计,Shopping Admin 12 题、Shopping 7 题、GitLab 6 题、Reddit 5 题。站点限定为这四个官方自托管环境;Wikipedia 与 Map 首轮排除,不是因为任务“太难”,而是它们分别要求大型 ZIM 和多套地图数据卷,会把环境下载与存储问题混进浏览器对比。

    {_table(['ID','本项目层级','估计最少步','官方 hard','入选理由','Chromium','Moli'], rows)}
    """ - - -def _environment_section(evidence: Path) -> str: - manifest = _read_json(evidence / "manifest.json") - run_config = _read_json(evidence / "run-config.json") - if not manifest or not run_config: - return """

    正式运行尚未形成 manifest;发布门槛会阻止这份草稿作为完整结果上线。

    """ - protocol = manifest.get("protocol") or {} - environment = manifest.get("environment") or {} - versions = environment.get("browser_versions") or {} - images = environment.get("browser_images") or {} - official = environment.get("official_evaluator") or {} - rows = [ - ["browser-eval", str(protocol.get("browser_eval_commit") or "—")], - ["CowAgent", str(protocol.get("cowagent_commit") or "—")], - ["模型", f"{protocol.get('model', '—')} · reasoning={protocol.get('reasoning_effort', '—')}"], - ["WebArena‑Verified", str(environment.get("webarena_verified_version") or "—")], - ["官方 evaluator checksum", str(official.get("evaluator_checksum") or "—")], - ["官方 data checksum", str(official.get("data_checksum") or "—")], - ["预注册选择集 SHA-256", str(run_config.get("selection_sha256") or "—")], - ["Docker", f"{environment.get('docker_architecture', '—')} · {environment.get('docker_server_version', '—')} · context={run_config.get('docker_context', '—')}"], - ] - for engine, label in (("chromium", "Chromium"), ("moli", "Moli")): - image = images.get(engine) or {} + for task in tasks: + task_id, site, kind = int(task["task_id"]), task["sites"][0], task["task_type"] + c, m = primary.get(("chromium", task_id)), primary.get(("moli", task_id)) + result = "pass" if c and m and float(c["score"]) == float(m["score"]) == 1 else "fail" + values = [ + Safe(f'#{task_id}'), + TIER_LABELS[task["tier"]], + SITE_LABELS.get(site, site), + kind, + task["estimated_min_steps"], + task["rationale"], + _status(c) if c else "—", + _status(m) if m else "—", + ] + cells = "".join(f"{x if isinstance(x, Safe) else _e(x)}" for x in values) rows.append( - [ - label, - f"{(versions.get(engine) or {}).get('Browser', '—')} · {image.get('architecture', '—')} · {str(image.get('id') or '—')}", - ] + f'{cells}' ) - site_rows = [] - for site, image in sorted((environment.get("site_images") or {}).items()): - identity = (image.get("repo_digests") or [image.get("id") or "—"])[0] - site_rows.append([site, image.get("reference") or "—", identity]) - sites = ( - _table(["站点", "官方镜像", "digest / image ID"], site_rows) - if site_rows - else '

    站点镜像身份尚未记录。

    ' + headers = "".join( + f"{_e(x)}" + for x in ["任务", "层级", "站点", "类型", "最少步", "入选理由", "Chromium", "Moli"] + ) + return ( + _filters() + + f'
    {headers}{"".join(rows)}
    ' ) - return f"""

    这些字段来自运行 manifest,用于限定复现边界,不参与评分。

    展开代码、镜像与校验身份{_table(['项目','记录值'], rows)}

    自托管站点镜像

    {sites}
    """ -def _review_page(evidence: Path) -> str: - review = _read_json(evidence / "result-review.json") - rows: list[list[Any]] = [] - if not review: - body = '

    复核尚未执行。正式发布要求逐试次证据审计覆盖全部实测记录,且没有未解决项。

    ' - else: - coverage = review.get("coverage") or {} - disagreements = review.get("disagreements") or [] - unresolved = review.get("unresolved") or [] - reviewer = review.get("reviewer") or {} - complete = ( - coverage.get("recorded_attempts") == coverage.get("reviewed_attempts") - and not unresolved - ) - state = "复核通过" if complete else "复核未通过" - klass = "good" if complete else "warn" - body = ( - f'

    {state}。' - f"已复核 {_e(coverage.get('reviewed_attempts', 0))}/{_e(coverage.get('recorded_attempts', 0))} 个试次;" - f"记分分歧 {_e(len(disagreements))},未解决 {_e(len(unresolved))}。

    " - f"

    复核者:{_e(reviewer.get('name', '—'))},{_e(reviewer.get('kind', '—'))}。" - "它检查证据文件是否完整、记录回答是否与落盘回答一致、主记录的分数和状态是否与官方 evaluator 输出一致、模型请求尝试是否完整入账,以及 evaluator/data checksum 是否统一。" - "它不重新判断任务成功与否,也不允许人工改分。

    " - ) - rows = [ - [ - item.get("engine", "—"), - item.get("task_id", "—"), - item.get("attempt", "—"), - "通过" if item.get("outcome") == "verified" else "未解决", - sum(not bool(value) for value in (item.get("checks") or {}).values()), - ] - for item in review.get("attempts") or [] +def _attempt_table(attempts: list[dict[str, Any]], tasks: list[dict[str, Any]]) -> str: + sites = {int(task["task_id"]): task["sites"][0] for task in tasks} + rows = [] + for row in attempts: + p = _public_attempt(row) + result = "pass" if p["score"] == 1 else "fail" + values = [ + Safe(f'{p["id"]}'), + ENGINE_LABELS[p["engine"]], + p["task_id"], + TIER_LABELS[p["tier"]], + "重试" if p["is_retry"] else "首轮", + _status(row), + _seconds(p["duration_ms"]), + _number(p["model_usage"]["total_tokens"]), + p["transport_error_count"], ] - coverage_table = ( - _table(["浏览器", "任务", "试次", "证据复核", "失败检查数"], rows) - if rows - else "

    尚无可列出的复核记录。

    " - ) - page_body = ( - _hero( - "Result review", - "每个试次都要与官方评分证据对得上", - "这是一项确定性证据审计,不是第二套 evaluator,也不是 LLM-as-a-judge。官方 WebArena-Verified 分数仍是唯一成绩来源。", - ) - + _section( - "复核结论", - body - + "

    判定协议:任何缺失文件、无法解析的 JSON、回答不一致、分数或状态不一致、官方版本身份缺失,以及 evaluator/data checksum 不一致,都会保留为未解决项并阻止发布。人工意见不能覆盖官方 evaluator。

    ", + cells = "".join(f"{x if isinstance(x, Safe) else _e(x)}" for x in values) + rows.append( + f'{cells}' ) - + _section("逐试次覆盖", coverage_table) + headers = "".join( + f"{_e(x)}" + for x in [ + "试次", + "浏览器", + "任务", + "层级", + "口径", + "结果", + "端到端耗时", + "Tokens", + "传输重试", + ] + ) + return ( + _filters() + + f'
    {headers}{"".join(rows)}
    ' ) - return _page(title="核验记录", active="review", body=page_body) -def _reproduction_page(evidence: Path) -> str: - body = _hero( - "Reproduction", - "复现从仓库、预注册题集和固定运行配置开始", - "代码、题集选择与运行入口以公开仓库为准;测量输出、登录 Cookie 和完整网络正文不进入 Git。", - ) - body += _section( - "唯一代码来源", - '

    lexmount/browser-eval 保存评测代码、预注册的 30 题、依赖锁文件和报告生成器。官方 evaluator 由锁定的 webarena-verified 包提供,本项目不复制其判分逻辑。

    ', - ) - body += _section( - "核心命令", - """
    ./examples/browser_agent/run.sh
    -make browser-agent-review EVIDENCE="$BROWSER_AGENT_EVIDENCE"
    -make browser-agent-report EVIDENCE="$BROWSER_AGENT_EVIDENCE"
    -make browser-agent-report-check EVIDENCE="$BROWSER_AGENT_EVIDENCE"

    运行前需在仓库 .env 或显式进程环境配置目标模型可用的 API key、代理地址和 CowAgent checkout。Python 依赖只按 uv.lock 冻结安装。

    """, - ) - body += _section("本轮运行身份", _environment_section(evidence)) - return _page(title="环境与复现", active="reproduction", body=body) +def _write_json(path: Path, value: Any) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: + selection = load_pilot(selection_path) + tasks = _resolve_tasks(selection) attempts = _read_attempts(evidence) - manifest = _read_json(evidence / "manifest.json") - protocol = manifest.get("protocol") or {} - run_model = str(protocol.get("model") or "gpt-5.6-sol") - run_date = str(manifest.get("created_at") or "2026-09-09")[:10] - output.mkdir(parents=True, exist_ok=True) - methodology = _table( - ["Benchmark", "评什么", "验收对象", "判分方式", "主要问题"], - [ - ["WebArena‑Verified", "自托管站点上的真实交互", "最终 JSON;需要时加网络事件", "确定性程序 evaluator;无 LLM judge;不比对固定动作序列", "部署重,站点状态必须逐次重置;仍可能有浏览器协议兼容问题"], - ["Online‑Mind2Web", "136 个真实网站的在线任务", "事实动作历史与最终完成情况", "官方建议 o4-mini WebJudge,并提供人工标签;属于 LLM-as-a-judge", "网页漂移、CAPTCHA、地区与账号差异;复现成本高"], - ["WebVoyager", "少量主流实时网站上的视觉交互", "最终回答与执行证据", "原始工作流使用多模态 LLM 自动评审并辅以人工核验", "judge 偏差和实时网站漂移,精确复现较弱"], - ["Mind2Web(2023)", "离线网页快照上的动作预测与泛化", "每一步目标元素与操作", "程序化 element accuracy、operation F1、step success、整任务 success", "主要评过程模仿,不证明今天能在真实网站完成任务"], - ["Mind2Web 2", "长链、多站点、时变的 agentic search", "带引用的研究答案", "树状 rubric 的 Agent-as-a-Judge;Partial Completion、Success、Pass@3", "更接近研究/搜索,不是浏览器 UI 内核对比的首选"], - ["BU Bench V1", "100 个混合来源的浏览器任务", "最终结果与运行轨迹", "官方仓库要求 Google judge LLM;二元分", "单一维护方、自定义题占比、judge 与云浏览器条件会影响可比性"], - ["Odysseys", "200 个真实互联网长链、多站任务", "最终产物、轨迹与平均 6.1 个任务 rubric", "按细粒度 rubric 做模型评审;报告 partial、perfect 与每步效率", "实时网页漂移,运行与评审成本高;更适合第二阶段长链能力"], - ], - ) - summary = _primary_summary(attempts) - paired = _paired_summary(attempts) - chromium = summary["chromium"] - moli = summary["moli"] - home = _hero( - f"Browser Agent Evaluation · {run_date}", - "同一自主 Agent、同一模型,只替换浏览器", - f"CowAgent + {run_model}(reasoning=low)在预注册的 30 题 WebArena-Verified 样本上分别运行 Chromium 与 Moli;主分只使用第一次正式执行。", + public_attempts = [_public_attempt(r) for r in attempts] + manifest, config, review = ( + _read_json(evidence / name) + for name in ("manifest.json", "run-config.json", "result-review.json") ) - home += _section( - "先看结论", - f'
    Chromium 首轮
    {chromium["passed"]}/{chromium["completed"]}

    官方 pass@1 · {_pct(chromium["score"])}

    Moli 首轮
    {moli["passed"]}/{moli["completed"]}

    官方 pass@1 · {_pct(moli["score"])}

    逐题不一致
    {paired["chromium_only"] + paired["moli_only"]}

    {paired["paired"]} 个完整配对中的单边胜出

    失败重试恢复
    {chromium["recovered"] + moli["recovered"]}/{chromium["retry_count"] + moli["retry_count"]}

    只作诊断,不回写主分

    当前最小结论:这个 30 题 pilot 没有观察到 Chromium 与 Moli 的任务成功率差异。双方均通过 25 题,并在同 5 题失败。共同失败更像 Agent、模型、提示或步数上限的共同瓶颈,不能据此断言某个浏览器更强。

    ', - ) - home += _section( - "怎么读这份报告", - '

    先理解结论

    为什么没有差异仍然是有效结果,以及它能支持什么决策。

    读报告解读 →

    再核对口径

    公开 benchmark 的采用情况、验收对象和本轮筛选规则。

    看评测集 →
    ', + summary, paired = _primary_summary(attempts), _paired_summary(attempts) + protocol, environment = manifest.get("protocol") or {}, manifest.get("environment") or {} + model, run_date = ( + str(protocol.get("model") or "未记录"), + str(manifest.get("created_at") or "")[:10] or "未记录", ) + parent = output.parent + parent.mkdir(parents=True, exist_ok=True) + stage = Path(tempfile.mkdtemp(prefix=f".{output.name}-", dir=parent)) + try: + for folder in ( + "assets", + "datasets", + "attempts", + "engines", + "guide", + "changes", + "methodology", + "analysis", + "review", + "adoption", + "reproduce", + "accounts", + "data", + ): + (stage / folder).mkdir() + (stage / "assets/style.css").write_text(REPORT_STYLE, encoding="utf-8") + (stage / "assets/report.js").write_text(REPORT_JS, encoding="utf-8") + article = _article_body(paired, attempts, tasks) + c, m = summary["chromium"], summary["moli"] + home = _hero( + f"Browser Agent Evaluation · {run_date}", + "同一自主 Agent、同一模型,只替换浏览器", + f"CowAgent + {model}(reasoning=low)在预注册的 30 题 WebArena‑Verified 样本上对比 Chromium 与 Moli。", + ) + home += _section( + "先看结论", + f'
    Chromium pass@1
    {c["passed"]}/{c["completed"]}

    {_pct(c["score"])}

    Moli pass@1
    {m["passed"]}/{m["completed"]}

    {_pct(m["score"])}

    单边胜出
    {paired["chromium_only"] + paired["moli_only"]}

    {paired["paired"]} 个完整配对

    失败重试恢复
    {c["recovered"] + m["recovered"]}/{c["retry_count"] + m["retry_count"]}

    不回写主分

    当前结论:本轮没有观察到浏览器成功率差异;这证明对照流程可比,不证明两个内核等价。五个共同失败更值得优先检查 Agent 的跨页聚合与最终状态验证。

    ', + ) + home += _section( + "从结论进入证据", + '
    ', + ) + home += _section( + "逐题配对比总分更重要", + _table( + ["配对结果", "任务数"], + [ + ["两者都通过", paired["both_pass"]], + ["仅 Chromium", paired["chromium_only"]], + ["仅 Moli", paired["moli_only"]], + ["两者都失败", paired["both_fail"]], + ], + caption="同一任务的两个首轮结果;重试不参与配对主分。", + ) + + '

    精确 McNemar 双侧 p=1.000。零个不一致配对意味着当前样本无法估计内核方向性效应。

    ', + ) + (stage / "index.html").write_text( + _page(title="首页", active="home", body=home), encoding="utf-8" + ) - interpretation = _hero( - "Report interpretation", - "这轮结果证明流程可比,但还不能证明内核等价", - "零个不一致配对意味着本样本没有给出成功率方向;它缩小了下一轮该查什么,而不是结束评测。", - ) - interpretation += _section( - "30 题回答了什么", - '

    两种浏览器面对完全相同的任务、站点快照、Agent、模型和资源上限,各完成 30 次首轮执行。25 题共同通过,5 题共同失败,失败后的唯一一次重试也没有恢复。这个模式支持一个克制的判断:在当前样本里,浏览器替换没有改变最终任务成功率。

    它没有证明 Chromium 与 Moli 在所有网站、长链任务、延迟或资源使用上等价。样本只有 30 题,且端到端耗时混合了模型响应、站点重置、页面等待和浏览器执行。成功率没有差异时,继续用同一批题反复重跑的收益有限;下一轮更应该扩大可稳定验收的题量,并补充能区分内核的协议兼容与长页面交互案例。

    ', - ) - interpretation += _section( - "为什么首轮选 WebArena-Verified", - '

    选择它不是因为采用次数最多,而是因为它最适合做浏览器 A/B:站点和数据可自托管,任务状态可以逐次重置,官方 evaluator 以结构化答案和必要网络副作用做确定性判分,不依赖 LLM judge,也不要求 Agent 重放参考点击路径。

    Mind2Web 家族采用更广,但原版主要评离线动作预测;Online-Mind2Web 和 WebVoyager 接近真实网站,却同时引入网页漂移与模型裁判。它们适合第二阶段测泛化,不适合作为第一组内核差异归因实验。

    ', - ) - interpretation += _section( - "下一轮决策", - '

    先扩大确定性样本,再引入实时网站。保持 CowAgent 和 pass@1 主分不变,把稳定可验收的 WebArena-Verified 题目扩到更大配对样本;另开 Online-Mind2Web / WebVoyager 泛化轨道,并把 LLM judge 与人工抽检结果单独报告。Astra 获权后新建独立运行,不能与本轮 GPT-5.6 成绩混合。

    ', - ) + guide = ( + _hero( + "Report interpretation", + "从 25 比 25 读出下一轮该怎么做", + "完整文章与首页共用同一份测量数据;另提供无 JavaScript 的独立导出版。", + ) + + _section( + "独立导出", + '', + ) + + f'
    {article}
    ' + ) + (stage / "guide/index.html").write_text( + _page(title="报告解读", active="guide", body=guide, depth=1), encoding="utf-8" + ) + (stage / "guide/article.html").write_text(_standalone_article(article), encoding="utf-8") + releases = ( + _read_json(Path("research/report-changes.json")).get("browser_agent_releases") or [] + ) + changes_rows = releases[0].get("changes") or [] if releases else [] + changes = _hero( + "Release notes", + "变更记录只写会影响理解和复现的正式变化", + "分数、分母与证据绑定没有因出版结构变化而改变。", + ) + _section( + "当前发布", + _table(["变更", "原因", "处理", "影响"], changes_rows) + if changes_rows + else "

    暂无发布记录。

    ", + ) + (stage / "changes/index.html").write_text( + _page(title="变更记录", active="changes", body=changes, depth=1), encoding="utf-8" + ) - dataset = _hero("Dataset", "公开 benchmark 怎样选", "先区分真实交互、离线动作预测和 agentic search,再看采用次数;不能把同一家族的不同变体当成同一成绩。") - dataset += _section( - "公开采用次数 Top 3", - '

    统计单位是公开一手材料中采用过该家族的机构数,同一机构重复发布只计一次。它衡量行业采用信号,不是 benchmark 质量分。

    Mind2Web 家族 · 5 家

    OpenAI、Anthropic、Google DeepMind、THUDM / ChatGLM、Browser Use。原版、Online 版与 Mind2Web 2 的任务和评分不同,成绩不能合并。

    WebArena 家族 · 3 家

    OpenAI、Anthropic、THUDM / ChatGLM。自托管、可重置、结果验收,最适合本轮浏览器内核 A/B。

    WebVoyager · 2 家

    OpenAI、Google DeepMind。真实网站和视觉交互直观,但网页漂移与 LLM judge 会降低精确复现性。

    ', - ) - dataset += _section( - "Mind2Web 没有统一的 easy / medium / hard", - '

    原始 Mind2Web 的官方拆分是 cross-task、cross-website、cross-domain,衡量从见过同站任务到跨领域的泛化,不是难度。Online-Mind2Web 也没有稳定的三档定义。Mind2Web 2 用 short(少于 10 次动作)、medium(10–50)、long(超过 50)描述任务跨度,仍不等于 easy / medium / hard。

    本轮的简单 5 题、中等 10 题、困难 15 题是运行前固定的操作长度层级:1–4、5–7、8+ 次最少可见 UI 操作。它只保证覆盖结构,不参与评分,也不冒充官方标签。

    ', - ) - dataset += _task_table(selection_path, attempts) + datasets = _hero( + "Dataset", + "候选 benchmark、选择依据与 30 个预注册任务", + "先区分最终结果、动作过程和模型裁判,再谈采用率;同一家族不同变体的成绩不能合并。", + ) + datasets += _section( + "公开采用信号 Top 3", + '

    Mind2Web 家族 · 5 家

    OpenAI、Anthropic、Google DeepMind、THUDM / ChatGLM、Browser Use。原版是离线动作预测,Online 与 2.0 的目标和判分不同。

    WebArena 家族 · 3 家

    OpenAI、Anthropic、THUDM / ChatGLM。自托管、可重置、结果验收,更适合浏览器 A/B。

    WebVoyager · 2 家

    OpenAI、Google DeepMind。真实网站直观,但网页漂移与 LLM judge 降低精确复现性。

    采用次数是公开一手材料中的机构数,不是质量分;未找到 DeepSeek、Kimi 公开的一手 UI browser-use benchmark,不等于内部没有评测。

    ', + ) + datasets += _section( + "Mind2Web 的拆分不是 easy / medium / hard", + "

    原始 Mind2Web 使用 cross-task、cross-website、cross-domain 衡量泛化;Online‑Mind2Web 没有统一三档;Mind2Web 2 的 short / medium / long 按动作跨度划分。本报告的简单 5、中等 10、困难 15 是预注册的操作长度层级。

    ", + ) + _section("30 个任务", _task_table(tasks, attempts)) + (stage / "datasets/index.html").write_text( + _page(title="评测集", active="datasets", body=datasets, depth=1), encoding="utf-8" + ) - methodology_page = _hero("Methodology", "到底评最终结果,还是评过程", "不同 benchmark 的验收对象差别很大;本轮只把官方确定性最终结果作为主分。") - methodology_page += _section("七个候选 benchmark 的判分边界", methodology) - methodology_page += _section( - "WebArena-Verified 的精确判分链", - '

    官方 1.2.3 数据共有 812 题,全部配置 AgentResponseEvaluator,其中 663 题还配置 NetworkEventEvaluator。前者解析 Agent 最终 JSON,核对任务类型、状态和带类型字段;后者从 trace 或 HAR 读取请求、响应、Cookie 等事件,验证导航或状态改变是否真的发生。

    单题为二元分:所有已配置 evaluator 都返回 1.0 才通过。它检查最终结果和必要副作用,不检查是否复刻参考路径;本轮也没有 LLM-as-a-judge。

    ', - ) - methodology_page += _section( - "本轮预注册协议", - _table(['项目','规则'], [['样本','固定 30 题:简单 5、中等 10、困难 15;运行后不换题'],['Agent 与模型',f'CowAgent 真实自主观察、规划和调用 BrowserTool;{run_model},reasoning=low'],['唯一变量','Agent、提示、模型、站点数据和资源限额相同,只替换 Chromium / Moli'],['主分','每个题×浏览器只跑一次,官方二元 pass@1;逐题配对'],['重试','首轮失败后最多重跑一次,只报告恢复诊断;模型传输错误同样最多一次重发并完整入证'],['排除','不纳入无法稳定验收的实时任务;首轮排除需要额外大型数据卷的 Wikipedia / Map']]) + '

    基础设施失败应修复后重跑同一预注册试次,不能记作 Agent 失败,也不能借机换题。Playwright trace 用于调试,官方 evaluator 读取最终回答和 HAR。

    ', - ) + by_attempt = {(r["engine"], int(r["task_id"]), int(r["attempt"])): r for r in attempts} + for task in tasks: + task_id, site = int(task["task_id"]), task["sites"][0] + task_rows = [] + for engine in ENGINE_LABELS: + for attempt_no in (1, 2): + row = by_attempt.get((engine, task_id, attempt_no)) + if row: + task_rows.append( + [ + ENGINE_LABELS[engine], + "首轮" if attempt_no == 1 else "失败诊断重试", + _status(row), + _seconds(float(row["duration_ms"])), + Safe(f'查看试次'), + ] + ) + detail = _hero( + f"Task #{task_id}", + task["rationale"], + "公开任务意图与测量结果;参考答案和完整 Agent 回答不发布。", + ) + detail += _section( + "任务身份", + f'
    站点{_e(SITE_LABELS.get(site, site))}
    本项目层级{_e(TIER_LABELS[task["tier"]])} · {_e(task["estimated_min_steps"])} 步起
    任务类型{_e(task["task_type"])}
    官方 hard subset{"是" if task["official_hard"] else "否"}

    公开任务意图

    {_e(task["intent"])}

    模板 ID {task.get("intent_template_id") or "—"} · revision {task.get("revision") or "—"}

    ', + ) + detail += _section( + "两种浏览器的全部正式试次", + _table(["浏览器", "口径", "官方结果", "端到端耗时", "证据"], task_rows) + + ( + f'

    共同失败诊断:{_e(FAILURE_NOTES[task_id])}

    ' + if task_id in FAILURE_NOTES + else "" + ), + ) + (stage / f"datasets/{task_id}.html").write_text( + _page(title=f"任务 {task_id}", active="datasets", body=detail, depth=1), + encoding="utf-8", + ) + + benchmarks = [ + [ + "WebArena‑Verified", + "自托管真实交互", + "最终 JSON + 必要网络副作用", + "程序化;无 LLM judge", + "部署重,最适合内核 A/B", + ], + ["Mind2Web", "离线网页快照", "每步目标元素与操作", "程序化动作指标", "评过程模仿"], + [ + "Online‑Mind2Web", + "136 个真实网站", + "动作历史与最终完成", + "WebJudge + 人工标签", + "漂移、CAPTCHA、judge 偏差", + ], + [ + "WebVoyager", + "实时网站视觉交互", + "最终回答与执行证据", + "多模态 LLM + 人工核验", + "精确复现较弱", + ], + [ + "Mind2Web 2", + "长链多站搜索", + "带引用研究答案", + "rubric Agent-as-a-Judge", + "非 UI 内核首选", + ], + [ + "BU Bench V1", + "100 个混合任务", + "最终结果与轨迹", + "Google judge LLM", + "云浏览器条件影响可比性", + ], + ["Odysseys", "200 个长链多站任务", "rubric 完成度", "模型评审", "成本高"], + ] + methodology = _hero( + "Methodology", + "评最终结果,不评参考点击路径", + "唯一成绩来源是未修改的 WebArena‑Verified 1.2.3 evaluator;确定性证据审计只验证记录一致性。", + ) + _section( + "候选 benchmark 的验收边界", + _table(["Benchmark", "评什么", "验收对象", "判分", "问题"], benchmarks), + ) + methodology += _section( + "本轮判分链", + '
    1. 预注册 30 题
    2. 逐题重置站点
    3. CowAgent 自主规划
    4. 只替换浏览器
    5. 官方 evaluator 验收
    6. 确定性证据复核

    官方数据 812 题全部配置 AgentResponseEvaluator,其中 663 题还配置 NetworkEventEvaluator。所有已配置 evaluator 都返回 1.0 才通过。它检查最终结果和必要副作用,不要求复刻参考动作序列。

    ', + ) + methodology += _section( + "固定协议", + _table( + ["项目", "规则"], + [ + ["Agent / 模型", f"CowAgent;{model};reasoning=low"], + ["唯一变量", "同题、同数据、同模型、同提示和资源上限,只替换浏览器"], + ["主分", "每题×浏览器第一次正式执行;二元 pass@1;逐题配对"], + ["重试", "首轮失败后最多一次,只做恢复诊断;模型传输失败同请求最多重发一次"], + [ + "排除", + "不纳入无法稳定验收的实时任务;首轮排除需大型数据卷的 Wikipedia / Map", + ], + ["裁判", "官方程序化 evaluator;无 LLM judge;人工不得改分"], + ], + ), + ) + (stage / "methodology/index.html").write_text( + _page(title="评测方法", active="methodology", body=methodology, depth=1), + encoding="utf-8", + ) - results = _hero("Results", "成功率相同,五个失败也完全重合", "结果页只展示由当前持久证据计算的首轮成绩、配对关系和失败诊断。") - results += _result_section(attempts) + _tier_section(attempts) + _failure_analysis(attempts) + analysis = _hero( + "Results", + "从总体配对下钻到每一次执行", + "公开必要测量字段,不公开参考答案、完整回答、Cookie、HAR 正文或机器本地路径。", + ) + analysis += _section( + "汇总", + _table( + [ + "浏览器", + "首轮通过", + "pass@1", + "失败重试恢复", + "耗时中位数", + "耗时 P95", + "Tokens 中位数", + ], + [ + [ + ENGINE_LABELS[e], + f"{summary[e]['passed']}/{summary[e]['completed']}", + _pct(summary[e]["score"]), + f"{summary[e]['recovered']}/{summary[e]['retry_count']}", + _seconds(summary[e]["median_duration_ms"]), + _seconds(summary[e]["p95_duration_ms"]), + _number(summary[e]["median_tokens"]), + ] + for e in ENGINE_LABELS + ], + ) + + '

    耗时包含模型响应、页面等待、站点重置与浏览器执行,不能解释为纯内核速度。

    ', + ) + _section("70 个试次", _attempt_table(attempts, tasks)) + (stage / "analysis/index.html").write_text( + _page(title="结果分析", active="analysis", body=analysis, depth=1), encoding="utf-8" + ) - methods = _hero("Methods", "两条轨道回答不同问题", "固定动作重放和自主 Agent 都有价值;错误在于混用名称或合并分数。") - methods += _section( - "浏览器内核微基准与自主 Agent 评测", - _table(['轨道','Agent 是否自主','控制变量','主指标','回答的问题'], [['浏览器内核微基准(原 30 题)','否;重放固定 BrowserTool 等价调用','同一动作序列,只替换浏览器','严格输出正确率、动作执行、资源与耗时','相同操作交给两个内核,谁更兼容、资源更省?'],['WebArena-Verified Agent 评测(本报告)','是;CowAgent 自主观察、规划和调用 BrowserTool','同一 Agent、模型、提示、站点快照与任务,只替换浏览器','官方首轮 pass@1;失败重试恢复率另列','浏览器差异会怎样影响完整自主任务成功率?']]) + '

    原 30 题确实由 CowAgent 真实执行 BrowserTool,但任务动作是预先固定的,因此它测执行层,不测自主规划。把它正名为微基准不会降低其价值,也不应把它冒充完整 Agent benchmark。

    ', - ) - methods += _section( - "Browser Use 官方实践里可借鉴什么", - '

    Browser Use 同时使用 BU Bench V1、Odysseys、Online-Mind2Web 和 Stealth Bench。最值得复用的是运行前确认任务可完成、保存逐题 trace、支持断点续跑,并把不可完成与 Agent 真失败分开。Stealth Bench 评反机器人检测和浏览器基础设施,不能替代自主任务成功率评测。

    BU Bench 和 Odysseys 使用模型评审,且包含实时网站、搜索或长链任务。它们适合第二阶段,而不是当前需要清晰归因的最小对照。

    ', - ) + review_by_key = { + (x["engine"], int(x["task_id"]), int(x["attempt"])): x + for x in review.get("attempts") or [] + } + for row, public in zip(attempts, public_attempts, strict=True): + item = review_by_key.get((public["engine"], public["task_id"], public["attempt"]), {}) + usage, resource, checks = ( + public["model_usage"], + public["resource"], + item.get("checks") or {}, + ) + detail = _hero( + public["id"], + f"{ENGINE_LABELS[public['engine']]} · Task #{public['task_id']} · {'重试' if public['is_retry'] else '首轮'}", + "公开评分、运行计量和证据完整性;敏感正文保留在非公开持久证据中。", + ) + detail += _section( + "结果与运行", + f'
    官方结果{_status(row)}
    端到端耗时{_seconds(public["duration_ms"])}
    模型 tokens{_number(usage["total_tokens"])}
    模型请求{usage["request_count"]} 次调用 / {usage["request_attempt_count"]} 次传输尝试
    传输错误{public["transport_error_count"]}
    证据复核{_e(item.get("outcome") or "未记录")}
    ', + ) + detail += _section( + "资源诊断", + _table(["指标", "值"], resource.items()) + + '

    整次 Agent 运行的容器计量只作诊断;不同进程模型下不直接比较内存或 PID。

    ', + ) + _section( + "证据完整性检查", + _table( + ["检查", "结果"], + [[key, "通过" if value else "未通过"] for key, value in checks.items()], + ) + + f'', + ) + (stage / f"attempts/{public['id']}.html").write_text( + _page(title=public["id"], active="analysis", body=detail, depth=1), encoding="utf-8" + ) - accounts = _hero("Accounts", "本地站点不需要真实业务账号", "账号页只解释运行权限和费用边界,不展示任何密钥、Cookie 或个人信息。") - accounts += _section( - "站点、模型与费用", - _table(['范围','本轮做法','是否需要外部付费账号'], [['Shopping / Admin / Reddit / GitLab','官方 Docker 镜像与内置测试账号;每题前重置状态','不需要'],['模型 API',f'{run_model},Responses API,reasoning=low','需要有权调用该模型的 API 账号'],['GPT-6 Astra','已直接验证当前 LiteLLM Team;返回 team_model_access_denied','当前团队权限不足,未运行'],['Artifact Hub','只发布脱敏后的静态报告;令牌不进报告和 Git','发布者需已有站点权限']]) + '

    本地可部署不等于轻量:四个首轮站点镜像约 91 GB,正式环境使用独立 140 GiB Docker profile。Apple Silicon 对部分旧 amd64 PHP-FPM 镜像还需要受控兼容层。

    ', - ) + coverage = review.get("coverage") or {} + review_rows = [ + [ + x["engine"], + x["task_id"], + x["attempt"], + x["outcome"], + sum(not v for v in (x.get("checks") or {}).values()), + ] + for x in review.get("attempts") or [] + ] + review_page = _hero( + "Result review", + "70/70 个试次均完成确定性证据复核", + "复核不重新判题,也不是 LLM-as-a-judge;官方 evaluator 仍是唯一成绩来源。", + ) + review_page += _section( + "核验结论", + f'
    记录试次
    {coverage.get("recorded_attempts", 0)}
    已复核
    {coverage.get("reviewed_attempts", 0)}
    记分分歧
    {len(review.get("disagreements") or [])}
    未决
    {len(review.get("unresolved") or [])}

    检查回答、HAR、Agent trace、事件和 evaluator 明细是否存在,并核对哈希、分数、状态、官方版本和模型传输尝试。

    ', + ) + _section( + "逐试次覆盖", _table(["浏览器", "任务", "试次", "结论", "失败检查"], review_rows) + ) + (stage / "review/index.html").write_text( + _page(title="核验记录", active="review", body=review_page, depth=1), encoding="utf-8" + ) - sources = '

    采用统计只计公开一手材料。未找到 DeepSeek 或 Kimi 的官方 UI browser-use benchmark,不代表其内部没有评测;BrowseComp 属于 agentic search,本报告不把它算作浏览器 UI 操作基准。

    ' - methods += _section("公开依据", sources) - - changes = _hero("Release notes", "变更记录从可审计版本开始", "这里只写会改变读者理解、分母或复现边界的正式变更,不记录制作过程。") + _release_notes() - - pages = { - "index.html": _page(title="首页", active="home", body=home), - "interpretation.html": _page(title="报告解读", active="interpretation", body=interpretation), - "changes.html": _page(title="变更记录", active="changes", body=changes), - "dataset.html": _page(title="评测集", active="dataset", body=dataset), - "methodology.html": _page(title="评测方法", active="methodology", body=methodology_page), - "results.html": _page(title="结果分析", active="results", body=results), - "review.html": _review_page(evidence), - "methods.html": _page(title="方法介绍", active="methods", body=methods), - "reproduction.html": _reproduction_page(evidence), - "accounts.html": _page(title="账号", active="accounts", body=accounts), - } - for filename, text in pages.items(): - (output / filename).write_text(text, encoding="utf-8") - report = output / "index.html" - review = _read_json(evidence / "result-review.json") - audit = { - "schema": "lex_browser_agent_report/1", - "attempt_count": len(attempts), - "summary": _primary_summary(attempts), - "paired_summary": _paired_summary(attempts), - "selection_sha256": load_pilot(selection_path).sha256, - "evidence_bindings": { - "attempts_sha256": _sha256(evidence / "attempts.jsonl"), - "manifest_sha256": _sha256(evidence / "manifest.json"), - }, - "review": { - "coverage": review.get("coverage"), - "disagreement_count": len(review.get("disagreements") or []), - "unresolved_count": len(review.get("unresolved") or []), - "bindings": review.get("bindings"), + adoption = _hero( + "Methods", + "两条轨道回答不同问题,不能混分", + "固定 BrowserTool 动作轨道与自主 Agent 轨道都有效,但控制变量和结论边界不同。", + ) + adoption += _section( + "仓库中的两个 harness", + _table( + ["轨道", "Agent 是否自主", "主指标", "回答的问题"], + [ + [ + "浏览器内核微基准", + "CowAgent 执行预先固定的 BrowserTool 动作", + "动作正确率、资源和耗时", + "相同动作交给两个内核,兼容性和成本怎样", + ], + [ + "WebArena‑Verified Agent 评测", + "CowAgent 自主观察、规划和操作", + "官方首轮 pass@1", + "替换浏览器如何影响完整任务成功率", + ], + ], + ) + + "

    本报告的 30 个 WebArena‑Verified 任务始终由 CowAgent 自主观察、规划和操作。表中的 browser_kernel 是仓库里另一套执行层 harness,不是这 30 个任务,也没有被拿来冒充自主 Agent 成绩。

    ", + ) + adoption += _section( + "Browser Use 官方实践", + "

    Browser Use 公开使用 BU Bench V1、Odysseys、Online‑Mind2Web 和 Stealth Bench。可借鉴的是运行前确认任务可完成、保存逐题 trace、断点续跑,并把站点不可完成与 Agent 真失败分开。Stealth Bench 测反机器人检测,不能替代自主任务成功率。

    ", + ) + adoption += _section( + "公开依据", + '', + ) + (stage / "adoption/index.html").write_text( + _page(title="方法介绍", active="adoption", body=adoption, depth=1), encoding="utf-8" + ) + + versions, images = ( + environment.get("browser_versions") or {}, + environment.get("browser_images") or {}, + ) + engine_rows = [] + for engine in ENGINE_LABELS: + version, image = versions.get(engine) or {}, images.get(engine) or {} + engine_rows.append( + [ + Safe(f'{ENGINE_LABELS[engine]}'), + version.get("Browser", "—"), + version.get("Protocol-Version", "—"), + image.get("architecture", "—"), + str(image.get("id") or "—"), + ] + ) + body = _hero( + "Engine", ENGINE_LABELS[engine], "本轮浏览器运行身份与汇总结果。" + ) + _section( + "运行身份", + _table( + ["字段", "值"], + [ + ["Browser", version.get("Browser", "—")], + ["DevTools Protocol", version.get("Protocol-Version", "—")], + ["架构", image.get("architecture", "—")], + ["镜像 ID", image.get("id", "—")], + ["首轮成绩", f"{summary[engine]['passed']}/{summary[engine]['completed']}"], + ["首轮耗时中位数", _seconds(summary[engine]["median_duration_ms"])], + ], + ) + + '

    版本与镜像身份用于限定复现边界,不参与评分。

    ', + ) + (stage / f"engines/{engine}.html").write_text( + _page(title=ENGINE_LABELS[engine], active="adoption", body=body, depth=1), + encoding="utf-8", + ) + + reproduce = _hero( + "Reproduction", + "复现从公开仓库、预注册题集和固定运行身份开始", + "公开包提供脱敏投影;模型回答、Cookie 和完整网络正文不进入 Git 或站点。", + ) + reproduce += _section( + "浏览器与代码身份", + _table(["浏览器", "版本", "协议", "架构", "镜像 ID"], engine_rows) + + _table( + ["项目", "值"], + [ + ["browser-eval commit", protocol.get("browser_eval_commit", "—")], + ["CowAgent commit", protocol.get("cowagent_commit", "—")], + ["模型", f"{model} · reasoning={protocol.get('reasoning_effort', '—')}"], + ["WebArena‑Verified", environment.get("webarena_verified_version", "—")], + ["运行配置选择集 SHA-256", config.get("selection_sha256", selection.sha256)], + ["当前选择集 SHA-256", selection.sha256], + [ + "evaluator checksum", + (environment.get("official_evaluator") or {}).get( + "evaluator_checksum", "—" + ), + ], + ], + ), + ) + reproduce += _section( + "公开复现入口", + '

    lexmount/browser-eval 是代码、题集和锁文件的唯一来源。

    ./examples/browser_agent/run.sh\nmake browser-agent-review EVIDENCE="$BROWSER_AGENT_EVIDENCE"\nmake browser-agent-report EVIDENCE="$BROWSER_AGENT_EVIDENCE"\nmake browser-agent-report-check EVIDENCE="$BROWSER_AGENT_EVIDENCE"
    ', + ) + (stage / "reproduce/index.html").write_text( + _page(title="环境与复现", active="reproduce", body=reproduce, depth=1), encoding="utf-8" + ) + accounts = _hero( + "Accounts", + "本地站点账号、模型权限与发布权限分开处理", + "这里只说明权限边界,不展示密钥、Cookie、用户名或个人信息。", + ) + _section( + "权限与费用", + _table( + ["范围", "本轮做法", "外部付费账号"], + [ + ["四个自托管站点", "官方 Docker 镜像与内置测试账号;每题前重置", "不需要"], + ["模型 API", f"{model},reasoning=low", "需要相应 API 权限"], + [ + "GPT‑6 Astra", + "代理实测返回 team_model_access_denied,未纳入成绩", + "当前团队无权限", + ], + ["Artifact Hub", "只发布脱敏静态报告", "发布者需已有站点权限"], + ], + ), + ) + (stage / "accounts/index.html").write_text( + _page(title="账号", active="accounts", body=accounts, depth=1), encoding="utf-8" + ) + + claims = { + "schema": "lex_browser_agent_interpretation/1", + "claims": [ + {"claim": "当前样本未观察到成功率差异", "basis": paired}, + { + "claim": "失败重试没有恢复", + "basis": { + e: { + "retry_count": summary[e]["retry_count"], + "recovered": summary[e]["recovered"], + } + for e in ENGINE_LABELS + }, + }, + {"claim": "五个失败完全重合", "basis": sorted(FAILURE_NOTES)}, + ], + "bound_attempts_sha256": _sha256(evidence / "attempts.jsonl"), + "selection_sha256": selection.sha256, } - if review - else None, - } - (output / "report-data.json").write_text( - json.dumps(audit, ensure_ascii=False, indent=2), encoding="utf-8" - ) - return report + _write_json(stage / "guide/interpretation-evidence.json", claims) + _write_json( + stage / "data/selection.json", + { + "schema": "lex_browser_agent_public_selection/1", + "selection_sha256": selection.sha256, + "tasks": tasks, + }, + ) + (stage / "data/results.jsonl").write_text( + "".join(json.dumps(row, ensure_ascii=False) + "\n" for row in public_attempts), + encoding="utf-8", + ) + audit = { + "schema": "lex_browser_agent_report/2", + "attempt_count": len(attempts), + "summary": summary, + "paired_summary": paired, + "selection_sha256": selection.sha256, + "evidence_bindings": { + "attempts_sha256": _sha256(evidence / "attempts.jsonl"), + "manifest_sha256": _sha256(evidence / "manifest.json"), + }, + "review": { + "coverage": review.get("coverage"), + "disagreement_count": len(review.get("disagreements") or []), + "unresolved_count": len(review.get("unresolved") or []), + "bindings": review.get("bindings"), + } + if review + else None, + } + _write_json(stage / "data/summary.json", audit) + _write_json(stage / "report-data.json", audit) + public_env = { + "schema": "lex_browser_agent_public_environment/1", + "protocol": { + key: protocol.get(key) + for key in ( + "agent", + "browser_eval_commit", + "cowagent_commit", + "model", + "reasoning_effort", + "primary_attempts_per_engine_task", + "failure_retries", + "model_transport_retries", + "official_score_uses", + "evaluator", + "environment_reset", + "engine_order", + "sites", + ) + }, + "environment": { + "platform": environment.get("platform"), + "docker_server_version": environment.get("docker_server_version"), + "docker_architecture": environment.get("docker_architecture"), + "webarena_verified_version": environment.get("webarena_verified_version"), + "browser_images": images, + "browser_versions": versions, + "site_images": environment.get("site_images"), + "official_evaluator": environment.get("official_evaluator"), + }, + } + _write_json(stage / "data/environment.json", public_env) + _write_json( + stage / "data/review.json", + { + key: review.get(key) + for key in ( + "schema", + "reviewer", + "reviewed_at", + "bindings", + "protocol", + "coverage", + "disagreements", + "unresolved", + "attempts", + ) + }, + ) + (stage / ".generated-report").write_text("browser-agent-report/2\n", encoding="utf-8") + if output.exists(): + shutil.rmtree(output) + stage.rename(output) + except Exception: + shutil.rmtree(stage, ignore_errors=True) + raise + return output / "index.html" def main() -> None: diff --git a/research/report-changes.json b/research/report-changes.json index d9a33123a..2d659ac5c 100644 --- a/research/report-changes.json +++ b/research/report-changes.json @@ -10,8 +10,8 @@ "attempts_sha256": "517b613adcc40fc0ebe41af2ab2115d76f49faaba5378b426cc196b5b633e9c7", "selection_sha256": "ba3c344df003dd4eba3a5d1ef97162ee98df2ed728f7c29c4188bc4247982d38", "changes": [ - ["多页阅读结构", "一页版把选型、方法、结果、复核和复现连续堆叠,首页无法承担快速决策入口。", "按首页、报告解读、变更记录、评测集、评测方法、结果分析、核验记录、方法介绍、环境与复现、账号拆成十页,并提供统一导航与明暗主题。", "只改变信息架构和文字组织;任务、分母、原始输出、分数与证据绑定均未修改。"], - ["新增自主 Agent 轨道", "原 30 题是固定 BrowserTool 动作重放,只能回答内核兼容与执行问题。", "新增 CowAgent 自主观察、规划和操作的 WebArena-Verified 轨道;同题配对且只替换浏览器。", "保留原轨道并正名为浏览器内核微基准;两条轨道分开计分,不能相加。"], + ["证据应用与独立解读", "只有栏目分页仍不足以核对任务选择、逐次执行、复核覆盖和结论依据。", "首页作为决策入口;另生成同源的完整解读与无 JavaScript 独立文章,并提供 30 个任务页、70 个试次页、双浏览器身份页、筛选表和五类脱敏下载数据。", "出版结构与公开证据粒度改变;任务、分母、原始输出、分数与证据绑定均未修改。"], + ["明确自主 Agent 轨道", "仓库同时存在执行层 browser_kernel harness 和自主 browser_agent harness,若统称为“原 30 题”会误读运行方式。", "本报告的 30 个 WebArena-Verified 任务明确由 CowAgent 自主观察、规划和操作;browser_kernel 作为另一条执行层微基准单独说明。", "本轮成绩只来自自主 Agent 轨道;两类 harness 不合并计分。"], ["公开 benchmark 选型", "模型厂商使用的 browser-use、computer-use、离线动作预测和 agentic search 口径混杂。", "按一手机构采用情况梳理 Mind2Web、WebArena、WebVoyager,并比较 Online-Mind2Web、BU Bench、Odysseys 等评测对象和 judge。", "首轮选择可自托管、确定性结果验收的 WebArena-Verified;实时网站和 LLM judge 留到后续泛化阶段。"], ["完整正式运行", "需要同时覆盖短、中、长操作链,且避免运行后换题或挑结果。", "预注册 5/10/15 题操作长度分层;每个题×浏览器只以第一次正式执行计 pass@1,失败最多重试一次且只作诊断。", "Chromium 与 Moli 均为 25/30;共同失败 5 题、单边胜出 0 题,30 题 pilot 未观察到成功率差异。"], ["证据与复核", "最终分数必须能回到模型回答、网络副作用和官方 evaluator,不能人工修分。", "保存回答、事件、HAR、Playwright trace 和 evaluator 明细;逐试次核对文件、哈希、分数、状态与模型请求尝试。", "70/70 个试次复核通过,记分分歧 0、未决 0;完整网络正文和登录凭据不公开。"], diff --git a/scripts/check_browser_agent_report.py b/scripts/check_browser_agent_report.py index e5351dbd5..2926feafc 100644 --- a/scripts/check_browser_agent_report.py +++ b/scripts/check_browser_agent_report.py @@ -24,15 +24,18 @@ def check( ) -> None: page_names = ( "index.html", - "interpretation.html", - "changes.html", - "dataset.html", - "methodology.html", - "results.html", - "review.html", - "methods.html", - "reproduction.html", - "accounts.html", + "guide/index.html", + "guide/article.html", + "changes/index.html", + "datasets/index.html", + "methodology/index.html", + "analysis/index.html", + "review/index.html", + "adoption/index.html", + "reproduce/index.html", + "accounts/index.html", + "engines/chromium.html", + "engines/moli.html", ) pages = tuple(report_dir / name for name in page_names) index = pages[0] @@ -40,7 +43,37 @@ def check( attempts_path = evidence_dir / "attempts.jsonl" manifest_path = evidence_dir / "manifest.json" run_config_path = evidence_dir / "run-config.json" - if not all(path.is_file() for path in (*pages, audit_path)): + task_pages = tuple( + report_dir / "datasets" / f"{task_id}.html" + for task_id in sorted(task.task_id for task in load_pilot(selection_path).tasks) + ) + attempt_pages = tuple( + report_dir + / "attempts" + / f"{row['engine']}-{int(row['task_id'])}-a{int(row['attempt'])}.html" + for row in ( + [ + json.loads(line) + for line in attempts_path.read_text(encoding="utf-8").splitlines() + if line.strip() + ] + if attempts_path.is_file() + else [] + ) + ) + data_files = tuple( + report_dir / "data" / name + for name in ( + "selection.json", + "results.jsonl", + "summary.json", + "environment.json", + "review.json", + ) + ) + if not all( + path.is_file() for path in (*pages, *task_pages, *attempt_pages, *data_files, audit_path) + ): raise ValueError("report is missing one or more required report pages") selection = load_pilot(selection_path) audit = json.loads(audit_path.read_text(encoding="utf-8")) @@ -55,9 +88,7 @@ def check( ] selected_ids = {task.task_id for task in selection.tasks} expected_primary = { - (engine, task_id, 1) - for engine in ("chromium", "moli") - for task_id in selected_ids + (engine, task_id, 1) for engine in ("chromium", "moli") for task_id in selected_ids } by_key: dict[tuple[str, int, int], dict] = {} for row in rows: @@ -76,13 +107,41 @@ def check( raise ValueError( f"retry is allowed only after a failed primary: {(engine, task_id)}" ) - rendered_pages = {page: page.read_text(encoding="utf-8") for page in pages} + rendered_pages = { + page: page.read_text(encoding="utf-8") for page in (*pages, *task_pages, *attempt_pages) + } if any( "expected_response" in text or "reference_answer" in text for text in rendered_pages.values() ): raise ValueError("private evaluator answers must not be published") html = rendered_pages[index] + standalone = (report_dir / "guide/article.html").read_text(encoding="utf-8") + if " None: output=output, selection_path=Path("evaluation/browser-agent/webarena-verified-pilot.json"), ) - text = (output / "reproduction.html").read_text(encoding="utf-8") + text = (output / "reproduce/index.html").read_text(encoding="utf-8") for expected in ( "abc123", "gpt-6-astra · reasoning=low", "eval-checksum", - "data-checksum", "selection-hash", "Chrome/151", "Moli/1.1.3", - "official/reddit@sha256:site", ): assert expected in text assert report.is_file() for name in ( - "interpretation.html", - "changes.html", - "dataset.html", - "methodology.html", - "results.html", - "review.html", - "methods.html", - "reproduction.html", - "accounts.html", + "guide/index.html", + "guide/article.html", + "changes/index.html", + "datasets/index.html", + "methodology/index.html", + "analysis/index.html", + "review/index.html", + "adoption/index.html", + "reproduce/index.html", + "accounts/index.html", + "engines/chromium.html", + "engines/moli.html", ): page = output / name assert page.is_file() - assert "切换主题" in page.read_text(encoding="utf-8") + if name != "guide/article.html": + assert "切换主题" in page.read_text(encoding="utf-8") + assert len(list((output / "datasets").glob("[0-9]*.html"))) == 30 + assert (output / "guide/article.html").read_text(encoding="utf-8").find(" Date: Wed, 9 Sep 2026 20:06:06 +0800 Subject: [PATCH 026/127] Honor Artifact Hub root paths in report pages --- harness/browser_agent/report.py | 20 ++++++++++---------- scripts/check_report_links.py | 11 ++++++++++- tests/test_report_links.py | 13 +++++++++++++ 3 files changed, 33 insertions(+), 11 deletions(-) diff --git a/harness/browser_agent/report.py b/harness/browser_agent/report.py index 665126ad4..d359d78f2 100644 --- a/harness/browser_agent/report.py +++ b/harness/browser_agent/report.py @@ -270,14 +270,14 @@ def _group_rows( def _page(*, title: str, active: str, body: str, depth: int = 0) -> str: - base = "../" * depth + local_base = "../" * depth or "./" links = "".join( - f'{_e(label)}" for href, label, key in NAV ) - return f'{_e(title)} · Browser Use 评测
    {body}

    结论只适用于本轮预注册样本、Agent、模型与运行配置;展示调整不改变测量结果。

    ' + return f'{_e(title)} · Browser Use 评测
    {body}

    结论只适用于本轮预注册样本、Agent、模型与运行配置;展示调整不改变测量结果。

    ' def _hero(eyebrow: str, title: str, lead: str) -> str: @@ -344,7 +344,7 @@ def _task_table(tasks: list[dict[str, Any]], attempts: list[dict[str, Any]]) -> c, m = primary.get(("chromium", task_id)), primary.get(("moli", task_id)) result = "pass" if c and m and float(c["score"]) == float(m["score"]) == 1 else "fail" values = [ - Safe(f'#{task_id}'), + Safe(f'#{task_id}'), TIER_LABELS[task["tier"]], SITE_LABELS.get(site, site), kind, @@ -374,7 +374,7 @@ def _attempt_table(attempts: list[dict[str, Any]], tasks: list[dict[str, Any]]) p = _public_attempt(row) result = "pass" if p["score"] == 1 else "fail" values = [ - Safe(f'{p["id"]}'), + Safe(f'{p["id"]}'), ENGINE_LABELS[p["engine"]], p["task_id"], TIER_LABELS[p["tier"]], @@ -492,7 +492,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) + _section( "独立导出", - '', + '', ) + f'
    {article}
    ' ) @@ -549,7 +549,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: "首轮" if attempt_no == 1 else "失败诊断重试", _status(row), _seconds(float(row["duration_ms"])), - Safe(f'查看试次'), + Safe(f'查看试次'), ] ) detail = _hero( @@ -714,7 +714,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ["检查", "结果"], [[key, "通过" if value else "未通过"] for key, value in checks.items()], ) - + f'', + + f'', ) (stage / f"attempts/{public['id']}.html").write_text( _page(title=public["id"], active="analysis", body=detail, depth=1), encoding="utf-8" @@ -793,7 +793,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: version, image = versions.get(engine) or {}, images.get(engine) or {} engine_rows.append( [ - Safe(f'{ENGINE_LABELS[engine]}'), + Safe(f'{ENGINE_LABELS[engine]}'), version.get("Browser", "—"), version.get("Protocol-Version", "—"), image.get("architecture", "—"), @@ -850,7 +850,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) reproduce += _section( "公开复现入口", - '

    lexmount/browser-eval 是代码、题集和锁文件的唯一来源。

    ./examples/browser_agent/run.sh\nmake browser-agent-review EVIDENCE="$BROWSER_AGENT_EVIDENCE"\nmake browser-agent-report EVIDENCE="$BROWSER_AGENT_EVIDENCE"\nmake browser-agent-report-check EVIDENCE="$BROWSER_AGENT_EVIDENCE"
    ', + '

    lexmount/browser-eval 是代码、题集和锁文件的唯一来源。

    ./examples/browser_agent/run.sh\nmake browser-agent-review EVIDENCE="$BROWSER_AGENT_EVIDENCE"\nmake browser-agent-report EVIDENCE="$BROWSER_AGENT_EVIDENCE"\nmake browser-agent-report-check EVIDENCE="$BROWSER_AGENT_EVIDENCE"
    ', ) (stage / "reproduce/index.html").write_text( _page(title="环境与复现", active="reproduce", body=reproduce, depth=1), encoding="utf-8" diff --git a/scripts/check_report_links.py b/scripts/check_report_links.py index 100644971..0541e3a59 100755 --- a/scripts/check_report_links.py +++ b/scripts/check_report_links.py @@ -12,8 +12,12 @@ def __init__(self) -> None: super().__init__() self.values: list[str] = [] self.ids: set[str] = set() + self.base_href: str | None = None def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: + if tag == "base": + self.base_href = next((value for key, value in attrs if key == "href" and value), None) + return for key, value in attrs: if key in {"href", "src"} and value: self.values.append(value) @@ -35,11 +39,16 @@ def parse_page(path: Path) -> Links: for page in pages: parser = parse_page(page) + base_dir = page.parent + if parser.base_href: + parsed_base = urlsplit(parser.base_href) + if not parsed_base.scheme: + base_dir = (page.parent / unquote(parsed_base.path)).resolve() for value in parser.values: parsed = urlsplit(value) if parsed.scheme or value.startswith("mailto:"): continue - target = page if not parsed.path else (page.parent / unquote(parsed.path)).resolve() + target = page if not parsed.path else (base_dir / unquote(parsed.path)).resolve() if not target.exists(): missing.append(f"{page.relative_to(root)} -> {value}") continue diff --git a/tests/test_report_links.py b/tests/test_report_links.py index 88b8b995c..4dd9780c1 100644 --- a/tests/test_report_links.py +++ b/tests/test_report_links.py @@ -35,6 +35,19 @@ def test_rejects_missing_fragment(self) -> None: self.assertEqual(result.returncode, 1) self.assertIn("missing fragment", result.stdout) + def test_honors_document_base(self) -> None: + with tempfile.TemporaryDirectory() as directory: + root = Path(directory) + (root / "assets").mkdir() + (root / "assets/style.css").write_text("", encoding="utf-8") + (root / "nested").mkdir() + (root / "nested/index.html").write_text( + '', + encoding="utf-8", + ) + self.assertEqual(self.run_check(root).returncode, 0) + if __name__ == "__main__": unittest.main() From 283eb95571becc9dfd9f5c41faac955540d57476 Mon Sep 17 00:00:00 2001 From: kevin Date: Wed, 9 Sep 2026 21:17:03 +0800 Subject: [PATCH 027/127] Expose browser-agent ground truth and trajectories --- .../reference-gap-matrix.md | 28 ++ harness/browser_agent/publication.py | 185 +++++++ harness/browser_agent/report.py | 464 ++++++++++++++++-- research/report-changes.json | 2 +- scripts/check_browser_agent_report.py | 48 +- tests/browser_agent/test_publication.py | 55 +++ tests/browser_agent/test_report.py | 20 + 7 files changed, 760 insertions(+), 42 deletions(-) create mode 100644 .ai/topics/browser-agent-report-rewrite/reference-gap-matrix.md create mode 100644 harness/browser_agent/publication.py create mode 100644 tests/browser_agent/test_publication.py diff --git a/.ai/topics/browser-agent-report-rewrite/reference-gap-matrix.md b/.ai/topics/browser-agent-report-rewrite/reference-gap-matrix.md new file mode 100644 index 000000000..a248337ef --- /dev/null +++ b/.ai/topics/browser-agent-report-rewrite/reference-gap-matrix.md @@ -0,0 +1,28 @@ +# Browser Agent report parity matrix + +This is an internal implementation checklist. It is not part of the public report. + +| Reader question / function | Web Fetch reference | BrowserTool reference | Browser Agent implementation | +|---|---|---|---| +| What was tested? | Searchable dataset inventory and per-case pages | Search, difficulty/site/action filters, per-task cards | Searchable 30-task inventory; per-task contract pages | +| What is the ground truth? | Full reference, stable anchors, field contract | Exact outputs, derivation, fixed reference actions | Official WebArena-Verified expected response and network-event contracts | +| Is there one correct path? | Not relevant to fetch | Yes, fixed harness action sequence | No; disclose outcome-based evaluation and show a non-prescriptive contract explanation | +| What did the method actually do? | Provider output | Step-by-step actual BrowserTool calls | Full CowAgent BrowserTool trajectory projected from agent events | +| What did it return? | Full actual output | Saved outputs per step | Final agent response plus normalized evaluator actual values | +| Why did it pass/fail? | Rule-level checks and reference/output comparison | Output and process validation | Per-evaluator expected/actual/assertions/error with derived failure stage | +| What are the raw records? | Lazy complete output, downloadable evidence | Modal raw parameters/output/error | Sanitized response, evaluation, events, trajectory and evidence metadata downloads | +| What stays private? | Explicit publication privacy manifest | Public-web evidence only | HAR and Playwright trace remain private because they may contain session state; hashes and sizes are public | +| Can readers find failures? | Default-fail filters and search | Multi-dimensional result explorer | Default-fail result explorer with tier/site/type/engine/attempt/evaluator/search filters | +| Can readers compare engines per task? | Provider rows on dataset pages | Side-by-side reference/action table | Side-by-side primary result, actual final answer, failed evaluator and trajectory links | +| Can readers sort and preserve context? | Sortable rankings; loader restores filters/scroll | Search/filter and deep links | Sortable tables, counts, search/filter, deep links; no virtual loader needed at this report size | +| Is review auditable? | Review rounds, protocol, coverage, disagreements | Programmatic result/process checks | Review protocol, full attempt coverage, per-attempt checks, disagreements and unresolved inventory | +| Are methodology and benchmark choice explained? | Track-local contracts and boundaries | Step/result metric definitions | Benchmark adoption, judge types, selection decision, deterministic outcome contract and limits | +| Are current report-only modules retained? | N/A | N/A | Benchmark landscape, Astra access probe, paired inference, autonomous-vs-fixed harness boundary | +| Is reproduction bounded? | Code/dataset identity and current commands | Source and raw data | Commit/model/reasoning/browser/site/evaluator identities and current commands | + +Implementation decisions: + +- Do not invent a canonical action path for WebArena-Verified. Its ground truth is a conjunction of evaluator contracts. +- Publish benchmark answers because they are part of the public benchmark package; redact credentials and session-bearing material. +- Publish sanitized CowAgent events and evaluator records. Do not publish raw HAR or Playwright trace archives. +- Keep the direct static-site architecture. The projected report remains far below the reference report's virtual-file threshold, so a gzip virtual loader would add complexity without reader value. diff --git a/harness/browser_agent/publication.py b/harness/browser_agent/publication.py new file mode 100644 index 000000000..0ebd35f00 --- /dev/null +++ b/harness/browser_agent/publication.py @@ -0,0 +1,185 @@ +"""Build privacy-safe public projections of browser-agent evidence.""" + +from __future__ import annotations + +import hashlib +import json +import re +from pathlib import Path +from typing import Any + + +_ORIGIN_REPLACEMENTS = { + "http://host.docker.internal:7770": "__SHOPPING__", + "http://host.docker.internal:7780/admin": "__SHOPPING_ADMIN__", + "http://host.docker.internal:7780": "__SHOPPING_ADMIN__", + "http://host.docker.internal:9999": "__REDDIT__", + "http://host.docker.internal:8023": "__GITLAB__", + "http://192.168.5.2:7770": "__SHOPPING__", + "http://192.168.5.2:7780/admin": "__SHOPPING_ADMIN__", + "http://192.168.5.2:7780": "__SHOPPING_ADMIN__", + "http://192.168.5.2:9999": "__REDDIT__", + "http://192.168.5.2:8023": "__GITLAB__", +} +_KNOWN_SECRET_VALUES = ( + "Password.123", + "admin1234", + "test1234", + "hello1234", +) +_SENSITIVE_KEY = re.compile( + r"(?:authorization|cookie|password|passwd|secret|token|storage_state|api[_-]?key)", re.I +) +_LOCAL_PATH = re.compile(r"/(?:Users|home|private|tmp|var|opt)/[^\s\"']+") +_BEARER = re.compile(r"(?i)bearer\s+[A-Za-z0-9._~+/=-]+") + + +def sha256_file(path: Path) -> str | None: + return hashlib.sha256(path.read_bytes()).hexdigest() if path.is_file() else None + + +def sanitize_text(value: str) -> str: + text = value + for origin, placeholder in sorted( + _ORIGIN_REPLACEMENTS.items(), key=lambda item: len(item[0]), reverse=True + ): + text = text.replace(origin, placeholder) + for secret in _KNOWN_SECRET_VALUES: + text = text.replace(secret, "[REDACTED]") + text = text.replace("host.docker.internal", "__BENCHMARK_HOST__") + text = text.replace("192.168.5.2", "__BENCHMARK_HOST__") + text = _BEARER.sub("Bearer [REDACTED]", text) + text = _LOCAL_PATH.sub("", text) + return text + + +def sanitize(value: Any, *, key: str = "") -> Any: + """Recursively redact credentials while preserving benchmark evidence.""" + + if _SENSITIVE_KEY.search(key): + return "[REDACTED]" + if isinstance(value, str): + return sanitize_text(value) + if isinstance(value, list): + return [sanitize(item) for item in value] + if isinstance(value, tuple): + return [sanitize(item) for item in value] + if isinstance(value, dict): + return {str(k): sanitize(v, key=str(k)) for k, v in value.items()} + return value + + +def read_public_json(path: Path) -> Any: + if not path.is_file(): + return None + return sanitize(json.loads(path.read_text(encoding="utf-8"))) + + +def task_contract(task: Any) -> dict[str, Any]: + raw = task.model_dump(mode="json") if hasattr(task, "model_dump") else dict(task) + return sanitize( + { + "task_id": raw.get("task_id"), + "intent_template_id": raw.get("intent_template_id"), + "revision": raw.get("revision"), + "sites": raw.get("sites") or [], + "start_urls": raw.get("start_urls") or [], + "intent": raw.get("intent"), + "evaluation_contract": raw.get("eval") or [], + } + ) + + +def evaluator_summary(result: dict[str, Any] | None) -> list[dict[str, Any]]: + if not result: + return [] + summaries = [] + for index, item in enumerate(result.get("evaluators_results") or [], start=1): + summaries.append( + { + "index": index, + "name": item.get("evaluator_name") or "UnknownEvaluator", + "status": item.get("status"), + "score": item.get("score"), + "expected": item.get("expected"), + "actual": item.get("actual_normalized", item.get("actual")), + "assertions": item.get("assertions"), + "error": item.get("error_msg"), + "should_not_exist": item.get("should_not_exist"), + } + ) + return summaries + + +def trajectory(events: list[dict[str, Any]] | None) -> list[dict[str, Any]]: + """Project CowAgent events into a readable, complete BrowserTool action sequence.""" + + if not events: + return [] + turn = 0 + starts: dict[str, dict[str, Any]] = {} + rows: list[dict[str, Any]] = [] + for event in events: + event_type = event.get("type") + data = event.get("data") or {} + if event_type == "turn_start": + turn = int(data.get("turn") or turn + 1) + elif event_type == "tool_execution_start": + call_id = str(data.get("tool_call_id") or "") + starts[call_id] = { + "step": len(rows) + 1, + "turn": turn, + "timestamp": event.get("timestamp"), + "tool": data.get("tool_name"), + "arguments": data.get("arguments"), + } + elif event_type == "tool_execution_end": + call_id = str(data.get("tool_call_id") or "") + row = starts.pop(call_id, {"step": len(rows) + 1, "turn": turn}) + row.update( + { + "status": data.get("status"), + "duration_seconds": data.get("execution_time"), + "result": data.get("result"), + "error": data.get("error"), + } + ) + rows.append(sanitize(row)) + for row in starts.values(): + row.update({"status": "missing_end_event", "result": None, "error": None}) + rows.append(sanitize(row)) + return rows + + +def trajectory_label(step: dict[str, Any]) -> str: + arguments = step.get("arguments") or {} + action = arguments.get("action") or step.get("tool") or "tool" + target = arguments.get("url") or arguments.get("ref") or arguments.get("text") or "" + if isinstance(target, (dict, list)): + target = json.dumps(target, ensure_ascii=False) + label = f"{action} {target}".strip() + return label[:180] + ("…" if len(label) > 180 else "") + + +def result_excerpt(step: dict[str, Any], limit: int = 900) -> str: + text = str(step.get("error") or step.get("result") or "") + if len(text) <= limit: + return text + return text[:limit].rstrip() + "\n…(完整内容见脱敏事件日志)" + + +def evidence_metadata(row: dict[str, Any]) -> dict[str, Any]: + files = {} + for label, raw_path in (row.get("paths") or {}).items(): + path = Path(raw_path) + files[label] = { + "present": path.is_file(), + "bytes": path.stat().st_size if path.is_file() else None, + "sha256": sha256_file(path), + "publication": ( + "sanitized_projection" + if label in {"response", "evaluation", "events"} + else "private_only_session_sensitive" + ), + } + return {"attempt_id": f"{row['engine']}-{row['task_id']}-a{row['attempt']}", "files": files} diff --git a/harness/browser_agent/report.py b/harness/browser_agent/report.py index d359d78f2..3c2277a02 100644 --- a/harness/browser_agent/report.py +++ b/harness/browser_agent/report.py @@ -17,6 +17,16 @@ from harness.web_fetch.report_style import STYLE as SHARED_STYLE +from .publication import ( + evaluator_summary, + evidence_metadata, + read_public_json, + result_excerpt, + sanitize, + task_contract, + trajectory, + trajectory_label, +) from .selection import PilotSelection, load_pilot, validate_against_official ENGINE_LABELS = {"chromium": "Chromium", "moli": "Moli 1.1.3"} @@ -203,7 +213,9 @@ def _resolve_tasks(selection: PilotSelection) -> list[dict[str, Any]]: try: from .runner import _configure_benchmark - tasks = validate_against_official(selection, _configure_benchmark()) + benchmark = _configure_benchmark() + tasks = validate_against_official(selection, benchmark) + official = {int(task["task_id"]): benchmark.get_task(int(task["task_id"])) for task in tasks} except Exception: tasks = [ { @@ -221,7 +233,12 @@ def _resolve_tasks(selection: PilotSelection) -> list[dict[str, Any]]: for t in selection.tasks ] for task in tasks: - task["task_type"] = _task_type(int(task["task_id"])) + official_task = official.get(int(task["task_id"])) if "official" in locals() else None + contract = task_contract(official_task) if official_task is not None else {} + task["contract"] = contract + evals = contract.get("evaluation_contract") or [] + expected = (evals[0].get("expected") or {}) if evals else {} + task["task_type"] = str(expected.get("task_type") or _task_type(int(task["task_id"]))).upper() return tasks @@ -247,12 +264,12 @@ def _group_rows( return result -REPORT_JS = """function theme(){const r=document.documentElement,n=r.dataset.theme==='dark'?'light':'dark';r.dataset.theme=n;try{localStorage.setItem('browser-agent-theme',n)}catch(e){}}function filterRows(){const f={tier:document.querySelector('[data-filter=tier]')?.value||'',site:document.querySelector('[data-filter=site]')?.value||'',kind:document.querySelector('[data-filter=kind]')?.value||'',result:document.querySelector('[data-filter=result]')?.value||''};document.querySelectorAll('tbody tr[data-tier]').forEach(r=>r.hidden=Object.entries(f).some(([k,v])=>v&&r.dataset[k]!==v));}document.addEventListener('DOMContentLoaded',()=>{try{document.documentElement.dataset.theme=localStorage.getItem('browser-agent-theme')||'light'}catch(e){}document.querySelectorAll('[data-filter]').forEach(x=>x.addEventListener('change',filterRows));filterRows();});""" +REPORT_JS = """function theme(){const r=document.documentElement,n=r.dataset.theme==='dark'?'light':'dark';r.dataset.theme=n;try{localStorage.setItem('browser-agent-theme',n)}catch(e){}}function filterRows(){const controls=[...document.querySelectorAll('[data-filter]')],query=(document.querySelector('[data-search]')?.value||'').trim().toLowerCase();let visible=0,total=0;document.querySelectorAll('tbody tr[data-tier]').forEach(r=>{total++;const hidden=controls.some(c=>c.value&&r.dataset[c.dataset.filter]!==c.value)||(query&&!String(r.dataset.search||r.textContent).toLowerCase().includes(query));r.hidden=hidden;if(!hidden)visible++;});document.querySelectorAll('[data-visible-count]').forEach(x=>x.textContent=visible+' / '+total);}function sortTable(id,col){const t=document.getElementById(id),b=t?.tBodies[0];if(!b)return;const dir=t.dataset.col==col&&t.dataset.dir==='asc'?'desc':'asc';[...b.rows].sort((a,z)=>{const av=a.cells[col]?.dataset.sort||a.cells[col]?.textContent||'',zv=z.cells[col]?.dataset.sort||z.cells[col]?.textContent||'';const an=Number(av.replace(/[^0-9.-]/g,'')),zn=Number(zv.replace(/[^0-9.-]/g,''));const cmp=!Number.isNaN(an)&&!Number.isNaN(zn)?an-zn:av.localeCompare(zv,'zh-CN');return dir==='asc'?cmp:-cmp}).forEach(r=>b.appendChild(r));t.dataset.col=col;t.dataset.dir=dir;}document.addEventListener('DOMContentLoaded',()=>{try{document.documentElement.dataset.theme=localStorage.getItem('browser-agent-theme')||'light'}catch(e){}document.querySelectorAll('[data-filter]').forEach(x=>x.addEventListener('change',filterRows));document.querySelectorAll('[data-search]').forEach(x=>x.addEventListener('input',filterRows));filterRows();});""" REPORT_STYLE = ( SHARED_STYLE + """ body{overflow-x:hidden}.hero h1,.publication-article h1,.publication-article h2{overflow-wrap:anywhere;word-break:break-word} -.top nav{gap:18px;flex-wrap:nowrap}.top nav a[aria-current=page]{color:var(--ink);font-weight:800;text-decoration:underline;text-decoration-thickness:2px;text-underline-offset:8px}.top .theme-toggle{margin-left:auto}.page-main{min-height:78vh}.hero.compact{padding-bottom:22px}.hero.compact h1{font-size:clamp(34px,4.5vw,52px)}.fine{font-size:13px;color:var(--muted)}.result-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px}.result-grid .card{border-top:3px solid var(--accent)}.filters{display:flex;gap:10px;flex-wrap:wrap;margin:18px 0}.filters select{padding:9px 12px;border:1px solid var(--line);border-radius:9px;background:var(--panel);color:var(--ink)}.status-pass{color:var(--good);font-weight:750}.status-fail{color:var(--bad);font-weight:750}.flow{display:grid;grid-template-columns:repeat(6,1fr);gap:9px;list-style:none;padding:0}.flow li{padding:14px;border:1px solid var(--line);border-radius:11px;background:var(--panel);font-size:13px}.flow li+li:before{content:'→';color:var(--accent);margin-right:7px}.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:12px}.detail-grid .kv{min-height:92px}.page-links{display:flex;gap:10px;flex-wrap:wrap}.page-links a{border:1px solid var(--line);background:var(--panel);padding:8px 12px;border-radius:9px}.rank-list{counter-reset:rank}.rank-item{counter-increment:rank;display:grid;grid-template-columns:54px 1fr;gap:16px;padding:20px 0;border-top:1px solid var(--line)}.rank-item:before{content:counter(rank);font-size:34px;font-weight:850;color:var(--accent)}.rank-item h3{margin:4px 0}.article-entry{border-left:4px solid var(--accent)}.source-list li{margin:9px 0}.table-wrap{margin:18px 0}.table-wrap table{min-width:760px}.table-wrap caption{text-align:left;padding:10px 12px;color:var(--muted);caption-side:bottom}.footer{border-top:1px solid var(--line)}@media(max-width:900px){.result-grid{grid-template-columns:repeat(2,1fr)}.flow{grid-template-columns:repeat(3,1fr)}}@media(max-width:600px){.wrap{width:calc(100% - 28px)}.result-grid,.flow{grid-template-columns:1fr}.flow li+li:before{content:'↓';display:block}.hero{padding-top:28px}.hero.compact h1{font-size:30px;line-height:1.18}}@media print{.top{display:none!important}.card,.table-wrap{box-shadow:none}} +.top nav{gap:18px;flex-wrap:nowrap}.top nav a[aria-current=page]{color:var(--ink);font-weight:800;text-decoration:underline;text-decoration-thickness:2px;text-underline-offset:8px}.top .theme-toggle{margin-left:auto}.page-main{min-height:78vh}.hero.compact{padding-bottom:22px}.hero.compact h1{font-size:clamp(34px,4.5vw,52px)}.fine{font-size:13px;color:var(--muted)}.result-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px}.result-grid .card{border-top:3px solid var(--accent)}.filters{display:flex;gap:10px;flex-wrap:wrap;align-items:end;margin:18px 0}.filters select,.filters input{padding:9px 12px;border:1px solid var(--line);border-radius:9px;background:var(--panel);color:var(--ink)}.filters input{min-width:260px}.status-pass{color:var(--good);font-weight:750}.status-fail{color:var(--bad);font-weight:750}.flow{display:grid;grid-template-columns:repeat(6,1fr);gap:9px;list-style:none;padding:0}.flow li{padding:14px;border:1px solid var(--line);border-radius:11px;background:var(--panel);font-size:13px}.flow li+li:before{content:'→';color:var(--accent);margin-right:7px}.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:12px}.detail-grid .kv{min-height:92px}.page-links{display:flex;gap:10px;flex-wrap:wrap}.page-links a{border:1px solid var(--line);background:var(--panel);padding:8px 12px;border-radius:9px}.rank-list{counter-reset:rank}.rank-item{counter-increment:rank;display:grid;grid-template-columns:54px 1fr;gap:16px;padding:20px 0;border-top:1px solid var(--line)}.rank-item:before{content:counter(rank);font-size:34px;font-weight:850;color:var(--accent)}.rank-item h3{margin:4px 0}.article-entry{border-left:4px solid var(--accent)}.source-list li{margin:9px 0}.table-wrap{margin:18px 0}.table-wrap table{min-width:760px}.table-wrap caption{text-align:left;padding:10px 12px;color:var(--muted);caption-side:bottom}.footer{border-top:1px solid var(--line)}.evidence-pair{display:grid;grid-template-columns:1fr 1fr;gap:14px}.evidence-card{border:1px solid var(--line);border-radius:12px;background:var(--panel);padding:16px;min-width:0}.evidence-card h3{margin-top:0}.json-view,.log-view{white-space:pre-wrap;overflow-wrap:anywhere;max-height:540px;overflow:auto;padding:14px;border:1px solid var(--line);border-radius:9px;background:var(--bg);font-size:12px;line-height:1.55}.trajectory{list-style:none;padding:0;counter-reset:step}.trajectory>li{counter-increment:step;display:grid;grid-template-columns:46px 1fr;gap:12px;padding:15px 0;border-top:1px solid var(--line)}.trajectory>li:before{content:counter(step);display:grid;place-items:center;width:34px;height:34px;border-radius:50%;background:var(--accent);color:white;font-weight:800}.trajectory summary{cursor:pointer;font-weight:750}.rule-fail{border-left:4px solid var(--bad)}.rule-pass{border-left:4px solid var(--good)}.count-note{margin-left:auto;color:var(--muted);font-size:13px}.badge{display:inline-block;padding:3px 8px;border-radius:999px;background:var(--wash);font-size:12px;margin-right:6px}.muted-box{padding:14px;border:1px dashed var(--line);border-radius:10px;color:var(--muted)}@media(max-width:900px){.result-grid{grid-template-columns:repeat(2,1fr)}.flow{grid-template-columns:repeat(3,1fr)}.evidence-pair{grid-template-columns:1fr}}@media(max-width:600px){.wrap{width:calc(100% - 28px)}.result-grid,.flow{grid-template-columns:1fr}.flow li+li:before{content:'↓';display:block}.hero{padding-top:28px}.hero.compact h1{font-size:30px;line-height:1.18}.filters input{min-width:0;width:100%}}@media print{.top,.filters{display:none!important}.card,.table-wrap{box-shadow:none}.json-view,.log-view{max-height:none;overflow:visible}.evidence-pair{grid-template-columns:1fr}} """ ) NAV = [ @@ -295,6 +312,121 @@ def _status(row: dict[str, Any]) -> Safe: ) +def _json_pre(value: Any, *, css_class: str = "json-view") -> str: + return f'
    {_e(json.dumps(value, ensure_ascii=False, indent=2))}
    ' + + +def _task_contract_html(task: dict[str, Any]) -> str: + contract = task.get("contract") or {} + rules = contract.get("evaluation_contract") or [] + cards = [] + for index, rule in enumerate(rules, start=1): + evaluator = rule.get("evaluator") or "UnknownEvaluator" + expected = rule.get("expected") or {} + qualifiers = { + key: value + for key, value in rule.items() + if key not in {"evaluator", "expected"} + } + cards.append( + '

    ' + + _e(f"规则 {index} · {evaluator}") + + "

    程序化验收该规则必须得分 1.0。

    " + + _json_pre(expected) + + (f"
    匹配选项与结果 schema{_json_pre(qualifiers)}
    " if qualifiers else "") + + "
    " + ) + return ( + '
    没有唯一参考点击路径。 WebArena‑Verified 允许 Agent 自主选择任意 UI 路径;' + "ground truth 是以下最终回答与必要网络副作用的合取。报告因此比较结果合同,而不把某条人工路径当作标准答案。
    " + + '
    ' + + "".join(cards) + + "
    " + ) + + +def _failure_diagnosis(result: dict[str, Any] | None, row: dict[str, Any]) -> str: + failed = [item for item in evaluator_summary(result) if float(item.get("score") or 0) < 1] + if not failed: + return "全部官方 evaluator 均通过。" + parts = [] + for item in failed: + if item["name"] == "AgentResponseEvaluator": + parts.append("最终回答与官方结构化答案不一致") + elif item["name"] == "NetworkEventEvaluator": + parts.append("未观察到满足合同的必要网络请求或副作用") + else: + parts.append(f"{item['name']} 未通过") + if item.get("error"): + parts.append(str(item["error"])) + if row.get("error"): + parts.append(str(sanitize(row["error"]))) + response = row.get("agent_response") or {} + if response.get("error_details"): + parts.append(str(sanitize(response["error_details"]))) + return ";".join(dict.fromkeys(x.strip() for x in parts if x.strip())) + "。" + + +def _evaluator_result_html(result: dict[str, Any] | None) -> str: + items = evaluator_summary(result) + if not items: + return '

    没有 evaluator 明细。

    ' + cards = [] + for item in items: + passed = float(item.get("score") or 0) == 1 + detail = { + key: item.get(key) + for key in ("assertions", "error", "should_not_exist") + if item.get(key) not in (None, [], {}) + } + cards.append( + f'

    规则 {item["index"]} · {_e(item["name"])}

    ' + f'

    {"通过" if passed else "失败"} · score={_e(item.get("score"))}

    ' + '

    官方期望

    ' + + _json_pre(item.get("expected")) + + '

    实际归一化结果

    ' + + _json_pre(item.get("actual")) + + "
    " + + (f"
    断言与错误{_json_pre(detail)}
    " if detail else "") + + "
    " + ) + return "".join(cards) + + +def _trajectory_html(steps: list[dict[str, Any]]) -> str: + if not steps: + return '

    没有可展示的 BrowserTool 调用。

    ' + rows = [] + for step in steps: + duration = step.get("duration_seconds") + meta = f"Turn {step.get('turn') or '—'} · {_e(step.get('status') or 'unknown')}" + if duration is not None: + meta += f" · {float(duration):.2f} 秒" + rows.append( + '
  • ' + + _e(trajectory_label(step)) + + f'

    {meta}

    调用参数

    ' + + _json_pre(step.get("arguments")) + + '

    执行输出节选

    ' + + f'
    {_e(result_excerpt(step))}
    ' + + "
  • " + ) + return '
      ' + "".join(rows) + "
    " + + +def _artifact_links(attempt_id: str) -> str: + root = f"artifacts/{attempt_id}" + return ( + '" + ) + + def _article_body( paired: dict[str, Any], attempts: list[dict[str, Any]], tasks: list[dict[str, Any]] ) -> str: @@ -332,8 +464,25 @@ def _standalone_article(body: str) -> str: return f'Browser Agent 实测解读
    {body}
    ' -def _filters() -> str: - return '
    ' +def _filters(*, attempts: bool = False) -> str: + extra = ( + '' + '' + '' + if attempts + else "" + ) + return ( + '
    ' + '' + '' + '' + '' + + extra + + '
    ' + ) def _task_table(tasks: list[dict[str, Any]], attempts: list[dict[str, Any]]) -> str: @@ -354,25 +503,51 @@ def _task_table(tasks: list[dict[str, Any]], attempts: list[dict[str, Any]]) -> _status(m) if m else "—", ] cells = "".join(f"{x if isinstance(x, Safe) else _e(x)}" for x in values) + search = " ".join( + str(x) + for x in ( + task_id, + task.get("intent"), + task.get("rationale"), + json.dumps(task.get("contract") or {}, ensure_ascii=False), + ) + ) rows.append( - f'{cells}' + f'{cells}' ) + labels = ["任务", "层级", "站点", "类型", "最少步", "入选理由", "Chromium", "Moli"] headers = "".join( - f"{_e(x)}" - for x in ["任务", "层级", "站点", "类型", "最少步", "入选理由", "Chromium", "Moli"] + f'' + for i, x in enumerate(labels) ) return ( _filters() - + f'
    {headers}{"".join(rows)}
    ' + + f'
    {headers}{"".join(rows)}
    ' ) def _attempt_table(attempts: list[dict[str, Any]], tasks: list[dict[str, Any]]) -> str: - sites = {int(task["task_id"]): task["sites"][0] for task in tasks} + task_meta = {int(task["task_id"]): task for task in tasks} rows = [] for row in attempts: p = _public_attempt(row) result = "pass" if p["score"] == 1 else "fail" + result_record = read_public_json(Path((row.get("paths") or {}).get("evaluation", ""))) + failed_names = [ + item["name"] + for item in evaluator_summary(result_record) + if float(item.get("score") or 0) < 1 + ] + failure = ( + "none" + if not failed_names + else "mixed" + if len(failed_names) > 1 + else "network" + if failed_names[0] == "NetworkEventEvaluator" + else "response" + ) + task = task_meta.get(p["task_id"], {}) values = [ Safe(f'{p["id"]}'), ENGINE_LABELS[p["engine"]], @@ -386,8 +561,10 @@ def _attempt_table(attempts: list[dict[str, Any]], tasks: list[dict[str, Any]]) ] cells = "".join(f"{x if isinstance(x, Safe) else _e(x)}" for x in values) rows.append( - f'{cells}' + f'{cells}' ) headers = "".join( f"{_e(x)}" @@ -404,11 +581,58 @@ def _attempt_table(attempts: list[dict[str, Any]], tasks: list[dict[str, Any]]) ] ) return ( - _filters() - + f'
    {headers}{"".join(rows)}
    ' + _filters(attempts=True) + + f'
    {headers}{"".join(rows)}
    ' ) +def _failure_inventory(attempts: list[dict[str, Any]], tasks: list[dict[str, Any]]) -> str: + task_by_id = {int(task["task_id"]): task for task in tasks} + rows = [] + for row in attempts: + if int(row["attempt"]) != 1 or float(row["score"]) == 1: + continue + task_id = int(row["task_id"]) + result = read_public_json(Path((row.get("paths") or {}).get("evaluation", ""))) + failed = [ + item["name"] + for item in evaluator_summary(result) + if float(item.get("score") or 0) < 1 + ] + rows.append( + [ + Safe(f'{_attempt_id(row)}'), + task_by_id.get(task_id, {}).get("rationale", f"Task #{task_id}"), + "、".join(failed) or "未定位", + _failure_diagnosis(result, row), + Safe(f'任务与两浏览器对照'), + ] + ) + return _table(["首轮试次", "任务", "失败规则", "直接原因", "同题对照"], rows) + + +def _engine_attempts(engine: str, attempts: list[dict[str, Any]], tasks: list[dict[str, Any]]) -> str: + meta = {int(task["task_id"]): task for task in tasks} + rows = [] + for row in attempts: + if row["engine"] != engine: + continue + task = meta[int(row["task_id"])] + result = read_public_json(Path((row.get("paths") or {}).get("evaluation", ""))) + rows.append( + [ + Safe(f'{_attempt_id(row)}'), + task["rationale"], + TIER_LABELS[task["tier"]], + "重试" if int(row["attempt"]) == 2 else "首轮", + _status(row), + _seconds(float(row.get("duration_ms") or 0)), + _failure_diagnosis(result, row) if float(row["score"]) < 1 else "—", + ] + ) + return _table(["试次", "任务", "层级", "口径", "结果", "耗时", "失败原因"], rows) + + def _write_json(path: Path, value: Any) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") @@ -447,6 +671,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: "reproduce", "accounts", "data", + "artifacts", ): (stage / folder).mkdir() (stage / "assets/style.css").write_text(REPORT_STYLE, encoding="utf-8") @@ -530,6 +755,9 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: datasets += _section( "Mind2Web 的拆分不是 easy / medium / hard", "

    原始 Mind2Web 使用 cross-task、cross-website、cross-domain 衡量泛化;Online‑Mind2Web 没有统一三档;Mind2Web 2 的 short / medium / long 按动作跨度划分。本报告的简单 5、中等 10、困难 15 是预注册的操作长度层级。

    ", + ) + _section( + "怎样阅读任务 ground truth", + '

    WebArena‑Verified 的 ground truth 不是一条固定点击脚本。每题声明一个结构化最终回答合同,并在需要确认页面副作用时附加一个或多个网络事件合同。所有合同都通过才记 1 分。任务详情同时展示合同、两种浏览器的实际回答、失败规则和 CowAgent 自主轨迹。

    ', ) + _section("30 个任务", _task_table(tasks, attempts)) (stage / "datasets/index.html").write_text( _page(title="评测集", active="datasets", body=datasets, depth=1), encoding="utf-8" @@ -555,12 +783,13 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: detail = _hero( f"Task #{task_id}", task["rationale"], - "公开任务意图与测量结果;参考答案和完整 Agent 回答不发布。", + "官方任务合同、两种浏览器的实际结果与完整自主执行证据。", ) detail += _section( "任务身份", f'
    站点{_e(SITE_LABELS.get(site, site))}
    本项目层级{_e(TIER_LABELS[task["tier"]])} · {_e(task["estimated_min_steps"])} 步起
    任务类型{_e(task["task_type"])}
    官方 hard subset{"是" if task["official_hard"] else "否"}

    公开任务意图

    {_e(task["intent"])}

    模板 ID {task.get("intent_template_id") or "—"} · revision {task.get("revision") or "—"}

    ', ) + detail += _section("Ground truth · 官方验收合同", _task_contract_html(task)) detail += _section( "两种浏览器的全部正式试次", _table(["浏览器", "口径", "官方结果", "端到端耗时", "证据"], task_rows) @@ -570,6 +799,44 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: else "" ), ) + primary_comparison = [] + for engine in ENGINE_LABELS: + row = by_attempt.get((engine, task_id, 1)) + if not row: + continue + result_record = read_public_json( + Path((row.get("paths") or {}).get("evaluation", "")) + ) + event_record = read_public_json( + Path((row.get("paths") or {}).get("events", "")) + ) + step_count = len( + trajectory(event_record if isinstance(event_record, list) else []) + ) + failed_rules = [ + item["name"] + for item in evaluator_summary(result_record) + if float(item.get("score") or 0) < 1 + ] + primary_comparison.append( + [ + ENGINE_LABELS[engine], + _status(row), + Safe(_json_pre(sanitize(row.get("agent_response") or {}))), + "、".join(failed_rules) or "无", + _failure_diagnosis(result_record, row), + Safe( + f'查看 {step_count} 步轨迹' + ), + ] + ) + detail += _section( + "首轮结果对照", + _table( + ["浏览器", "结果", "实际最终回答", "失败规则", "为什么这样判", "执行过程"], + primary_comparison, + ), + ) (stage / f"datasets/{task_id}.html").write_text( _page(title=f"任务 {task_id}", active="datasets", body=detail, depth=1), encoding="utf-8", @@ -643,6 +910,29 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ], ), ) + methodology += _section( + "两类程序化 evaluator 到底检查什么", + _table( + ["Evaluator", "输入", "比较方式", "能证明什么", "不能证明什么"], + [ + [ + "AgentResponseEvaluator", + "Agent 最终 JSON", + "先按 JSON schema 和数据类型归一化,再比较 task_type、status 与 retrieved_data;按题可声明列表有序或无序。", + "最终回答符合该题公开答案合同。", + "没有检查回答是通过哪条 UI 路径得到的,也不评操作效率。", + ], + [ + "NetworkEventEvaluator", + "本次浏览器 HAR", + "在网络事件中匹配 URL、方法、状态码以及按需声明的 query、POST、响应字段或 Cookie 条件。", + "导航或页面变更产生了指定的可观察副作用。", + "没有要求复刻固定点击序列;未声明的页面状态不在验收范围内。", + ], + ], + ) + + '

    合取规则:一题配置的每个 evaluator 都必须得分 1.0,整题才通过。这里没有 LLM-as-a-judge,也没有人工改分。

    ', + ) (stage / "methodology/index.html").write_text( _page(title="评测方法", active="methodology", body=methodology, depth=1), encoding="utf-8", @@ -651,7 +941,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: analysis = _hero( "Results", "从总体配对下钻到每一次执行", - "公开必要测量字段,不公开参考答案、完整回答、Cookie、HAR 正文或机器本地路径。", + "先定位十个首轮失败的规则与直接原因,再筛选全部 70 次执行并下钻回答、轨迹和日志。", ) analysis += _section( "汇总", @@ -679,7 +969,17 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ], ) + '

    耗时包含模型响应、页面等待、站点重置与浏览器执行,不能解释为纯内核速度。

    ', - ) + _section("70 个试次", _attempt_table(attempts, tasks)) + ) + analysis += _section( + "首轮失败定位", + '

    两种浏览器在同五题失败。下表不是人工猜测:失败环节来自官方 evaluator,补充说明来自 Agent 最终错误和执行记录。

    ' + + _failure_inventory(attempts, tasks), + ) + analysis += _section( + "70 个试次", + '

    默认只显示失败;可按浏览器、层级、站点、任务类型、首轮/重试、失败环节和全文关键词筛选。

    ' + + _attempt_table(attempts, tasks), + ) (stage / "analysis/index.html").write_text( _page(title="结果分析", active="analysis", body=analysis, depth=1), encoding="utf-8" ) @@ -690,6 +990,20 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: } for row, public in zip(attempts, public_attempts, strict=True): item = review_by_key.get((public["engine"], public["task_id"], public["attempt"]), {}) + task = next(t for t in tasks if int(t["task_id"]) == public["task_id"]) + paths = row.get("paths") or {} + response_record = read_public_json(Path(paths.get("response", ""))) + evaluation_record = read_public_json(Path(paths.get("evaluation", ""))) + events_record = read_public_json(Path(paths.get("events", ""))) + steps = trajectory(events_record if isinstance(events_record, list) else []) + metadata = evidence_metadata(row) + artifact_dir = stage / "artifacts" / public["id"] + artifact_dir.mkdir(parents=True) + _write_json(artifact_dir / "agent-response.json", response_record) + _write_json(artifact_dir / "evaluation.json", evaluation_record) + _write_json(artifact_dir / "agent-events.json", events_record) + _write_json(artifact_dir / "trajectory.json", steps) + _write_json(artifact_dir / "evidence-metadata.json", metadata) usage, resource, checks = ( public["model_usage"], public["resource"], @@ -698,22 +1012,66 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: detail = _hero( public["id"], f"{ENGINE_LABELS[public['engine']]} · Task #{public['task_id']} · {'重试' if public['is_retry'] else '首轮'}", - "公开评分、运行计量和证据完整性;敏感正文保留在非公开持久证据中。", + "从官方 ground truth、实际回答、规则级判定下钻到 CowAgent 的每一步 BrowserTool 调用。", + ) + # Pages declare so every internal URL is rooted at + # the report bundle, including same-page fragment links. + attempt_page = f"attempts/{public['id']}.html" + detail += ( + '" ) detail += _section( "结果与运行", - f'
    官方结果{_status(row)}
    端到端耗时{_seconds(public["duration_ms"])}
    模型 tokens{_number(usage["total_tokens"])}
    模型请求{usage["request_count"]} 次调用 / {usage["request_attempt_count"]} 次传输尝试
    传输错误{public["transport_error_count"]}
    证据复核{_e(item.get("outcome") or "未记录")}
    ', + f'
    官方结果{_status(row)}
    失败定位{_e(_failure_diagnosis(evaluation_record, row))}
    自主操作{len(steps)} 次 BrowserTool 调用
    端到端耗时{_seconds(public["duration_ms"])}
    模型 tokens{_number(usage["total_tokens"])}
    模型请求{usage["request_count"]} 次调用 / {usage["request_attempt_count"]} 次传输尝试
    传输错误{public["transport_error_count"]}
    证据复核{_e(item.get("outcome") or "未记录")}
    ', ) - detail += _section( - "资源诊断", - _table(["指标", "值"], resource.items()) - + '

    整次 Agent 运行的容器计量只作诊断;不同进程模型下不直接比较内存或 PID。

    ', - ) + _section( - "证据完整性检查", - _table( + detail += '
    ' + _section( + "Ground truth 与实际最终回答", + '

    官方任务合同

    ' + + _task_contract_html(task) + + '

    Agent 实际回答

    ' + + _json_pre(response_record) + + "
    ", + ) + detail += '
    ' + _section( + "官方 evaluator 逐规则判定", + '

    所有规则都必须得分 1.0;这里使用 evaluator 已归一化的实际值与期望值直接对照。

    ' + + _evaluator_result_html(evaluation_record), + ) + detail += '
    ' + _section( + "CowAgent 自主执行轨迹", + '

    以下顺序来自本次执行的 Agent 事件流,不是固定动作回放。展开可查看调用参数和输出摘录;完整脱敏日志可下载。

    ' + + _trajectory_html(steps), + ) + detail += '
    ' + _section( + "运行、日志与证据", + _artifact_links(public["id"]) + + '

    公开包包含脱敏的回答、evaluator、事件流与轨迹。原始 HAR 和 Playwright trace 可能携带登录态、Cookie、页面快照或请求正文,因此不公开;其字节数和 SHA-256 保留用于证明证据绑定。

    ' + + _table(["指标", "值"], resource.items()) + + '

    整次 Agent 运行的容器计量只作诊断;不同进程模型下不直接比较内存或 PID。

    ' + + '

    证据完整性检查

    ' + + _table( ["检查", "结果"], [[key, "通过" if value else "未通过"] for key, value in checks.items()], ) + + '

    原始证据文件绑定

    ' + + _table( + ["证据", "存在", "字节", "SHA-256", "公开策略"], + [ + [ + label, + "是" if value.get("present") else "否", + value.get("bytes"), + value.get("sha256") or "—", + value.get("publication"), + ] + for label, value in (metadata.get("files") or {}).items() + ], + ) + f'', ) (stage / f"attempts/{public['id']}.html").write_text( @@ -723,11 +1081,13 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: coverage = review.get("coverage") or {} review_rows = [ [ - x["engine"], - x["task_id"], - x["attempt"], + Safe( + f'' + f'{x["engine"]}-{int(x["task_id"])}-a{int(x["attempt"])}' + ), x["outcome"], sum(not v for v in (x.get("checks") or {}).values()), + ", ".join(k for k, v in (x.get("checks") or {}).items() if not v) or "—", ] for x in review.get("attempts") or [] ] @@ -739,8 +1099,15 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: review_page += _section( "核验结论", f'
    记录试次
    {coverage.get("recorded_attempts", 0)}
    已复核
    {coverage.get("reviewed_attempts", 0)}
    记分分歧
    {len(review.get("disagreements") or [])}
    未决
    {len(review.get("unresolved") or [])}

    检查回答、HAR、Agent trace、事件和 evaluator 明细是否存在,并核对哈希、分数、状态、官方版本和模型传输尝试。

    ', - ) + _section( - "逐试次覆盖", _table(["浏览器", "任务", "试次", "结论", "失败检查"], review_rows) + ) + review_page += _section( + "复核协议", + _json_pre(review.get("protocol") or {}) + + '

    人工或二次模型不得修改官方分数;复核只验证文件存在、哈希绑定、状态/分数一致、官方版本一致和传输尝试计数。

    ', + ) + review_page += _section( + "逐试次覆盖", + _table(["试次", "结论", "失败检查数", "失败检查项"], review_rows), ) (stage / "review/index.html").write_text( _page(title="核验记录", active="review", body=review_page, depth=1), encoding="utf-8" @@ -801,7 +1168,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ] ) body = _hero( - "Engine", ENGINE_LABELS[engine], "本轮浏览器运行身份与汇总结果。" + "Engine", ENGINE_LABELS[engine], "本轮浏览器运行身份、完整任务结果与失败证据入口。" ) + _section( "运行身份", _table( @@ -817,6 +1184,11 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) + '

    版本与镜像身份用于限定复现边界,不参与评分。

    ', ) + body += _section( + "全部执行记录", + '

    首轮决定主分;仅失败任务有一次诊断重试,重试不回写主分。

    ' + + _engine_attempts(engine, attempts, tasks), + ) (stage / f"engines/{engine}.html").write_text( _page(title=ENGINE_LABELS[engine], active="adoption", body=body, depth=1), encoding="utf-8", @@ -850,7 +1222,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) reproduce += _section( "公开复现入口", - '

    lexmount/browser-eval 是代码、题集和锁文件的唯一来源。

    ./examples/browser_agent/run.sh\nmake browser-agent-review EVIDENCE="$BROWSER_AGENT_EVIDENCE"\nmake browser-agent-report EVIDENCE="$BROWSER_AGENT_EVIDENCE"\nmake browser-agent-report-check EVIDENCE="$BROWSER_AGENT_EVIDENCE"
    ', + '

    lexmount/browser-eval 是代码、题集和锁文件的唯一来源。

    ./examples/browser_agent/run.sh\nmake browser-agent-review EVIDENCE="$BROWSER_AGENT_EVIDENCE"\nmake browser-agent-report EVIDENCE="$BROWSER_AGENT_EVIDENCE"\nmake browser-agent-report-check EVIDENCE="$BROWSER_AGENT_EVIDENCE"

    每个试次的脱敏回答、evaluator、事件和轨迹位于 artifacts/<attempt-id>/,也可从试次详情页直接下载。

    ', ) (stage / "reproduce/index.html").write_text( _page(title="环境与复现", active="reproduce", body=reproduce, depth=1), encoding="utf-8" @@ -981,6 +1353,30 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) }, ) + _write_json( + stage / "data/publication-privacy.json", + { + "schema": "lex_browser_agent_publication_privacy/1", + "published": [ + "official task intent and evaluator contracts", + "sanitized final agent response", + "sanitized evaluator expected/actual/assertions/errors", + "sanitized CowAgent event stream and BrowserTool trajectory", + "artifact byte sizes and SHA-256 bindings", + ], + "private_only": [ + "raw HAR because it may contain cookies, headers and request bodies", + "raw Playwright trace because it may contain session state and page snapshots", + "model API credentials and local filesystem paths", + ], + "redactions": [ + "known benchmark credentials", + "authorization, cookie, password, secret, token and API-key fields", + "local service origins replaced with benchmark URL placeholders", + "machine-local filesystem paths", + ], + }, + ) (stage / ".generated-report").write_text("browser-agent-report/2\n", encoding="utf-8") if output.exists(): shutil.rmtree(output) diff --git a/research/report-changes.json b/research/report-changes.json index 2d659ac5c..512292083 100644 --- a/research/report-changes.json +++ b/research/report-changes.json @@ -10,7 +10,7 @@ "attempts_sha256": "517b613adcc40fc0ebe41af2ab2115d76f49faaba5378b426cc196b5b633e9c7", "selection_sha256": "ba3c344df003dd4eba3a5d1ef97162ee98df2ed728f7c29c4188bc4247982d38", "changes": [ - ["证据应用与独立解读", "只有栏目分页仍不足以核对任务选择、逐次执行、复核覆盖和结论依据。", "首页作为决策入口;另生成同源的完整解读与无 JavaScript 独立文章,并提供 30 个任务页、70 个试次页、双浏览器身份页、筛选表和五类脱敏下载数据。", "出版结构与公开证据粒度改变;任务、分母、原始输出、分数与证据绑定均未修改。"], + ["证据应用与独立解读", "只有栏目分页仍不足以核对任务选择、逐次执行、复核覆盖和结论依据。", "首页作为决策入口;30 个任务页公开官方 ground truth 与两浏览器首轮对照;70 个试次页公开实际回答、规则级 expected/actual、CowAgent 自主轨迹、输出摘录和脱敏事件下载。", "出版结构与公开证据粒度改变;任务、分母、原始输出、分数与证据绑定均未修改。原始 HAR 与 Playwright trace 因含登录态只公开哈希和大小。"], ["明确自主 Agent 轨道", "仓库同时存在执行层 browser_kernel harness 和自主 browser_agent harness,若统称为“原 30 题”会误读运行方式。", "本报告的 30 个 WebArena-Verified 任务明确由 CowAgent 自主观察、规划和操作;browser_kernel 作为另一条执行层微基准单独说明。", "本轮成绩只来自自主 Agent 轨道;两类 harness 不合并计分。"], ["公开 benchmark 选型", "模型厂商使用的 browser-use、computer-use、离线动作预测和 agentic search 口径混杂。", "按一手机构采用情况梳理 Mind2Web、WebArena、WebVoyager,并比较 Online-Mind2Web、BU Bench、Odysseys 等评测对象和 judge。", "首轮选择可自托管、确定性结果验收的 WebArena-Verified;实时网站和 LLM judge 留到后续泛化阶段。"], ["完整正式运行", "需要同时覆盖短、中、长操作链,且避免运行后换题或挑结果。", "预注册 5/10/15 题操作长度分层;每个题×浏览器只以第一次正式执行计 pass@1,失败最多重试一次且只作诊断。", "Chromium 与 Moli 均为 25/30;共同失败 5 题、单边胜出 0 题,30 题 pilot 未观察到成功率差异。"], diff --git a/scripts/check_browser_agent_report.py b/scripts/check_browser_agent_report.py index 2926feafc..9b1301b83 100644 --- a/scripts/check_browser_agent_report.py +++ b/scripts/check_browser_agent_report.py @@ -69,10 +69,23 @@ def check( "summary.json", "environment.json", "review.json", + "publication-privacy.json", + ) + ) + artifact_files = tuple( + report_dir / "artifacts" / page.stem / name + for page in attempt_pages + for name in ( + "agent-response.json", + "evaluation.json", + "agent-events.json", + "trajectory.json", + "evidence-metadata.json", ) ) if not all( - path.is_file() for path in (*pages, *task_pages, *attempt_pages, *data_files, audit_path) + path.is_file() + for path in (*pages, *task_pages, *attempt_pages, *data_files, *artifact_files, audit_path) ): raise ValueError("report is missing one or more required report pages") selection = load_pilot(selection_path) @@ -110,11 +123,21 @@ def check( rendered_pages = { page: page.read_text(encoding="utf-8") for page in (*pages, *task_pages, *attempt_pages) } - if any( - "expected_response" in text or "reference_answer" in text - for text in rendered_pages.values() + if task_pages and any( + "Ground truth · 官方验收合同" not in rendered_pages[page] for page in task_pages ): - raise ValueError("private evaluator answers must not be published") + raise ValueError("every task page must publish its official evaluation contract") + if attempt_pages and any( + marker not in rendered_pages[page] + for page in attempt_pages + for marker in ( + "Agent 实际回答", + "官方 evaluator 逐规则判定", + "CowAgent 自主执行轨迹", + "运行、日志与证据", + ) + ): + raise ValueError("every attempt page must expose result, evaluator, trajectory and logs") html = rendered_pages[index] standalone = (report_dir / "guide/article.html").read_text(encoding="utf-8") if " None: diff --git a/tests/browser_agent/test_publication.py b/tests/browser_agent/test_publication.py new file mode 100644 index 000000000..4f79a711a --- /dev/null +++ b/tests/browser_agent/test_publication.py @@ -0,0 +1,55 @@ +from harness.browser_agent.publication import sanitize, trajectory + + +def test_sanitize_redacts_credentials_origins_and_local_paths() -> None: + value = sanitize( + { + "password": "hello1234", + "url": "http://host.docker.internal:8023/dashboard/todos", + "message": "Bearer abc.def from /Users/example/run/output.json", + } + ) + + assert value["password"] == "[REDACTED]" + assert value["url"] == "__GITLAB__/dashboard/todos" + assert value["message"] == "Bearer [REDACTED] from " + + +def test_trajectory_pairs_tool_start_and_end_events() -> None: + events = [ + {"type": "turn_start", "timestamp": 1, "data": {"turn": 3}}, + { + "type": "tool_execution_start", + "timestamp": 2, + "data": { + "tool_call_id": "call-1", + "tool_name": "browser", + "arguments": {"action": "navigate", "url": "http://192.168.5.2:8023"}, + }, + }, + { + "type": "tool_execution_end", + "timestamp": 3, + "data": { + "tool_call_id": "call-1", + "tool_name": "browser", + "status": "success", + "result": "ok", + "execution_time": 1.25, + }, + }, + ] + + assert trajectory(events) == [ + { + "step": 1, + "turn": 3, + "timestamp": 2, + "tool": "browser", + "arguments": {"action": "navigate", "url": "__GITLAB__"}, + "status": "success", + "duration_seconds": 1.25, + "result": "ok", + "error": None, + } + ] diff --git a/tests/browser_agent/test_report.py b/tests/browser_agent/test_report.py index c61cea3b6..21de0284c 100644 --- a/tests/browser_agent/test_report.py +++ b/tests/browser_agent/test_report.py @@ -65,6 +65,26 @@ def test_report_keeps_primary_score_and_retry_recovery_separate(tmp_path: Path) data = json.loads((output / "report-data.json").read_text(encoding="utf-8")) assert data["summary"]["chromium"]["score"] == 0 assert data["summary"]["chromium"]["recovered"] == 1 + task = (output / "datasets/44.html").read_text(encoding="utf-8") + assert "Ground truth · 官方验收合同" in task + assert "没有唯一参考点击路径" in task + attempt = (output / "attempts/chromium-44-a1.html").read_text(encoding="utf-8") + for expected in ( + "Agent 实际回答", + "官方 evaluator 逐规则判定", + "CowAgent 自主执行轨迹", + "运行、日志与证据", + 'href="attempts/chromium-44-a1.html#trajectory"', + ): + assert expected in attempt + for artifact in ( + "agent-response.json", + "evaluation.json", + "agent-events.json", + "trajectory.json", + "evidence-metadata.json", + ): + assert (output / "artifacts/chromium-44-a1" / artifact).is_file() def test_report_renders_measured_runtime_identities(tmp_path: Path) -> None: From cb5e6a7c4024913baf7b991e99f8c969c771cad1 Mon Sep 17 00:00:00 2001 From: kevin Date: Wed, 9 Sep 2026 21:46:32 +0800 Subject: [PATCH 028/127] Complete browser-agent report evidence parity --- .../reference-gap-matrix.md | 7 +- Makefile | 1 + harness/browser_agent/publication.py | 2 + harness/browser_agent/report.py | 315 ++++++++++++++---- research/report-changes.json | 2 +- scripts/check_browser_agent_report.py | 19 +- tests/browser_agent/test_publication.py | 2 + tests/browser_agent/test_report.py | 35 +- tests/browser_agent/test_report_ui.mjs | 77 +++++ 9 files changed, 397 insertions(+), 63 deletions(-) create mode 100644 tests/browser_agent/test_report_ui.mjs diff --git a/.ai/topics/browser-agent-report-rewrite/reference-gap-matrix.md b/.ai/topics/browser-agent-report-rewrite/reference-gap-matrix.md index a248337ef..10062ec88 100644 --- a/.ai/topics/browser-agent-report-rewrite/reference-gap-matrix.md +++ b/.ai/topics/browser-agent-report-rewrite/reference-gap-matrix.md @@ -6,7 +6,7 @@ This is an internal implementation checklist. It is not part of the public repor |---|---|---|---| | What was tested? | Searchable dataset inventory and per-case pages | Search, difficulty/site/action filters, per-task cards | Searchable 30-task inventory; per-task contract pages | | What is the ground truth? | Full reference, stable anchors, field contract | Exact outputs, derivation, fixed reference actions | Official WebArena-Verified expected response and network-event contracts | -| Is there one correct path? | Not relevant to fetch | Yes, fixed harness action sequence | No; disclose outcome-based evaluation and show a non-prescriptive contract explanation | +| Is there one correct path? | Not relevant to fetch | Yes, fixed harness action sequence | No canonical path; disclose outcome-based evaluation and separately label whether this run contains a successful trajectory proving current-snapshot solvability | | What did the method actually do? | Provider output | Step-by-step actual BrowserTool calls | Full CowAgent BrowserTool trajectory projected from agent events | | What did it return? | Full actual output | Saved outputs per step | Final agent response plus normalized evaluator actual values | | Why did it pass/fail? | Rule-level checks and reference/output comparison | Output and process validation | Per-evaluator expected/actual/assertions/error with derived failure stage | @@ -14,15 +14,18 @@ This is an internal implementation checklist. It is not part of the public repor | What stays private? | Explicit publication privacy manifest | Public-web evidence only | HAR and Playwright trace remain private because they may contain session state; hashes and sizes are public | | Can readers find failures? | Default-fail filters and search | Multi-dimensional result explorer | Default-fail result explorer with tier/site/type/engine/attempt/evaluator/search filters | | Can readers compare engines per task? | Provider rows on dataset pages | Side-by-side reference/action table | Side-by-side primary result, actual final answer, failed evaluator and trajectory links | -| Can readers sort and preserve context? | Sortable rankings; loader restores filters/scroll | Search/filter and deep links | Sortable tables, counts, search/filter, deep links; no virtual loader needed at this report size | +| Can readers sort and preserve context? | Sortable rankings; loader restores filters/scroll | Search/filter and deep links | Sortable tables, URL-backed filters and sort, scroll restoration, counts and deep links; no virtual loader needed at this report size | | Is review auditable? | Review rounds, protocol, coverage, disagreements | Programmatic result/process checks | Review protocol, full attempt coverage, per-attempt checks, disagreements and unresolved inventory | | Are methodology and benchmark choice explained? | Track-local contracts and boundaries | Step/result metric definitions | Benchmark adoption, judge types, selection decision, deterministic outcome contract and limits | | Are current report-only modules retained? | N/A | N/A | Benchmark landscape, Astra access probe, paired inference, autonomous-vs-fixed harness boundary | | Is reproduction bounded? | Code/dataset identity and current commands | Source and raw data | Commit/model/reasoning/browser/site/evaluator identities and current commands | +| Is there a compact English entry point? | One-page English edition | No | One-page English results, scope, method, evidence, review, access and privacy summary | +| Can machine readers discover every artifact? | Root manifests and virtual artifact catalog | One bundled data index | Public evidence index maps all 70 attempt pages and five sanitized artifact types | Implementation decisions: - Do not invent a canonical action path for WebArena-Verified. Its ground truth is a conjunction of evaluator contracts. +- Do not treat an official contract as proof that the current site snapshot is solvable. Label tasks with a measured successful trajectory separately from tasks that have only the official contract. - Publish benchmark answers because they are part of the public benchmark package; redact credentials and session-bearing material. - Publish sanitized CowAgent events and evaluator records. Do not publish raw HAR or Playwright trace archives. - Keep the direct static-site architecture. The projected report remains far below the reference report's virtual-file threshold, so a gzip virtual loader would add complexity without reader value. diff --git a/Makefile b/Makefile index c6cf6c59a..f3fe8703c 100644 --- a/Makefile +++ b/Makefile @@ -18,6 +18,7 @@ test-python: runtime-check test-ui: node tests/test_report_ui.mjs node tests/test_report_loader.mjs + node tests/browser_agent/test_report_ui.mjs fixture-smoke: runtime-check $(PY) -m harness.web_fetch validate --providers fixture diff --git a/harness/browser_agent/publication.py b/harness/browser_agent/publication.py index 0ebd35f00..0e24cedda 100644 --- a/harness/browser_agent/publication.py +++ b/harness/browser_agent/publication.py @@ -56,6 +56,8 @@ def sanitize_text(value: str) -> str: def sanitize(value: Any, *, key: str = "") -> Any: """Recursively redact credentials while preserving benchmark evidence.""" + if value is None or value == [] or value == {}: + return value if _SENSITIVE_KEY.search(key): return "[REDACTED]" if isinstance(value, str): diff --git a/harness/browser_agent/report.py b/harness/browser_agent/report.py index 3c2277a02..ce0c48807 100644 --- a/harness/browser_agent/report.py +++ b/harness/browser_agent/report.py @@ -264,12 +264,18 @@ def _group_rows( return result -REPORT_JS = """function theme(){const r=document.documentElement,n=r.dataset.theme==='dark'?'light':'dark';r.dataset.theme=n;try{localStorage.setItem('browser-agent-theme',n)}catch(e){}}function filterRows(){const controls=[...document.querySelectorAll('[data-filter]')],query=(document.querySelector('[data-search]')?.value||'').trim().toLowerCase();let visible=0,total=0;document.querySelectorAll('tbody tr[data-tier]').forEach(r=>{total++;const hidden=controls.some(c=>c.value&&r.dataset[c.dataset.filter]!==c.value)||(query&&!String(r.dataset.search||r.textContent).toLowerCase().includes(query));r.hidden=hidden;if(!hidden)visible++;});document.querySelectorAll('[data-visible-count]').forEach(x=>x.textContent=visible+' / '+total);}function sortTable(id,col){const t=document.getElementById(id),b=t?.tBodies[0];if(!b)return;const dir=t.dataset.col==col&&t.dataset.dir==='asc'?'desc':'asc';[...b.rows].sort((a,z)=>{const av=a.cells[col]?.dataset.sort||a.cells[col]?.textContent||'',zv=z.cells[col]?.dataset.sort||z.cells[col]?.textContent||'';const an=Number(av.replace(/[^0-9.-]/g,'')),zn=Number(zv.replace(/[^0-9.-]/g,''));const cmp=!Number.isNaN(an)&&!Number.isNaN(zn)?an-zn:av.localeCompare(zv,'zh-CN');return dir==='asc'?cmp:-cmp}).forEach(r=>b.appendChild(r));t.dataset.col=col;t.dataset.dir=dir;}document.addEventListener('DOMContentLoaded',()=>{try{document.documentElement.dataset.theme=localStorage.getItem('browser-agent-theme')||'light'}catch(e){}document.querySelectorAll('[data-filter]').forEach(x=>x.addEventListener('change',filterRows));document.querySelectorAll('[data-search]').forEach(x=>x.addEventListener('input',filterRows));filterRows();});""" +REPORT_JS = """function theme(){const r=document.documentElement,n=r.dataset.theme==='dark'?'light':'dark';r.dataset.theme=n;try{localStorage.setItem('browser-agent-theme',n)}catch(e){}} +function updateViewUrl(){const u=new URL(location.href),q=document.querySelector('[data-search]');[...u.searchParams.keys()].filter(k=>k==='q'||k.startsWith('f_')).forEach(k=>u.searchParams.delete(k));if(q?.value)u.searchParams.set('q',q.value);document.querySelectorAll('[data-filter]').forEach(c=>{if(c.value)u.searchParams.set('f_'+c.dataset.filter,c.value)});history.replaceState(null,'',u.pathname+(u.search?'?'+u.searchParams.toString():'')+u.hash)} +function filterRows(save=true){const controls=[...document.querySelectorAll('[data-filter]')],query=(document.querySelector('[data-search]')?.value||'').trim().toLowerCase();let visible=0,total=0;document.querySelectorAll('tbody tr[data-tier]').forEach(r=>{total++;const hidden=Boolean(controls.some(c=>c.value&&r.dataset[c.dataset.filter]!==c.value)||(query&&!String(r.dataset.search||r.textContent).toLowerCase().includes(query)));r.hidden=hidden;if(!hidden)visible++});document.querySelectorAll('[data-visible-count]').forEach(x=>x.textContent=visible+' / '+total);if(save)updateViewUrl()} +function applySort(t,col,dir){const b=t?.tBodies[0];if(!b)return;[...b.rows].sort((a,z)=>{const av=a.cells[col]?.dataset.sort||a.cells[col]?.textContent||'',zv=z.cells[col]?.dataset.sort||z.cells[col]?.textContent||'';const an=Number(av.replace(/[^0-9.-]/g,'')),zn=Number(zv.replace(/[^0-9.-]/g,''));const cmp=!Number.isNaN(an)&&!Number.isNaN(zn)?an-zn:av.localeCompare(zv,'zh-CN');return dir==='asc'?cmp:-cmp}).forEach(r=>b.appendChild(r));t.dataset.col=String(col);t.dataset.dir=dir} +function sortTable(id,col){const t=document.getElementById(id);if(!t)return;const dir=t.dataset.col==col&&t.dataset.dir==='asc'?'desc':'asc';applySort(t,col,dir);const u=new URL(location.href);u.searchParams.set('sort',id+':'+col+':'+dir);history.replaceState(null,'',u.pathname+'?'+u.searchParams.toString()+u.hash)} +function restoreView(){const p=new URLSearchParams(location.search),q=document.querySelector('[data-search]');if(q&&p.has('q'))q.value=p.get('q')||'';document.querySelectorAll('[data-filter]').forEach(c=>{const v=p.get('f_'+c.dataset.filter);if(v!==null)c.value=v});const s=(p.get('sort')||'').split(':');if(s.length===3)applySort(document.getElementById(s[0]),Number(s[1]),s[2]);filterRows(false);try{const y=sessionStorage.getItem('browser-agent-scroll:'+location.pathname+location.search);if(y!==null)requestAnimationFrame(()=>scrollTo(0,Number(y)||0))}catch(e){}} +document.addEventListener('DOMContentLoaded',()=>{try{document.documentElement.dataset.theme=localStorage.getItem('browser-agent-theme')||'light'}catch(e){}restoreView();document.querySelectorAll('[data-filter]').forEach(x=>x.addEventListener('change',()=>filterRows()));document.querySelectorAll('[data-search]').forEach(x=>x.addEventListener('input',()=>filterRows()))});addEventListener('pagehide',()=>{try{sessionStorage.setItem('browser-agent-scroll:'+location.pathname+location.search,String(scrollY))}catch(e){}});""" REPORT_STYLE = ( SHARED_STYLE + """ body{overflow-x:hidden}.hero h1,.publication-article h1,.publication-article h2{overflow-wrap:anywhere;word-break:break-word} -.top nav{gap:18px;flex-wrap:nowrap}.top nav a[aria-current=page]{color:var(--ink);font-weight:800;text-decoration:underline;text-decoration-thickness:2px;text-underline-offset:8px}.top .theme-toggle{margin-left:auto}.page-main{min-height:78vh}.hero.compact{padding-bottom:22px}.hero.compact h1{font-size:clamp(34px,4.5vw,52px)}.fine{font-size:13px;color:var(--muted)}.result-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px}.result-grid .card{border-top:3px solid var(--accent)}.filters{display:flex;gap:10px;flex-wrap:wrap;align-items:end;margin:18px 0}.filters select,.filters input{padding:9px 12px;border:1px solid var(--line);border-radius:9px;background:var(--panel);color:var(--ink)}.filters input{min-width:260px}.status-pass{color:var(--good);font-weight:750}.status-fail{color:var(--bad);font-weight:750}.flow{display:grid;grid-template-columns:repeat(6,1fr);gap:9px;list-style:none;padding:0}.flow li{padding:14px;border:1px solid var(--line);border-radius:11px;background:var(--panel);font-size:13px}.flow li+li:before{content:'→';color:var(--accent);margin-right:7px}.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:12px}.detail-grid .kv{min-height:92px}.page-links{display:flex;gap:10px;flex-wrap:wrap}.page-links a{border:1px solid var(--line);background:var(--panel);padding:8px 12px;border-radius:9px}.rank-list{counter-reset:rank}.rank-item{counter-increment:rank;display:grid;grid-template-columns:54px 1fr;gap:16px;padding:20px 0;border-top:1px solid var(--line)}.rank-item:before{content:counter(rank);font-size:34px;font-weight:850;color:var(--accent)}.rank-item h3{margin:4px 0}.article-entry{border-left:4px solid var(--accent)}.source-list li{margin:9px 0}.table-wrap{margin:18px 0}.table-wrap table{min-width:760px}.table-wrap caption{text-align:left;padding:10px 12px;color:var(--muted);caption-side:bottom}.footer{border-top:1px solid var(--line)}.evidence-pair{display:grid;grid-template-columns:1fr 1fr;gap:14px}.evidence-card{border:1px solid var(--line);border-radius:12px;background:var(--panel);padding:16px;min-width:0}.evidence-card h3{margin-top:0}.json-view,.log-view{white-space:pre-wrap;overflow-wrap:anywhere;max-height:540px;overflow:auto;padding:14px;border:1px solid var(--line);border-radius:9px;background:var(--bg);font-size:12px;line-height:1.55}.trajectory{list-style:none;padding:0;counter-reset:step}.trajectory>li{counter-increment:step;display:grid;grid-template-columns:46px 1fr;gap:12px;padding:15px 0;border-top:1px solid var(--line)}.trajectory>li:before{content:counter(step);display:grid;place-items:center;width:34px;height:34px;border-radius:50%;background:var(--accent);color:white;font-weight:800}.trajectory summary{cursor:pointer;font-weight:750}.rule-fail{border-left:4px solid var(--bad)}.rule-pass{border-left:4px solid var(--good)}.count-note{margin-left:auto;color:var(--muted);font-size:13px}.badge{display:inline-block;padding:3px 8px;border-radius:999px;background:var(--wash);font-size:12px;margin-right:6px}.muted-box{padding:14px;border:1px dashed var(--line);border-radius:10px;color:var(--muted)}@media(max-width:900px){.result-grid{grid-template-columns:repeat(2,1fr)}.flow{grid-template-columns:repeat(3,1fr)}.evidence-pair{grid-template-columns:1fr}}@media(max-width:600px){.wrap{width:calc(100% - 28px)}.result-grid,.flow{grid-template-columns:1fr}.flow li+li:before{content:'↓';display:block}.hero{padding-top:28px}.hero.compact h1{font-size:30px;line-height:1.18}.filters input{min-width:0;width:100%}}@media print{.top,.filters{display:none!important}.card,.table-wrap{box-shadow:none}.json-view,.log-view{max-height:none;overflow:visible}.evidence-pair{grid-template-columns:1fr}} +.top nav{gap:18px;flex-wrap:nowrap;overflow-x:auto;overscroll-behavior-inline:contain;scrollbar-width:thin}.top nav>a,.top nav>button{flex:0 0 auto}.top nav a[aria-current=page]{color:var(--ink);font-weight:800;text-decoration:underline;text-decoration-thickness:2px;text-underline-offset:8px}.top .theme-toggle{margin-left:auto}.page-main{min-height:78vh}.hero.compact{padding-bottom:22px}.hero.compact h1{font-size:clamp(34px,4.5vw,52px)}.fine{font-size:13px;color:var(--muted)}.result-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px}.result-grid .card{border-top:3px solid var(--accent)}.filters{display:flex;gap:10px;flex-wrap:wrap;align-items:end;margin:18px 0}.filters select,.filters input{padding:9px 12px;border:1px solid var(--line);border-radius:9px;background:var(--panel);color:var(--ink)}.filters input{min-width:260px}.status-pass{color:var(--good);font-weight:750}.status-fail{color:var(--bad);font-weight:750}.status-unproven{color:var(--warn,var(--bad));font-weight:750}.flow{display:grid;grid-template-columns:repeat(6,1fr);gap:9px;list-style:none;padding:0}.flow li{padding:14px;border:1px solid var(--line);border-radius:11px;background:var(--panel);font-size:13px}.flow li+li:before{content:'→';color:var(--accent);margin-right:7px}.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:12px}.detail-grid .kv{min-height:92px}.page-links{display:flex;gap:10px;flex-wrap:wrap}.page-links a{border:1px solid var(--line);background:var(--panel);padding:8px 12px;border-radius:9px}.rank-list{counter-reset:rank}.rank-item{counter-increment:rank;display:grid;grid-template-columns:54px 1fr;gap:16px;padding:20px 0;border-top:1px solid var(--line)}.rank-item:before{content:counter(rank);font-size:34px;font-weight:850;color:var(--accent)}.rank-item h3{margin:4px 0}.article-entry{border-left:4px solid var(--accent)}.source-list li{margin:9px 0}.table-wrap{margin:18px 0}.table-wrap table{min-width:760px}#task-table{min-width:1280px}.table-wrap caption{text-align:left;padding:10px 12px;color:var(--muted);caption-side:bottom}.footer{border-top:1px solid var(--line)}.evidence-pair{display:grid;grid-template-columns:1fr 1fr;gap:14px}.evidence-card{border:1px solid var(--line);border-radius:12px;background:var(--panel);padding:16px;min-width:0}.evidence-card h3{margin-top:0}.json-view,.log-view{white-space:pre-wrap;overflow-wrap:anywhere;max-height:540px;overflow:auto;padding:14px;border:1px solid var(--line);border-radius:9px;background:var(--bg);font-size:12px;line-height:1.55}.trajectory{list-style:none;padding:0;counter-reset:step}.trajectory>li{counter-increment:step;display:grid;grid-template-columns:46px 1fr;gap:12px;padding:15px 0;border-top:1px solid var(--line)}.trajectory>li:before{content:counter(step);display:grid;place-items:center;width:34px;height:34px;border-radius:50%;background:var(--accent);color:white;font-weight:800}.trajectory summary{cursor:pointer;font-weight:750}.rule-fail{border-left:4px solid var(--bad)}.rule-pass{border-left:4px solid var(--good)}.count-note{margin-left:auto;color:var(--muted);font-size:13px}.badge{display:inline-block;padding:3px 8px;border-radius:999px;background:var(--wash);font-size:12px;margin-right:6px}.muted-box{padding:14px;border:1px dashed var(--line);border-radius:10px;color:var(--muted)}@media(max-width:900px){.result-grid{grid-template-columns:repeat(2,1fr)}.flow{grid-template-columns:repeat(3,1fr)}.evidence-pair{grid-template-columns:1fr}}@media(max-width:600px){.wrap{width:calc(100% - 28px)}.top nav{gap:14px}.result-grid,.flow{grid-template-columns:1fr}.flow li+li:before{content:'↓';display:block}.hero{padding-top:28px}.hero.compact h1{font-size:30px;line-height:1.18}.filters input{min-width:0;width:100%}}@media print{.top,.filters{display:none!important}.card,.table-wrap{box-shadow:none}.json-view,.log-view{max-height:none;overflow:visible}.evidence-pair{grid-template-columns:1fr}} """ ) NAV = [ @@ -283,6 +289,7 @@ def _group_rows( ("adoption/", "方法介绍", "adoption"), ("reproduce/", "环境与复现", "reproduce"), ("accounts/", "账号", "accounts"), + ("en/", "English", "en"), ] @@ -297,6 +304,31 @@ def _page(*, title: str, active: str, body: str, depth: int = 0) -> str: return f'{_e(title)} · Browser Use 评测
    {body}

    结论只适用于本轮预注册样本、Agent、模型与运行配置;展示调整不改变测量结果。

    ' +def _english_page(body: str) -> str: + links = "".join( + f'{label}' + for anchor, label in ( + ("results", "Results"), + ("scope", "Scope"), + ("method", "Method"), + ("evidence", "Evidence"), + ("review", "Review"), + ("access", "Access"), + ) + ) + return ( + '' + '' + 'English edition · Browser Use evaluation' + '
    ' + '
    ' + + body + + '

    Conclusions apply only to this preregistered sample, agent, model, and runtime configuration.

    ' + ) + + def _hero(eyebrow: str, title: str, lead: str) -> str: return f'
    {_e(eyebrow)}

    {_e(title)}

    {_e(lead)}

    ' @@ -351,20 +383,53 @@ def _failure_diagnosis(result: dict[str, Any] | None, row: dict[str, Any]) -> st return "全部官方 evaluator 均通过。" parts = [] for item in failed: - if item["name"] == "AgentResponseEvaluator": - parts.append("最终回答与官方结构化答案不一致") - elif item["name"] == "NetworkEventEvaluator": - parts.append("未观察到满足合同的必要网络请求或副作用") - else: - parts.append(f"{item['name']} 未通过") + messages: list[str] = [] + for assertion in item.get("assertions") or []: + assertion_messages = assertion.get("assertion_msgs") or [] + if assertion_messages: + messages.append(str(assertion_messages[0])) + if assertion.get("error_msg"): + messages.append(str(assertion["error_msg"])) if item.get("error"): - parts.append(str(item["error"])) + messages.append(str(item["error"])) + if not messages: + messages.append( + "最终回答与官方结构化答案不一致" + if item["name"] == "AgentResponseEvaluator" + else "未观察到满足合同的必要网络请求或副作用" + if item["name"] == "NetworkEventEvaluator" + else "该规则未通过" + ) + expected, actual = item.get("expected"), item.get("actual") + if expected is not None or actual is not None: + comparison = ( + f"expected={json.dumps(expected, ensure_ascii=False, separators=(',', ':'))}; " + f"actual={json.dumps(actual, ensure_ascii=False, separators=(',', ':'))}" + ) + messages.append(comparison) + exact = ";".join( + dict.fromkeys(str(sanitize(message)).strip().rstrip("。.;; ") for message in messages) + ) + if len(exact) > 420: + exact = exact[:417].rstrip() + "…" + parts.append(f"规则 {item['index']} {item['name']}:{exact}") if row.get("error"): parts.append(str(sanitize(row["error"]))) response = row.get("agent_response") or {} if response.get("error_details"): parts.append(str(sanitize(response["error_details"]))) - return ";".join(dict.fromkeys(x.strip() for x in parts if x.strip())) + "。" + return ( + ";".join(dict.fromkeys(x.strip() for x in parts if x.strip())).rstrip("。.;; ") + + "。" + ) + + +def _failed_rule_labels(result: dict[str, Any] | None) -> list[str]: + return [ + f"规则 {item['index']} {item['name']}" + for item in evaluator_summary(result) + if float(item.get("score") or 0) < 1 + ] def _evaluator_result_html(result: dict[str, Any] | None) -> str: @@ -444,7 +509,11 @@ def _article_body( caption="表 1|30 个首轮任务的逐题配对;重试不并入主分。", ) + "

    严谨表述只能是“本轮没有观察到成功率差异”,不能写成“两种内核等价”。30 题对小效应检出力有限;更关键的是,样本没有诱发内核分歧。

    二、五个共同失败指向检索聚合与最终状态,不是重试次数

    失败集中在四个信息聚合任务和一个购物车状态任务。十个失败试次各获得一次重试,恢复数仍为 0。继续增加重试只会提高成本并模糊 pass@1。

    " - + _table(["任务", "公开诊断"], failures, caption="表 2|不公开参考答案或 Agent 原始回答。") + + _table( + ["任务", "公开诊断"], + failures, + caption="表 2|任务详情公开官方合同、实际回答、逐规则判定与执行轨迹。", + ) + "

    共同失败不能单独证明模型是唯一根因,因为 CowAgent 提示、工具抽象和步数上限也是共同变量;但它足以改变下一步优先级:先检查跨页收集、去重、范围约束和提交后状态验证。

    三、简单、中等、困难都覆盖了,但“难度”只是操作长度

    WebArena‑Verified 没有官方 easy/medium/hard 三档。本项目在运行前按最少可见 UI 操作数注册为 1–4、5–7、8+ 三档,配额 5/10/15。它控制操作链覆盖,不等同于推理难度。

    " + _table( ["本项目分层", "Chromium", "Moli"], @@ -472,6 +541,11 @@ def _filters(*, attempts: bool = False) -> str: if attempts else "" ) + solvability = ( + "" + if attempts + else '' + ) return ( '
    ' '' @@ -480,6 +554,7 @@ def _filters(*, attempts: bool = False) -> str: '' + + solvability + extra + '
    ' ) @@ -492,6 +567,15 @@ def _task_table(tasks: list[dict[str, Any]], attempts: list[dict[str, Any]]) -> task_id, site, kind = int(task["task_id"]), task["sites"][0], task["task_type"] c, m = primary.get(("chromium", task_id)), primary.get(("moli", task_id)) result = "pass" if c and m and float(c["score"]) == float(m["score"]) == 1 else "fail" + measured_success = any( + int(row["task_id"]) == task_id and float(row["score"]) == 1 for row in attempts + ) + solvability = "measured" if measured_success else "unproven" + solvability_label = ( + Safe('本轮已有成功轨迹') + if measured_success + else Safe('仅官方合同') + ) values = [ Safe(f'#{task_id}'), TIER_LABELS[task["tier"]], @@ -499,6 +583,8 @@ def _task_table(tasks: list[dict[str, Any]], attempts: list[dict[str, Any]]) -> kind, task["estimated_min_steps"], task["rationale"], + task.get("intent") or "—", + solvability_label, _status(c) if c else "—", _status(m) if m else "—", ] @@ -513,9 +599,20 @@ def _task_table(tasks: list[dict[str, Any]], attempts: list[dict[str, Any]]) -> ) ) rows.append( - f'{cells}' + f'{cells}' ) - labels = ["任务", "层级", "站点", "类型", "最少步", "入选理由", "Chromium", "Moli"] + labels = [ + "任务", + "层级", + "站点", + "类型", + "最少步", + "入选理由", + "官方任务意图", + "可完成性证据", + "Chromium", + "Moli", + ] headers = "".join( f'' for i, x in enumerate(labels) @@ -533,18 +630,18 @@ def _attempt_table(attempts: list[dict[str, Any]], tasks: list[dict[str, Any]]) p = _public_attempt(row) result = "pass" if p["score"] == 1 else "fail" result_record = read_public_json(Path((row.get("paths") or {}).get("evaluation", ""))) - failed_names = [ + failed_names = { item["name"] for item in evaluator_summary(result_record) if float(item.get("score") or 0) < 1 - ] + } failure = ( "none" if not failed_names else "mixed" if len(failed_names) > 1 else "network" - if failed_names[0] == "NetworkEventEvaluator" + if "NetworkEventEvaluator" in failed_names else "response" ) task = task_meta.get(p["task_id"], {}) @@ -594,11 +691,7 @@ def _failure_inventory(attempts: list[dict[str, Any]], tasks: list[dict[str, Any continue task_id = int(row["task_id"]) result = read_public_json(Path((row.get("paths") or {}).get("evaluation", ""))) - failed = [ - item["name"] - for item in evaluator_summary(result) - if float(item.get("score") or 0) < 1 - ] + failed = _failed_rule_labels(result) rows.append( [ Safe(f'{_attempt_id(row)}'), @@ -648,6 +741,14 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: for name in ("manifest.json", "run-config.json", "result-review.json") ) summary, paired = _primary_summary(attempts), _paired_summary(attempts) + measured_solvable_ids = { + int(row["task_id"]) for row in attempts if float(row.get("score") or 0) == 1 + } + unproven_ids = sorted( + int(task["task_id"]) + for task in tasks + if int(task["task_id"]) not in measured_solvable_ids + ) protocol, environment = manifest.get("protocol") or {}, manifest.get("environment") or {} model, run_date = ( str(protocol.get("model") or "未记录"), @@ -670,6 +771,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: "adoption", "reproduce", "accounts", + "en", "data", "artifacts", ): @@ -709,6 +811,54 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: _page(title="首页", active="home", body=home), encoding="utf-8" ) + english = _hero( + f"Browser Agent Evaluation · {run_date}", + "One autonomous agent and model; only the browser changes", + f"CowAgent with {model} (reasoning=low) ran the same 30 preregistered WebArena-Verified tasks on Chromium and Moli.", + ) + english += '
    ' + _section( + "Results", + _table( + ["Browser", "Primary passes", "pass@1", "Retry recoveries", "Median latency"], + [ + [ + ENGINE_LABELS[engine], + f"{summary[engine]['passed']}/{summary[engine]['completed']}", + _pct(summary[engine]["score"]), + f"{summary[engine]['recovered']}/{summary[engine]['retry_count']}", + ( + f"{summary[engine]['median_duration_ms'] / 1000:.1f} s" + if summary[engine]["median_duration_ms"] is not None + else "—" + ), + ] + for engine in ENGINE_LABELS + ], + ) + + f'

    Observed result: {paired["both_pass"]} tasks passed on both browsers, {paired["both_fail"]} failed on both, and {paired["chromium_only"] + paired["moli_only"]} had a one-browser-only pass. This run found no directional success-rate difference; it does not establish browser equivalence.

    ', + ) + english += '
    ' + _section( + "Scope", + '

    The suite contains 5 easy, 10 medium, and 15 hard project tiers, defined before execution by estimated minimum visible UI actions. These are operation-length strata, not official WebArena difficulty labels. Every primary failure receives at most one diagnostic retry, which never changes pass@1.

    ', + ) + english += '
    ' + _section( + "How tasks are judged", + '

    WebArena-Verified does not prescribe one canonical click path. A task passes only when every configured deterministic evaluator scores 1.0: AgentResponseEvaluator checks the normalized final JSON, while NetworkEventEvaluator checks required observable requests or side effects. There is no LLM-as-a-judge and no human score override.

    ', + ) + english += '
    ' + _section( + "Task and attempt evidence", + '

    Each task page publishes the official evaluation contract, whether this run contains a successful trajectory, every Chromium and Moli result, exact failed assertions, and links to the actual autonomous trajectory. Each attempt page publishes the final response, rule-level expected/actual values, BrowserTool calls, output excerpts, and downloadable sanitized records.

    ', + ) + english += '
    ' + _section( + "Independent integrity review", + f'

    {(review.get("coverage") or {}).get("reviewed_attempts", 0)}/{(review.get("coverage") or {}).get("recorded_attempts", 0)} attempts passed deterministic evidence checks, with {len(review.get("disagreements") or [])} score disagreements and {len(review.get("unresolved") or [])} unresolved records. The official evaluator remains the only scoring authority.

    Open the review inventory →

    ', + ) + english += '
    ' + _section( + "Access, privacy, and reproduction", + '

    The four benchmark sites are self-hosted and reset before each task. Model access is separate. The public bundle includes sanitized responses, evaluator records, events, trajectories, hashes, and runtime identities. Raw HAR and Playwright traces remain private because they may contain cookies and session state.

    ', + ) + (stage / "en/index.html").write_text(_english_page(english), encoding="utf-8") + guide = ( _hero( "Report interpretation", @@ -758,6 +908,9 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) + _section( "怎样阅读任务 ground truth", '

    WebArena‑Verified 的 ground truth 不是一条固定点击脚本。每题声明一个结构化最终回答合同,并在需要确认页面副作用时附加一个或多个网络事件合同。所有合同都通过才记 1 分。任务详情同时展示合同、两种浏览器的实际回答、失败规则和 CowAgent 自主轨迹。

    ', + ) + _section( + "参考状态与可完成性证据", + f'

    {len(measured_solvable_ids)}/30 题在本轮至少有一条自主成功轨迹,直接证明当前环境存在可完成路径;{len(unproven_ids)}/30 题只有官方验收合同而没有本轮成功轨迹(任务 {", ".join(f"#{task_id}" for task_id in unproven_ids) or "无"})。后者仍可稳定判分,但本轮不能独立证明当前快照可完成,也不能仅凭共同失败归因浏览器。

    “有成功路径证据”不等于存在唯一标准路径;WebArena‑Verified 不要求复刻人工动作序列。

    ', ) + _section("30 个任务", _task_table(tasks, attempts)) (stage / "datasets/index.html").write_text( _page(title="评测集", active="datasets", body=datasets, depth=1), encoding="utf-8" @@ -766,6 +919,11 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: by_attempt = {(r["engine"], int(r["task_id"]), int(r["attempt"])): r for r in attempts} for task in tasks: task_id, site = int(task["task_id"]), task["sites"][0] + measured_passes = [ + row + for row in attempts + if int(row["task_id"]) == task_id and float(row["score"]) == 1 + ] task_rows = [] for engine in ENGINE_LABELS: for attempt_no in (1, 2): @@ -787,9 +945,24 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) detail += _section( "任务身份", - f'
    站点{_e(SITE_LABELS.get(site, site))}
    本项目层级{_e(TIER_LABELS[task["tier"]])} · {_e(task["estimated_min_steps"])} 步起
    任务类型{_e(task["task_type"])}
    官方 hard subset{"是" if task["official_hard"] else "否"}

    公开任务意图

    {_e(task["intent"])}

    模板 ID {task.get("intent_template_id") or "—"} · revision {task.get("revision") or "—"}

    ', + f'
    站点{_e(SITE_LABELS.get(site, site))}
    本项目层级{_e(TIER_LABELS[task["tier"]])} · {_e(task["estimated_min_steps"])} 步起
    任务类型{_e(task["task_type"])}
    官方 hard subset{"是" if task["official_hard"] else "否"}
    本轮可完成性证据{"已有成功轨迹" if measured_passes else "尚无成功轨迹"}

    公开任务意图

    {_e(task["intent"])}

    官方来源:WebArena‑Verified {_e(environment.get("webarena_verified_version", "—"))} · Task #{task_id} · 模板 ID {task.get("intent_template_id") or "—"} · revision {task.get("revision") or "—"}

    ', ) detail += _section("Ground truth · 官方验收合同", _task_contract_html(task)) + if measured_passes: + proof_links = "".join( + f'{_e(_attempt_id(row))} 成功轨迹' + for row in measured_passes + ) + detail += _section( + "参考状态 · 当前环境是否有成功路径证据", + '

    有。本轮至少一个自主试次通过全部官方合同,因此当前站点快照存在已实测的可完成路径。成功轨迹不是唯一标准路径,也不要求另一浏览器复刻。

    ' + f'', + ) + else: + detail += _section( + "参考状态 · 当前环境是否有成功路径证据", + '

    本轮没有。官方任务包给出了确定性验收合同,但两种浏览器的首轮和唯一诊断重试都未通过。因此本报告不能独立证明当前快照存在可完成路径,也不能把失败直接归因给浏览器;需要另做不参与计分的人工或 oracle 可完成性核验。

    ', + ) detail += _section( "两种浏览器的全部正式试次", _table(["浏览器", "口径", "官方结果", "端到端耗时", "证据"], task_rows) @@ -799,42 +972,47 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: else "" ), ) - primary_comparison = [] + attempt_comparison = [] for engine in ENGINE_LABELS: - row = by_attempt.get((engine, task_id, 1)) - if not row: - continue - result_record = read_public_json( - Path((row.get("paths") or {}).get("evaluation", "")) - ) - event_record = read_public_json( - Path((row.get("paths") or {}).get("events", "")) - ) - step_count = len( - trajectory(event_record if isinstance(event_record, list) else []) - ) - failed_rules = [ - item["name"] - for item in evaluator_summary(result_record) - if float(item.get("score") or 0) < 1 - ] - primary_comparison.append( - [ - ENGINE_LABELS[engine], - _status(row), - Safe(_json_pre(sanitize(row.get("agent_response") or {}))), - "、".join(failed_rules) or "无", - _failure_diagnosis(result_record, row), - Safe( - f'查看 {step_count} 步轨迹' - ), - ] - ) + for attempt_no in (1, 2): + row = by_attempt.get((engine, task_id, attempt_no)) + if not row: + continue + result_record = read_public_json( + Path((row.get("paths") or {}).get("evaluation", "")) + ) + event_record = read_public_json( + Path((row.get("paths") or {}).get("events", "")) + ) + step_count = len( + trajectory(event_record if isinstance(event_record, list) else []) + ) + attempt_comparison.append( + [ + ENGINE_LABELS[engine], + "首轮" if attempt_no == 1 else "失败诊断重试", + _status(row), + Safe(_json_pre(sanitize(row.get("agent_response") or {}))), + "、".join(_failed_rule_labels(result_record)) or "无", + _failure_diagnosis(result_record, row), + Safe( + f'查看 {step_count} 步轨迹' + ), + ] + ) detail += _section( - "首轮结果对照", + "全部试次结果对照", _table( - ["浏览器", "结果", "实际最终回答", "失败规则", "为什么这样判", "执行过程"], - primary_comparison, + [ + "浏览器", + "口径", + "结果", + "实际最终回答", + "失败规则", + "为什么这样判", + "执行过程", + ], + attempt_comparison, ), ) (stage / f"datasets/{task_id}.html").write_text( @@ -1222,7 +1400,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) reproduce += _section( "公开复现入口", - '

    lexmount/browser-eval 是代码、题集和锁文件的唯一来源。

    ./examples/browser_agent/run.sh\nmake browser-agent-review EVIDENCE="$BROWSER_AGENT_EVIDENCE"\nmake browser-agent-report EVIDENCE="$BROWSER_AGENT_EVIDENCE"\nmake browser-agent-report-check EVIDENCE="$BROWSER_AGENT_EVIDENCE"

    每个试次的脱敏回答、evaluator、事件和轨迹位于 artifacts/<attempt-id>/,也可从试次详情页直接下载。

    ', + '

    lexmount/browser-eval 是代码、题集和锁文件的唯一来源。

    ./examples/browser_agent/run.sh\nmake browser-agent-review EVIDENCE="$BROWSER_AGENT_EVIDENCE"\nmake browser-agent-report EVIDENCE="$BROWSER_AGENT_EVIDENCE"\nmake browser-agent-report-check EVIDENCE="$BROWSER_AGENT_EVIDENCE"

    每个试次的脱敏回答、evaluator、事件和轨迹位于 artifacts/<attempt-id>/;evidence-index.json 给出全部 70 个试次的页面与下载路径。

    ', ) (stage / "reproduce/index.html").write_text( _page(title="环境与复现", active="reproduce", body=reproduce, depth=1), encoding="utf-8" @@ -1283,6 +1461,31 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: "".join(json.dumps(row, ensure_ascii=False) + "\n" for row in public_attempts), encoding="utf-8", ) + _write_json( + stage / "data/evidence-index.json", + { + "schema": "lex_browser_agent_public_evidence_index/1", + "attempts": [ + { + "attempt_id": row["id"], + "engine": row["engine"], + "task_id": row["task_id"], + "attempt": row["attempt"], + "score": row["score"], + "task_page": f"datasets/{row['task_id']}.html", + "attempt_page": f"attempts/{row['id']}.html", + "artifacts": { + "agent_response": f"artifacts/{row['id']}/agent-response.json", + "evaluation": f"artifacts/{row['id']}/evaluation.json", + "agent_events": f"artifacts/{row['id']}/agent-events.json", + "trajectory": f"artifacts/{row['id']}/trajectory.json", + "evidence_metadata": f"artifacts/{row['id']}/evidence-metadata.json", + }, + } + for row in public_attempts + ], + }, + ) audit = { "schema": "lex_browser_agent_report/2", "attempt_count": len(attempts), diff --git a/research/report-changes.json b/research/report-changes.json index 512292083..6a9966f1b 100644 --- a/research/report-changes.json +++ b/research/report-changes.json @@ -10,7 +10,7 @@ "attempts_sha256": "517b613adcc40fc0ebe41af2ab2115d76f49faaba5378b426cc196b5b633e9c7", "selection_sha256": "ba3c344df003dd4eba3a5d1ef97162ee98df2ed728f7c29c4188bc4247982d38", "changes": [ - ["证据应用与独立解读", "只有栏目分页仍不足以核对任务选择、逐次执行、复核覆盖和结论依据。", "首页作为决策入口;30 个任务页公开官方 ground truth 与两浏览器首轮对照;70 个试次页公开实际回答、规则级 expected/actual、CowAgent 自主轨迹、输出摘录和脱敏事件下载。", "出版结构与公开证据粒度改变;任务、分母、原始输出、分数与证据绑定均未修改。原始 HAR 与 Playwright trace 因含登录态只公开哈希和大小。"], + ["证据应用与独立解读", "只有栏目分页仍不足以核对任务选择、逐次执行、复核覆盖、当前快照可完成性和结论依据。", "首页作为决策入口;30 个任务页公开官方 ground truth、可完成性证据与全部试次对照;70 个试次页公开实际回答、精确 assertion、规则级 expected/actual、CowAgent 自主轨迹、输出摘录和脱敏事件下载;增加英文摘要、证据索引和筛选状态保持。", "出版结构与公开证据粒度改变;任务、分母、原始输出、分数与证据绑定均未修改。25 题有本轮成功轨迹,5 题只有官方合同;原始 HAR 与 Playwright trace 因含登录态只公开哈希和大小。"], ["明确自主 Agent 轨道", "仓库同时存在执行层 browser_kernel harness 和自主 browser_agent harness,若统称为“原 30 题”会误读运行方式。", "本报告的 30 个 WebArena-Verified 任务明确由 CowAgent 自主观察、规划和操作;browser_kernel 作为另一条执行层微基准单独说明。", "本轮成绩只来自自主 Agent 轨道;两类 harness 不合并计分。"], ["公开 benchmark 选型", "模型厂商使用的 browser-use、computer-use、离线动作预测和 agentic search 口径混杂。", "按一手机构采用情况梳理 Mind2Web、WebArena、WebVoyager,并比较 Online-Mind2Web、BU Bench、Odysseys 等评测对象和 judge。", "首轮选择可自托管、确定性结果验收的 WebArena-Verified;实时网站和 LLM judge 留到后续泛化阶段。"], ["完整正式运行", "需要同时覆盖短、中、长操作链,且避免运行后换题或挑结果。", "预注册 5/10/15 题操作长度分层;每个题×浏览器只以第一次正式执行计 pass@1,失败最多重试一次且只作诊断。", "Chromium 与 Moli 均为 25/30;共同失败 5 题、单边胜出 0 题,30 题 pilot 未观察到成功率差异。"], diff --git a/scripts/check_browser_agent_report.py b/scripts/check_browser_agent_report.py index 9b1301b83..1047c7400 100644 --- a/scripts/check_browser_agent_report.py +++ b/scripts/check_browser_agent_report.py @@ -34,6 +34,7 @@ def check( "adoption/index.html", "reproduce/index.html", "accounts/index.html", + "en/index.html", "engines/chromium.html", "engines/moli.html", ) @@ -70,6 +71,7 @@ def check( "environment.json", "review.json", "publication-privacy.json", + "evidence-index.json", ) ) artifact_files = tuple( @@ -124,9 +126,14 @@ def check( page: page.read_text(encoding="utf-8") for page in (*pages, *task_pages, *attempt_pages) } if task_pages and any( - "Ground truth · 官方验收合同" not in rendered_pages[page] for page in task_pages + "Ground truth · 官方验收合同" not in rendered_pages[page] + or "参考状态 · 当前环境是否有成功路径证据" not in rendered_pages[page] + or "全部试次结果对照" not in rendered_pages[page] + for page in task_pages ): - raise ValueError("every task page must publish its official evaluation contract") + raise ValueError( + "every task page must publish its contract, solvability status and all attempts" + ) if attempt_pages and any( marker not in rendered_pages[page] for page in attempt_pages @@ -156,6 +163,14 @@ def check( ] if len(public_results) != len(rows): raise ValueError("public result projection must cover every measured attempt") + evidence_index = json.loads( + (report_dir / "data/evidence-index.json").read_text(encoding="utf-8") + ) + indexed_attempts = evidence_index.get("attempts") or [] + if {item.get("attempt_id") for item in indexed_attempts} != { + page.stem for page in attempt_pages + }: + raise ValueError("public evidence index must cover every measured attempt exactly once") forbidden = { "agent_response", "paths", diff --git a/tests/browser_agent/test_publication.py b/tests/browser_agent/test_publication.py index 4f79a711a..2d2380d6b 100644 --- a/tests/browser_agent/test_publication.py +++ b/tests/browser_agent/test_publication.py @@ -5,12 +5,14 @@ def test_sanitize_redacts_credentials_origins_and_local_paths() -> None: value = sanitize( { "password": "hello1234", + "response_cookies": None, "url": "http://host.docker.internal:8023/dashboard/todos", "message": "Bearer abc.def from /Users/example/run/output.json", } ) assert value["password"] == "[REDACTED]" + assert value["response_cookies"] is None assert value["url"] == "__GITLAB__/dashboard/todos" assert value["message"] == "Bearer [REDACTED] from " diff --git a/tests/browser_agent/test_report.py b/tests/browser_agent/test_report.py index 21de0284c..9c244e8d3 100644 --- a/tests/browser_agent/test_report.py +++ b/tests/browser_agent/test_report.py @@ -1,7 +1,7 @@ import json from pathlib import Path -from harness.browser_agent.report import _paired_summary, build_report +from harness.browser_agent.report import _failure_diagnosis, _paired_summary, build_report def test_paired_summary_uses_primary_attempts_only() -> None: @@ -68,6 +68,9 @@ def test_report_keeps_primary_score_and_retry_recovery_separate(tmp_path: Path) task = (output / "datasets/44.html").read_text(encoding="utf-8") assert "Ground truth · 官方验收合同" in task assert "没有唯一参考点击路径" in task + assert "参考状态 · 当前环境是否有成功路径证据" in task + assert "全部试次结果对照" in task + assert "本轮已有成功轨迹" in (output / "datasets/index.html").read_text(encoding="utf-8") attempt = (output / "attempts/chromium-44-a1.html").read_text(encoding="utf-8") for expected in ( "Agent 实际回答", @@ -160,14 +163,42 @@ def test_report_renders_measured_runtime_identities(tmp_path: Path) -> None: "adoption/index.html", "reproduce/index.html", "accounts/index.html", + "en/index.html", "engines/chromium.html", "engines/moli.html", ): page = output / name assert page.is_file() - if name != "guide/article.html": + if name not in {"guide/article.html", "en/index.html"}: assert "切换主题" in page.read_text(encoding="utf-8") assert len(list((output / "datasets").glob("[0-9]*.html"))) == 30 assert (output / "guide/article.html").read_text(encoding="utf-8").find(" None: + result = { + "evaluators_results": [ + { + "evaluator_name": "AgentResponseEvaluator", + "score": 0, + "assertions": [ + { + "assertion_msgs": [ + "Array is missing 1 expected element(s). Matched (1/2)" + ] + } + ], + } + ] + } + + diagnosis = _failure_diagnosis(result, {}) + + assert "规则 1 AgentResponseEvaluator" in diagnosis + assert "Array is missing 1 expected element(s)" in diagnosis diff --git a/tests/browser_agent/test_report_ui.mjs b/tests/browser_agent/test_report_ui.mjs new file mode 100644 index 000000000..a9cbb858f --- /dev/null +++ b/tests/browser_agent/test_report_ui.mjs @@ -0,0 +1,77 @@ +import assert from 'node:assert/strict'; +import fs from 'node:fs'; +import vm from 'node:vm'; + +const source = fs.readFileSync( + new URL('../../harness/browser_agent/report.py', import.meta.url), + 'utf8', +); +const match = source.match(/REPORT_JS = """([\s\S]*?)"""\nREPORT_STYLE/); +assert.ok(match, 'browser-agent report JavaScript must remain extractable'); + +const controls = [ + {dataset: {filter: 'result'}, value: '', addEventListener() {}}, + {dataset: {filter: 'engine'}, value: '', addEventListener() {}}, +]; +const search = {value: '', addEventListener() {}}; +const rows = [ + {dataset: {tier: 'hard', result: 'fail', engine: 'moli'}, textContent: 'missing item'}, + {dataset: {tier: 'easy', result: 'pass', engine: 'chromium'}, textContent: 'complete'}, +]; +const count = {textContent: ''}; +const listeners = {}; +const storage = new Map(); +let replaced = ''; +const location = { + href: 'https://example.test/analysis/?f_result=fail&q=missing', + pathname: '/analysis/', + search: '?f_result=fail&q=missing', + hash: '', +}; +const document = { + documentElement: {dataset: {}}, + addEventListener(name, callback) { listeners[name] = callback; }, + querySelectorAll(selector) { + if (selector === '[data-filter]') return controls; + if (selector === '[data-search]') return [search]; + if (selector === 'tbody tr[data-tier]') return rows; + if (selector === '[data-visible-count]') return [count]; + return []; + }, + querySelector(selector) { return selector === '[data-search]' ? search : null; }, + getElementById() { return null; }, +}; +const context = vm.createContext({ + URL, + URLSearchParams, + document, + location, + history: {replaceState(_state, _title, url) { replaced = url; }}, + localStorage: {getItem() { return null; }, setItem() {}}, + sessionStorage: { + getItem(key) { return storage.get(key) ?? null; }, + setItem(key, value) { storage.set(key, value); }, + }, + requestAnimationFrame(callback) { callback(); }, + scrollTo() {}, + scrollY: 240, + addEventListener(name, callback) { listeners[name] = callback; }, +}); + +vm.runInContext(match[1], context); +listeners.DOMContentLoaded(); +assert.equal(search.value, 'missing'); +assert.equal(controls[0].value, 'fail'); +assert.deepEqual(rows.map(row => row.hidden), [false, true]); +assert.equal(count.textContent, '1 / 2'); + +controls[0].value = 'pass'; +search.value = ''; +vm.runInContext('filterRows()', context); +assert.deepEqual(rows.map(row => row.hidden), [true, false]); +assert.match(replaced, /f_result=pass/); +assert.doesNotMatch(replaced, /q=/); + +listeners.pagehide(); +assert.equal(storage.get('browser-agent-scroll:/analysis/?f_result=fail&q=missing'), '240'); +console.log('PASS: browser-agent filters restore from URL, update shareable state, and save scroll'); From ac79556728782db46006ea4b8cca9ed95ef40e58 Mon Sep 17 00:00:00 2001 From: kevin Date: Wed, 9 Sep 2026 21:51:35 +0800 Subject: [PATCH 029/127] Format browser-agent report implementation --- harness/browser_agent/publication.py | 1 - harness/browser_agent/report.py | 98 +++++++++++++++------------ scripts/check_browser_agent_report.py | 4 +- tests/browser_agent/test_report.py | 10 +-- 4 files changed, 60 insertions(+), 53 deletions(-) diff --git a/harness/browser_agent/publication.py b/harness/browser_agent/publication.py index 0e24cedda..eb0470cd1 100644 --- a/harness/browser_agent/publication.py +++ b/harness/browser_agent/publication.py @@ -8,7 +8,6 @@ from pathlib import Path from typing import Any - _ORIGIN_REPLACEMENTS = { "http://host.docker.internal:7770": "__SHOPPING__", "http://host.docker.internal:7780/admin": "__SHOPPING_ADMIN__", diff --git a/harness/browser_agent/report.py b/harness/browser_agent/report.py index ce0c48807..b0d3dc4d6 100644 --- a/harness/browser_agent/report.py +++ b/harness/browser_agent/report.py @@ -215,7 +215,9 @@ def _resolve_tasks(selection: PilotSelection) -> list[dict[str, Any]]: benchmark = _configure_benchmark() tasks = validate_against_official(selection, benchmark) - official = {int(task["task_id"]): benchmark.get_task(int(task["task_id"])) for task in tasks} + official = { + int(task["task_id"]): benchmark.get_task(int(task["task_id"])) for task in tasks + } except Exception: tasks = [ { @@ -238,7 +240,9 @@ def _resolve_tasks(selection: PilotSelection) -> list[dict[str, Any]]: task["contract"] = contract evals = contract.get("evaluation_contract") or [] expected = (evals[0].get("expected") or {}) if evals else {} - task["task_type"] = str(expected.get("task_type") or _task_type(int(task["task_id"]))).upper() + task["task_type"] = str( + expected.get("task_type") or _task_type(int(task["task_id"])) + ).upper() return tasks @@ -356,16 +360,18 @@ def _task_contract_html(task: dict[str, Any]) -> str: evaluator = rule.get("evaluator") or "UnknownEvaluator" expected = rule.get("expected") or {} qualifiers = { - key: value - for key, value in rule.items() - if key not in {"evaluator", "expected"} + key: value for key, value in rule.items() if key not in {"evaluator", "expected"} } cards.append( '

    ' + _e(f"规则 {index} · {evaluator}") - + "

    程序化验收该规则必须得分 1.0。

    " + + '

    程序化验收该规则必须得分 1.0。

    ' + _json_pre(expected) - + (f"
    匹配选项与结果 schema{_json_pre(qualifiers)}
    " if qualifiers else "") + + ( + f"
    匹配选项与结果 schema{_json_pre(qualifiers)}
    " + if qualifiers + else "" + ) + "
    " ) return ( @@ -418,10 +424,7 @@ def _failure_diagnosis(result: dict[str, Any] | None, row: dict[str, Any]) -> st response = row.get("agent_response") or {} if response.get("error_details"): parts.append(str(sanitize(response["error_details"]))) - return ( - ";".join(dict.fromkeys(x.strip() for x in parts if x.strip())).rstrip("。.;; ") - + "。" - ) + return ";".join(dict.fromkeys(x.strip() for x in parts if x.strip())).rstrip("。.;; ") + "。" def _failed_rule_labels(result: dict[str, Any] | None) -> list[str]: @@ -446,13 +449,17 @@ def _evaluator_result_html(result: dict[str, Any] | None) -> str: } cards.append( f'

    规则 {item["index"]} · {_e(item["name"])}

    ' - f'

    {"通过" if passed else "失败"} · score={_e(item.get("score"))}

    ' + f"

    {'通过' if passed else '失败'} · score={_e(item.get('score'))}

    " '

    官方期望

    ' + _json_pre(item.get("expected")) - + '

    实际归一化结果

    ' + + "

    实际归一化结果

    " + _json_pre(item.get("actual")) + "
    " - + (f"
    断言与错误{_json_pre(detail)}
    " if detail else "") + + ( + f"
    断言与错误{_json_pre(detail)}
    " + if detail + else "" + ) + "
    " ) return "".join(cards) @@ -468,11 +475,11 @@ def _trajectory_html(steps: list[dict[str, Any]]) -> str: if duration is not None: meta += f" · {float(duration):.2f} 秒" rows.append( - '
  • ' + "
  • " + _e(trajectory_label(step)) + f'

    {meta}

    调用参数

    ' + _json_pre(step.get("arguments")) - + '

    执行输出节选

    ' + + "

    执行输出节选

    " + f'
    {_e(result_excerpt(step))}
    ' + "
  • " ) @@ -553,7 +560,7 @@ def _filters(*, attempts: bool = False) -> str: '' '' + + ">失败" + solvability + extra + '
    ' @@ -704,7 +711,9 @@ def _failure_inventory(attempts: list[dict[str, Any]], tasks: list[dict[str, Any return _table(["首轮试次", "任务", "失败规则", "直接原因", "同题对照"], rows) -def _engine_attempts(engine: str, attempts: list[dict[str, Any]], tasks: list[dict[str, Any]]) -> str: +def _engine_attempts( + engine: str, attempts: list[dict[str, Any]], tasks: list[dict[str, Any]] +) -> str: meta = {int(task["task_id"]): task for task in tasks} rows = [] for row in attempts: @@ -745,9 +754,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: int(row["task_id"]) for row in attempts if float(row.get("score") or 0) == 1 } unproven_ids = sorted( - int(task["task_id"]) - for task in tasks - if int(task["task_id"]) not in measured_solvable_ids + int(task["task_id"]) for task in tasks if int(task["task_id"]) not in measured_solvable_ids ) protocol, environment = manifest.get("protocol") or {}, manifest.get("environment") or {} model, run_date = ( @@ -839,11 +846,11 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) english += '
    ' + _section( "Scope", - '

    The suite contains 5 easy, 10 medium, and 15 hard project tiers, defined before execution by estimated minimum visible UI actions. These are operation-length strata, not official WebArena difficulty labels. Every primary failure receives at most one diagnostic retry, which never changes pass@1.

    ', + "

    The suite contains 5 easy, 10 medium, and 15 hard project tiers, defined before execution by estimated minimum visible UI actions. These are operation-length strata, not official WebArena difficulty labels. Every primary failure receives at most one diagnostic retry, which never changes pass@1.

    ", ) english += '
    ' + _section( "How tasks are judged", - '

    WebArena-Verified does not prescribe one canonical click path. A task passes only when every configured deterministic evaluator scores 1.0: AgentResponseEvaluator checks the normalized final JSON, while NetworkEventEvaluator checks required observable requests or side effects. There is no LLM-as-a-judge and no human score override.

    ', + "

    WebArena-Verified does not prescribe one canonical click path. A task passes only when every configured deterministic evaluator scores 1.0: AgentResponseEvaluator checks the normalized final JSON, while NetworkEventEvaluator checks required observable requests or side effects. There is no LLM-as-a-judge and no human score override.

    ", ) english += '
    ' + _section( "Task and attempt evidence", @@ -902,16 +909,21 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: "公开采用信号 Top 3", '

    Mind2Web 家族 · 5 家

    OpenAI、Anthropic、Google DeepMind、THUDM / ChatGLM、Browser Use。原版是离线动作预测,Online 与 2.0 的目标和判分不同。

    WebArena 家族 · 3 家

    OpenAI、Anthropic、THUDM / ChatGLM。自托管、可重置、结果验收,更适合浏览器 A/B。

    WebVoyager · 2 家

    OpenAI、Google DeepMind。真实网站直观,但网页漂移与 LLM judge 降低精确复现性。

    采用次数是公开一手材料中的机构数,不是质量分;未找到 DeepSeek、Kimi 公开的一手 UI browser-use benchmark,不等于内部没有评测。

    ', ) - datasets += _section( - "Mind2Web 的拆分不是 easy / medium / hard", - "

    原始 Mind2Web 使用 cross-task、cross-website、cross-domain 衡量泛化;Online‑Mind2Web 没有统一三档;Mind2Web 2 的 short / medium / long 按动作跨度划分。本报告的简单 5、中等 10、困难 15 是预注册的操作长度层级。

    ", - ) + _section( - "怎样阅读任务 ground truth", - '

    WebArena‑Verified 的 ground truth 不是一条固定点击脚本。每题声明一个结构化最终回答合同,并在需要确认页面副作用时附加一个或多个网络事件合同。所有合同都通过才记 1 分。任务详情同时展示合同、两种浏览器的实际回答、失败规则和 CowAgent 自主轨迹。

    ', - ) + _section( - "参考状态与可完成性证据", - f'

    {len(measured_solvable_ids)}/30 题在本轮至少有一条自主成功轨迹,直接证明当前环境存在可完成路径;{len(unproven_ids)}/30 题只有官方验收合同而没有本轮成功轨迹(任务 {", ".join(f"#{task_id}" for task_id in unproven_ids) or "无"})。后者仍可稳定判分,但本轮不能独立证明当前快照可完成,也不能仅凭共同失败归因浏览器。

    “有成功路径证据”不等于存在唯一标准路径;WebArena‑Verified 不要求复刻人工动作序列。

    ', - ) + _section("30 个任务", _task_table(tasks, attempts)) + datasets += ( + _section( + "Mind2Web 的拆分不是 easy / medium / hard", + "

    原始 Mind2Web 使用 cross-task、cross-website、cross-domain 衡量泛化;Online‑Mind2Web 没有统一三档;Mind2Web 2 的 short / medium / long 按动作跨度划分。本报告的简单 5、中等 10、困难 15 是预注册的操作长度层级。

    ", + ) + + _section( + "怎样阅读任务 ground truth", + "

    WebArena‑Verified 的 ground truth 不是一条固定点击脚本。每题声明一个结构化最终回答合同,并在需要确认页面副作用时附加一个或多个网络事件合同。所有合同都通过才记 1 分。任务详情同时展示合同、两种浏览器的实际回答、失败规则和 CowAgent 自主轨迹。

    ", + ) + + _section( + "参考状态与可完成性证据", + f'

    {len(measured_solvable_ids)}/30 题在本轮至少有一条自主成功轨迹,直接证明当前环境存在可完成路径;{len(unproven_ids)}/30 题只有官方验收合同而没有本轮成功轨迹(任务 {", ".join(f"#{task_id}" for task_id in unproven_ids) or "无"})。后者仍可稳定判分,但本轮不能独立证明当前快照可完成,也不能仅凭共同失败归因浏览器。

    “有成功路径证据”不等于存在唯一标准路径;WebArena‑Verified 不要求复刻人工动作序列。

    ', + ) + + _section("30 个任务", _task_table(tasks, attempts)) + ) (stage / "datasets/index.html").write_text( _page(title="评测集", active="datasets", body=datasets, depth=1), encoding="utf-8" ) @@ -1109,7 +1121,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ], ], ) - + '

    合取规则:一题配置的每个 evaluator 都必须得分 1.0,整题才通过。这里没有 LLM-as-a-judge,也没有人工改分。

    ', + + "

    合取规则:一题配置的每个 evaluator 都必须得分 1.0,整题才通过。这里没有 LLM-as-a-judge,也没有人工改分。

    ", ) (stage / "methodology/index.html").write_text( _page(title="评测方法", active="methodology", body=methodology, depth=1), @@ -1150,12 +1162,12 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) analysis += _section( "首轮失败定位", - '

    两种浏览器在同五题失败。下表不是人工猜测:失败环节来自官方 evaluator,补充说明来自 Agent 最终错误和执行记录。

    ' + "

    两种浏览器在同五题失败。下表不是人工猜测:失败环节来自官方 evaluator,补充说明来自 Agent 最终错误和执行记录。

    " + _failure_inventory(attempts, tasks), ) analysis += _section( "70 个试次", - '

    默认只显示失败;可按浏览器、层级、站点、任务类型、首轮/重试、失败环节和全文关键词筛选。

    ' + "

    默认只显示失败;可按浏览器、层级、站点、任务类型、首轮/重试、失败环节和全文关键词筛选。

    " + _attempt_table(attempts, tasks), ) (stage / "analysis/index.html").write_text( @@ -1217,12 +1229,12 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) detail += '
    ' + _section( "官方 evaluator 逐规则判定", - '

    所有规则都必须得分 1.0;这里使用 evaluator 已归一化的实际值与期望值直接对照。

    ' + "

    所有规则都必须得分 1.0;这里使用 evaluator 已归一化的实际值与期望值直接对照。

    " + _evaluator_result_html(evaluation_record), ) detail += '
    ' + _section( "CowAgent 自主执行轨迹", - '

    以下顺序来自本次执行的 Agent 事件流,不是固定动作回放。展开可查看调用参数和输出摘录;完整脱敏日志可下载。

    ' + "

    以下顺序来自本次执行的 Agent 事件流,不是固定动作回放。展开可查看调用参数和输出摘录;完整脱敏日志可下载。

    " + _trajectory_html(steps), ) detail += '
    ' + _section( @@ -1231,12 +1243,12 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: + '

    公开包包含脱敏的回答、evaluator、事件流与轨迹。原始 HAR 和 Playwright trace 可能携带登录态、Cookie、页面快照或请求正文,因此不公开;其字节数和 SHA-256 保留用于证明证据绑定。

    ' + _table(["指标", "值"], resource.items()) + '

    整次 Agent 运行的容器计量只作诊断;不同进程模型下不直接比较内存或 PID。

    ' - + '

    证据完整性检查

    ' + + "

    证据完整性检查

    " + _table( ["检查", "结果"], [[key, "通过" if value else "未通过"] for key, value in checks.items()], ) - + '

    原始证据文件绑定

    ' + + "

    原始证据文件绑定

    " + _table( ["证据", "存在", "字节", "SHA-256", "公开策略"], [ @@ -1261,7 +1273,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: [ Safe( f'' - f'{x["engine"]}-{int(x["task_id"])}-a{int(x["attempt"])}' + f"{x['engine']}-{int(x['task_id'])}-a{int(x['attempt'])}" ), x["outcome"], sum(not v for v in (x.get("checks") or {}).values()), @@ -1281,7 +1293,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: review_page += _section( "复核协议", _json_pre(review.get("protocol") or {}) - + '

    人工或二次模型不得修改官方分数;复核只验证文件存在、哈希绑定、状态/分数一致、官方版本一致和传输尝试计数。

    ', + + "

    人工或二次模型不得修改官方分数;复核只验证文件存在、哈希绑定、状态/分数一致、官方版本一致和传输尝试计数。

    ", ) review_page += _section( "逐试次覆盖", @@ -1364,7 +1376,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) body += _section( "全部执行记录", - '

    首轮决定主分;仅失败任务有一次诊断重试,重试不回写主分。

    ' + "

    首轮决定主分;仅失败任务有一次诊断重试,重试不回写主分。

    " + _engine_attempts(engine, attempts, tasks), ) (stage / f"engines/{engine}.html").write_text( diff --git a/scripts/check_browser_agent_report.py b/scripts/check_browser_agent_report.py index 1047c7400..342bbe391 100644 --- a/scripts/check_browser_agent_report.py +++ b/scripts/check_browser_agent_report.py @@ -250,7 +250,9 @@ def check( if path.is_file() and path.suffix in {".html", ".json", ".jsonl", ".js", ".css"}: text = path.read_text(encoding="utf-8") if any(secret in text for secret in forbidden_text): - raise ValueError(f"published evidence contains a secret or local origin: {path}") + raise ValueError( + f"published evidence contains a secret or local origin: {path}" + ) def main() -> None: diff --git a/tests/browser_agent/test_report.py b/tests/browser_agent/test_report.py index 9c244e8d3..b34b52a62 100644 --- a/tests/browser_agent/test_report.py +++ b/tests/browser_agent/test_report.py @@ -175,9 +175,7 @@ def test_report_renders_measured_runtime_identities(tmp_path: Path) -> None: assert (output / "guide/article.html").read_text(encoding="utf-8").find(" None: "evaluator_name": "AgentResponseEvaluator", "score": 0, "assertions": [ - { - "assertion_msgs": [ - "Array is missing 1 expected element(s). Matched (1/2)" - ] - } + {"assertion_msgs": ["Array is missing 1 expected element(s). Matched (1/2)"]} ], } ] From 013633fc46398577952a7e1d53f24c0f7d60f589 Mon Sep 17 00:00:00 2001 From: kevin Date: Thu, 10 Sep 2026 12:33:17 +0800 Subject: [PATCH 030/127] Restructure browser agent evidence report --- Makefile | 2 +- evaluation/browser-agent/current-report.json | 33 ++ harness/browser_agent/report.py | 363 +++++++++---- harness/browser_agent/report_i18n.py | 491 ++++++++++++++++++ pyproject.toml | 1 + research/report-changes.json | 34 ++ scripts/check_browser_agent_report.py | 46 +- scripts/publish_browser_agent_artifact_hub.py | 62 ++- tests/browser_agent/test_report.py | 1 - tests/browser_agent/test_report_ui.mjs | 19 + 10 files changed, 941 insertions(+), 111 deletions(-) create mode 100644 evaluation/browser-agent/current-report.json create mode 100644 harness/browser_agent/report_i18n.py diff --git a/Makefile b/Makefile index f3fe8703c..bce2ce9b8 100644 --- a/Makefile +++ b/Makefile @@ -47,5 +47,5 @@ browser-agent-review: runtime-check browser-agent-report-check: runtime-check @test -n "$(EVIDENCE)" || (echo 'EVIDENCE=/absolute/path is required' >&2; exit 2) - $(PY) scripts/check_browser_agent_report.py reports/browser-agent --evidence "$(EVIDENCE)" + $(PY) scripts/check_browser_agent_report.py reports/browser-agent --evidence "$(EVIDENCE)" --require-complete $(PY) scripts/check_report_links.py reports/browser-agent diff --git a/evaluation/browser-agent/current-report.json b/evaluation/browser-agent/current-report.json new file mode 100644 index 000000000..c85384288 --- /dev/null +++ b/evaluation/browser-agent/current-report.json @@ -0,0 +1,33 @@ +{ + "schema": "lex_browser_agent_current_report/1", + "benchmark": "WebArena-Verified", + "benchmark_version": "1.2.3", + "task_count": 30, + "selection_sha256": "ba3c344df003dd4eba3a5d1ef97162ee98df2ed728f7c29c4188bc4247982d38", + "official_data_checksum": "d65275660814663375028e9017e1f929e3c38321041b125795e2713b52243d30", + "engines": [ + "chromium", + "moli" + ], + "model": "gpt-5.6-sol", + "reasoning_effort": "low", + "primary_attempts_per_engine_task": 1, + "failure_retries": 1, + "model_transport_retries": 1, + "scoring_authority": "unmodified official programmatic evaluator", + "review_kind": "deterministic evidence integrity audit", + "independent_semantic_review": false, + "tasks_without_measured_success_path": [ + 68, + 124, + 142, + 168, + 506 + ], + "publication": { + "base_url": "https://artifact-hub.dev.lexmount.net", + "site_slug": "28OH4YZkUYF1", + "share_url": "https://artifact-hub.dev.lexmount.net/v/5Vhnhdc_f8FEM2RzctQm-ChRlXUf174Y", + "state": "verified" + } +} diff --git a/harness/browser_agent/report.py b/harness/browser_agent/report.py index b0d3dc4d6..cedc87135 100644 --- a/harness/browser_agent/report.py +++ b/harness/browser_agent/report.py @@ -27,6 +27,7 @@ trajectory, trajectory_label, ) +from .report_i18n import write_english_report from .selection import PilotSelection, load_pilot, validate_against_official ENGINE_LABELS = {"chromium": "Chromium", "moli": "Moli 1.1.3"} @@ -39,12 +40,12 @@ } MUTATE_IDS = {399, 404, 441, 506} NAVIGATE_IDS = {44, 45, 118} -FAILURE_NOTES = { - 68: "多帖聚合不完整:四次执行都只返回一本书。", - 124: "把全局价格范围当成目标商品范围。", - 142: "未完成目标月份与品类的完整订单汇总。", - 168: "个人项目筛选返回空列表,官方答案非空。", - 506: "购物车最终状态不满足唯一目标商品;重试也未在步数上限内完成。", +FAILURE_OBSERVATIONS = { + 68: "官方期望包含多本书;四次执行的实际回答都只包含一本。", + 124: "实际回答中的价格范围与该题官方目标商品范围不一致。", + 142: "实际订单汇总与官方期望值不一致。", + 168: "实际回答为空列表,而官方期望为非空项目列表。", + 506: "官方网络副作用合同未通过;购物车最终状态未满足题目要求。", } @@ -85,6 +86,10 @@ def _number(value: float | int | None) -> str: return "—" if value is None else f"{value:,.0f}" +def _probability(value: float | None) -> str: + return "—" if value is None else f"{value:.3f}" + + def _table(headers: list[str], rows: Iterable[Iterable[Any]], *, caption: str = "") -> str: head = "".join(f"{_e(x)}" for x in headers) body = "".join( @@ -109,6 +114,7 @@ def _public_attempt(row: dict[str, Any]) -> dict[str, Any]: usage, resource = row.get("model_usage") or {}, row.get("resource") or {} return { "id": _attempt_id(row), + "run_id": str(row.get("run_id") or ""), "engine": str(row["engine"]), "task_id": int(row["task_id"]), "tier": str(row["tier"]), @@ -209,16 +215,28 @@ def _paired_summary(rows: list[dict[str, Any]]) -> dict[str, Any]: return {"paired": len(ids), **counts, "mcnemar_exact_p": p} -def _resolve_tasks(selection: PilotSelection) -> list[dict[str, Any]]: +def _resolve_tasks( + selection: PilotSelection, *, expected_data_checksum: str | None = None +) -> list[dict[str, Any]]: try: from .runner import _configure_benchmark benchmark = _configure_benchmark() + if expected_data_checksum: + dataset_path = Path(benchmark.config.test_data_file) + actual_data_checksum = _sha256(dataset_path) + if actual_data_checksum != expected_data_checksum: + raise ValueError( + "installed WebArena-Verified data checksum differs from measured evidence: " + f"{actual_data_checksum} != {expected_data_checksum}" + ) tasks = validate_against_official(selection, benchmark) official = { int(task["task_id"]): benchmark.get_task(int(task["task_id"])) for task in tasks } except Exception: + if expected_data_checksum: + raise tasks = [ { "task_id": t.task_id, @@ -238,7 +256,12 @@ def _resolve_tasks(selection: PilotSelection) -> list[dict[str, Any]]: official_task = official.get(int(task["task_id"])) if "official" in locals() else None contract = task_contract(official_task) if official_task is not None else {} task["contract"] = contract + task["contract_sha256"] = hashlib.sha256( + json.dumps(contract, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode() + ).hexdigest() evals = contract.get("evaluation_contract") or [] + if expected_data_checksum and not evals: + raise ValueError(f"official task {task['task_id']} has no evaluator contract") expected = (evals[0].get("expected") or {}) if evals else {} task["task_type"] = str( expected.get("task_type") or _task_type(int(task["task_id"])) @@ -246,6 +269,153 @@ def _resolve_tasks(selection: PilotSelection) -> list[dict[str, Any]]: return tasks +def _wilson_interval(successes: int, total: int, z: float = 1.96) -> tuple[float, float] | None: + if total <= 0: + return None + p = successes / total + denominator = 1 + z**2 / total + centre = (p + z**2 / (2 * total)) / denominator + spread = z * math.sqrt((p * (1 - p) + z**2 / (4 * total)) / total) / denominator + return max(0.0, centre - spread), min(1.0, centre + spread) + + +def _task_family_summary( + tasks: list[dict[str, Any]], attempts: list[dict[str, Any]] +) -> list[dict[str, Any]]: + primary = { + (str(row["engine"]), int(row["task_id"])): float(row["score"]) == 1 + for row in attempts + if int(row["attempt"]) == 1 + } + result = [] + for kind in ("RETRIEVE", "NAVIGATE", "MUTATE"): + selected = [task for task in tasks if task["task_type"] == kind] + result.append( + { + "kind": kind, + "count": len(selected), + "scores": { + engine: sum(primary.get((engine, int(task["task_id"])), False) for task in selected) + for engine in ENGINE_LABELS + }, + } + ) + return result + + +def _adoption_top3(evidence: dict[str, Any]) -> str: + families = sorted( + (item for item in evidence.get("benchmark_families") or [] if item.get("interaction")), + key=lambda item: (-len(item.get("adopters") or []), str(item.get("family") or "")), + )[:3] + if len(families) != 3 or not evidence.get("verified_at"): + raise ValueError("benchmark adoption evidence must provide a dated interaction Top 3") + cards = [] + for item in families: + adopters = item.get("adopters") or [] + note = item.get("warning") or { + "WebArena": "自托管、可重置、结果验收,更适合浏览器 A/B。", + "WebVoyager": "真实网站直观,但网页漂移与 LLM judge 降低精确复现性。", + }.get(str(item.get("family")), "采用数不代表评测质量。") + cards.append( + '
    ' + f'

    {_e(item.get("family"))} 家族 · {len(adopters)} 家

    ' + f'

    {_e("、".join(adopters))}。{_e(note)}

    ' + ) + return ( + '
    ' + + "".join(cards) + + '

    采用次数来自公开一手材料中的机构去重计数,不是质量分;' + f'证据核对日期 {_e(evidence.get("verified_at") or "未记录")}。未找到 DeepSeek、Kimi ' + "公开的一手 UI browser-use benchmark,不等于内部没有评测。

    " + ) + + +def _paired_task_table(tasks: list[dict[str, Any]], attempts: list[dict[str, Any]]) -> str: + primary = { + (str(row["engine"]), int(row["task_id"])): row + for row in attempts + if int(row["attempt"]) == 1 + } + rows = [] + for task in tasks: + task_id = int(task["task_id"]) + chromium = primary.get(("chromium", task_id)) + moli = primary.get(("moli", task_id)) + if not chromium or not moli: + continue + c_pass, m_pass = float(chromium["score"]) == 1, float(moli["score"]) == 1 + relation = ( + "both_pass" + if c_pass and m_pass + else "chromium_only" + if c_pass + else "moli_only" + if m_pass + else "both_fail" + ) + relation_label = { + "both_pass": "共同通过", + "chromium_only": "仅 Chromium", + "moli_only": "仅 Moli", + "both_fail": "共同失败", + }[relation] + site = str(task["sites"][0]) + + def metrics(row: dict[str, Any]) -> str: + events = read_public_json(Path((row.get("paths") or {}).get("events", ""))) + step_count = len(trajectory(events if isinstance(events, list) else [])) + usage = row.get("model_usage") or {} + return ( + f'{_status(row)}
    {step_count} 步 · ' + f'{int(usage.get("request_count") or 0)} 次模型请求 · ' + f'{_number(int(usage.get("total_tokens") or 0))} tokens · ' + f'{_seconds(float(row.get("duration_ms") or 0))}' + ) + + search = " ".join( + (str(task_id), task["rationale"], str(task.get("intent") or ""), relation_label) + ) + attrs = ( + f' data-relation="{relation}" data-tier="{_e(task["tier"])}"' + f' data-site="{_e(site)}" data-kind="{_e(task["task_type"])}"' + f' data-search="{_e(search)}"' + ) + rows.append( + f"#{task_id}" + f'
    {_e(task["rationale"])}
    ' + f"{_e(TIER_LABELS[task['tier']])}" + f"{_e(SITE_LABELS.get(site, site))}{_e(task['task_type'])}" + f"{metrics(chromium)}{metrics(moli)}" + f'{_e(relation_label)}' + f'查看两种浏览器全部试次' + ) + controls = ( + '
    ' + '' + '' + '' + '' + '
    ' + ) + return ( + controls + + '
    ' + '' + '' + + "".join(rows) + + "
    任务层级站点类型ChromiumMoli 1.1.3对照结论证据
    " + ) + + def _group_rows( attempts: list[dict[str, Any]], tasks: list[dict[str, Any]], field: str ) -> list[list[Any]]: @@ -273,26 +443,27 @@ def _group_rows( function filterRows(save=true){const controls=[...document.querySelectorAll('[data-filter]')],query=(document.querySelector('[data-search]')?.value||'').trim().toLowerCase();let visible=0,total=0;document.querySelectorAll('tbody tr[data-tier]').forEach(r=>{total++;const hidden=Boolean(controls.some(c=>c.value&&r.dataset[c.dataset.filter]!==c.value)||(query&&!String(r.dataset.search||r.textContent).toLowerCase().includes(query)));r.hidden=hidden;if(!hidden)visible++});document.querySelectorAll('[data-visible-count]').forEach(x=>x.textContent=visible+' / '+total);if(save)updateViewUrl()} function applySort(t,col,dir){const b=t?.tBodies[0];if(!b)return;[...b.rows].sort((a,z)=>{const av=a.cells[col]?.dataset.sort||a.cells[col]?.textContent||'',zv=z.cells[col]?.dataset.sort||z.cells[col]?.textContent||'';const an=Number(av.replace(/[^0-9.-]/g,'')),zn=Number(zv.replace(/[^0-9.-]/g,''));const cmp=!Number.isNaN(an)&&!Number.isNaN(zn)?an-zn:av.localeCompare(zv,'zh-CN');return dir==='asc'?cmp:-cmp}).forEach(r=>b.appendChild(r));t.dataset.col=String(col);t.dataset.dir=dir} function sortTable(id,col){const t=document.getElementById(id);if(!t)return;const dir=t.dataset.col==col&&t.dataset.dir==='asc'?'desc':'asc';applySort(t,col,dir);const u=new URL(location.href);u.searchParams.set('sort',id+':'+col+':'+dir);history.replaceState(null,'',u.pathname+'?'+u.searchParams.toString()+u.hash)} -function restoreView(){const p=new URLSearchParams(location.search),q=document.querySelector('[data-search]');if(q&&p.has('q'))q.value=p.get('q')||'';document.querySelectorAll('[data-filter]').forEach(c=>{const v=p.get('f_'+c.dataset.filter);if(v!==null)c.value=v});const s=(p.get('sort')||'').split(':');if(s.length===3)applySort(document.getElementById(s[0]),Number(s[1]),s[2]);filterRows(false);try{const y=sessionStorage.getItem('browser-agent-scroll:'+location.pathname+location.search);if(y!==null)requestAnimationFrame(()=>scrollTo(0,Number(y)||0))}catch(e){}} -document.addEventListener('DOMContentLoaded',()=>{try{document.documentElement.dataset.theme=localStorage.getItem('browser-agent-theme')||'light'}catch(e){}restoreView();document.querySelectorAll('[data-filter]').forEach(x=>x.addEventListener('change',()=>filterRows()));document.querySelectorAll('[data-search]').forEach(x=>x.addEventListener('input',()=>filterRows()))});addEventListener('pagehide',()=>{try{sessionStorage.setItem('browser-agent-scroll:'+location.pathname+location.search,String(scrollY))}catch(e){}});""" +function pairRows(save=true){const controls=[...document.querySelectorAll('[data-pair-filter]')];let shown=0;document.querySelectorAll('#pair-rows tr').forEach(r=>{const hidden=controls.some(c=>{if(!c.value)return false;if(c.dataset.pairFilter==='relation'&&c.value==='discordant')return !['chromium_only','moli_only'].includes(r.dataset.relation);return r.dataset[c.dataset.pairFilter]!==c.value});r.hidden=hidden;if(!hidden)shown++});document.querySelectorAll('[data-pair-count]').forEach(x=>x.textContent=(document.documentElement.lang==='en'?'Showing ':'找到 ')+shown+(document.documentElement.lang==='en'?' paired tasks':' 个配对任务'));if(save){const u=new URL(location.href);[...u.searchParams.keys()].filter(k=>k.startsWith('p_')).forEach(k=>u.searchParams.delete(k));controls.forEach(c=>{if(c.value)u.searchParams.set('p_'+c.dataset.pairFilter,c.value)});history.replaceState(null,'',u.pathname+(u.search?'?'+u.searchParams.toString():'')+u.hash)}} +function restoreView(){const p=new URLSearchParams(location.search),q=document.querySelector('[data-search]');if(q&&p.has('q'))q.value=p.get('q')||'';document.querySelectorAll('[data-filter]').forEach(c=>{const v=p.get('f_'+c.dataset.filter);if(v!==null)c.value=v});document.querySelectorAll('[data-pair-filter]').forEach(c=>{const v=p.get('p_'+c.dataset.pairFilter);if(v!==null)c.value=v});const s=(p.get('sort')||'').split(':');if(s.length===3)applySort(document.getElementById(s[0]),Number(s[1]),s[2]);filterRows(false);pairRows(false);try{const y=sessionStorage.getItem('browser-agent-scroll:'+location.pathname+location.search);if(y!==null)requestAnimationFrame(()=>scrollTo(0,Number(y)||0))}catch(e){}} +document.addEventListener('DOMContentLoaded',()=>{try{document.documentElement.dataset.theme=localStorage.getItem('browser-agent-theme')||'light'}catch(e){}restoreView();document.querySelectorAll('[data-filter]').forEach(x=>x.addEventListener('change',()=>filterRows()));document.querySelectorAll('[data-search]').forEach(x=>x.addEventListener('input',()=>filterRows()));document.querySelectorAll('[data-pair-filter]').forEach(x=>x.addEventListener('change',()=>pairRows()))});addEventListener('pagehide',()=>{try{sessionStorage.setItem('browser-agent-scroll:'+location.pathname+location.search,String(scrollY))}catch(e){}});""" REPORT_STYLE = ( SHARED_STYLE + """ -body{overflow-x:hidden}.hero h1,.publication-article h1,.publication-article h2{overflow-wrap:anywhere;word-break:break-word} -.top nav{gap:18px;flex-wrap:nowrap;overflow-x:auto;overscroll-behavior-inline:contain;scrollbar-width:thin}.top nav>a,.top nav>button{flex:0 0 auto}.top nav a[aria-current=page]{color:var(--ink);font-weight:800;text-decoration:underline;text-decoration-thickness:2px;text-underline-offset:8px}.top .theme-toggle{margin-left:auto}.page-main{min-height:78vh}.hero.compact{padding-bottom:22px}.hero.compact h1{font-size:clamp(34px,4.5vw,52px)}.fine{font-size:13px;color:var(--muted)}.result-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px}.result-grid .card{border-top:3px solid var(--accent)}.filters{display:flex;gap:10px;flex-wrap:wrap;align-items:end;margin:18px 0}.filters select,.filters input{padding:9px 12px;border:1px solid var(--line);border-radius:9px;background:var(--panel);color:var(--ink)}.filters input{min-width:260px}.status-pass{color:var(--good);font-weight:750}.status-fail{color:var(--bad);font-weight:750}.status-unproven{color:var(--warn,var(--bad));font-weight:750}.flow{display:grid;grid-template-columns:repeat(6,1fr);gap:9px;list-style:none;padding:0}.flow li{padding:14px;border:1px solid var(--line);border-radius:11px;background:var(--panel);font-size:13px}.flow li+li:before{content:'→';color:var(--accent);margin-right:7px}.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:12px}.detail-grid .kv{min-height:92px}.page-links{display:flex;gap:10px;flex-wrap:wrap}.page-links a{border:1px solid var(--line);background:var(--panel);padding:8px 12px;border-radius:9px}.rank-list{counter-reset:rank}.rank-item{counter-increment:rank;display:grid;grid-template-columns:54px 1fr;gap:16px;padding:20px 0;border-top:1px solid var(--line)}.rank-item:before{content:counter(rank);font-size:34px;font-weight:850;color:var(--accent)}.rank-item h3{margin:4px 0}.article-entry{border-left:4px solid var(--accent)}.source-list li{margin:9px 0}.table-wrap{margin:18px 0}.table-wrap table{min-width:760px}#task-table{min-width:1280px}.table-wrap caption{text-align:left;padding:10px 12px;color:var(--muted);caption-side:bottom}.footer{border-top:1px solid var(--line)}.evidence-pair{display:grid;grid-template-columns:1fr 1fr;gap:14px}.evidence-card{border:1px solid var(--line);border-radius:12px;background:var(--panel);padding:16px;min-width:0}.evidence-card h3{margin-top:0}.json-view,.log-view{white-space:pre-wrap;overflow-wrap:anywhere;max-height:540px;overflow:auto;padding:14px;border:1px solid var(--line);border-radius:9px;background:var(--bg);font-size:12px;line-height:1.55}.trajectory{list-style:none;padding:0;counter-reset:step}.trajectory>li{counter-increment:step;display:grid;grid-template-columns:46px 1fr;gap:12px;padding:15px 0;border-top:1px solid var(--line)}.trajectory>li:before{content:counter(step);display:grid;place-items:center;width:34px;height:34px;border-radius:50%;background:var(--accent);color:white;font-weight:800}.trajectory summary{cursor:pointer;font-weight:750}.rule-fail{border-left:4px solid var(--bad)}.rule-pass{border-left:4px solid var(--good)}.count-note{margin-left:auto;color:var(--muted);font-size:13px}.badge{display:inline-block;padding:3px 8px;border-radius:999px;background:var(--wash);font-size:12px;margin-right:6px}.muted-box{padding:14px;border:1px dashed var(--line);border-radius:10px;color:var(--muted)}@media(max-width:900px){.result-grid{grid-template-columns:repeat(2,1fr)}.flow{grid-template-columns:repeat(3,1fr)}.evidence-pair{grid-template-columns:1fr}}@media(max-width:600px){.wrap{width:calc(100% - 28px)}.top nav{gap:14px}.result-grid,.flow{grid-template-columns:1fr}.flow li+li:before{content:'↓';display:block}.hero{padding-top:28px}.hero.compact h1{font-size:30px;line-height:1.18}.filters input{min-width:0;width:100%}}@media print{.top,.filters{display:none!important}.card,.table-wrap{box-shadow:none}.json-view,.log-view{max-height:none;overflow:visible}.evidence-pair{grid-template-columns:1fr}} +.hero h1,.publication-article h1,.publication-article h2{overflow-wrap:anywhere;word-break:break-word} +.top nav{gap:18px;flex-wrap:nowrap;overflow-x:auto;overscroll-behavior-inline:contain;scrollbar-width:thin}.top nav>a,.top nav>button{flex:0 0 auto}.top nav .brand{font-weight:850;color:var(--ink)}.top nav a[aria-current=page]{color:var(--ink);font-weight:800;text-decoration:underline;text-decoration-thickness:2px;text-underline-offset:8px}.top .theme-toggle{margin-left:auto}.page-main{min-height:78vh}.hero.compact{padding-bottom:22px}.hero.compact h1{font-size:clamp(34px,4.5vw,52px)}.fine{font-size:13px;color:var(--muted)}.result-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px}.result-grid .card{border-top:3px solid var(--accent)}.overview-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.overview-card{border:1px solid var(--line);border-radius:14px;background:var(--panel);padding:20px;min-width:0}.overview-card h3{margin:8px 0}.audit-strip{display:grid;gap:10px}.audit-strip .callout{margin:0}.filters,.pair-controls{display:flex;gap:10px;flex-wrap:wrap;align-items:end;margin:18px 0}.filters select,.filters input,.pair-controls select{padding:9px 12px;border:1px solid var(--line);border-radius:9px;background:var(--panel);color:var(--ink)}.filters input{min-width:260px}.comparison-count{margin-left:auto;padding:8px 12px;border-radius:999px;background:var(--wash);color:var(--accent);font-weight:750}.status-pass{color:var(--good);font-weight:750}.status-fail{color:var(--bad);font-weight:750}.status-unproven{color:var(--warn,var(--bad));font-weight:750}.flow{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:9px;list-style:none;padding:0}.flow li{padding:14px;border:1px solid var(--line);border-radius:11px;background:var(--panel);font-size:13px;min-width:0}.flow li+li:before{content:'→';color:var(--accent);margin-right:7px}.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:12px}.detail-grid .kv{min-height:92px;min-width:0}.page-links{display:flex;gap:10px;flex-wrap:wrap}.page-links a{border:1px solid var(--line);background:var(--panel);padding:8px 12px;border-radius:9px}.rank-list{counter-reset:rank}.rank-item{counter-increment:rank;display:grid;grid-template-columns:54px minmax(0,1fr);gap:16px;padding:20px 0;border-top:1px solid var(--line)}.rank-item:before{content:counter(rank);font-size:34px;font-weight:850;color:var(--accent)}.rank-item h3{margin:4px 0}.article-entry{border-left:4px solid var(--accent)}.source-list li{margin:9px 0}.table-wrap{margin:18px 0}.table-wrap table{min-width:760px}#task-table{min-width:1280px}#pair-table{min-width:1180px}.table-wrap caption{text-align:left;padding:10px 12px;color:var(--muted);caption-side:bottom}.footer{border-top:1px solid var(--line)}.evidence-pair{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:14px}.evidence-card{border:1px solid var(--line);border-radius:12px;background:var(--panel);padding:16px;min-width:0}.evidence-card h3{margin-top:0}.json-view,.log-view{white-space:pre-wrap;overflow-wrap:anywhere;max-height:540px;overflow:auto;padding:14px;border:1px solid var(--line);border-radius:9px;background:var(--bg);font-size:12px;line-height:1.55}.trajectory{list-style:none;padding:0;counter-reset:step}.trajectory>li{counter-increment:step;display:grid;grid-template-columns:46px minmax(0,1fr);gap:12px;padding:15px 0;border-top:1px solid var(--line)}.trajectory>li:before{content:counter(step);display:grid;place-items:center;width:34px;height:34px;border-radius:50%;background:var(--accent);color:white;font-weight:800}.trajectory summary{cursor:pointer;font-weight:750}.rule-fail{border-left:4px solid var(--bad)}.rule-pass{border-left:4px solid var(--good)}.count-note{margin-left:auto;color:var(--muted);font-size:13px}.badge{display:inline-block;padding:3px 8px;border-radius:999px;background:var(--wash);font-size:12px;margin-right:6px}.muted-box{padding:14px;border:1px dashed var(--line);border-radius:10px;color:var(--muted)}@media(max-width:900px){.result-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.overview-grid{grid-template-columns:1fr}.flow{grid-template-columns:repeat(3,minmax(0,1fr))}.evidence-pair{grid-template-columns:1fr}.grid>.card{grid-column:1/-1!important}}@media(max-width:600px){.wrap{width:calc(100% - 28px)}.top nav{gap:14px}.result-grid,.flow{grid-template-columns:1fr}.flow li+li:before{content:'↓';display:block}.hero{padding-top:28px}.hero.compact h1{font-size:30px;line-height:1.18}.filters input{min-width:0;width:100%}.comparison-count{margin-left:0}.rank-item{grid-template-columns:36px minmax(0,1fr)}}@media print{.top,.filters,.pair-controls{display:none!important}.card,.table-wrap{box-shadow:none}.json-view,.log-view{max-height:none;overflow:visible}.evidence-pair{grid-template-columns:1fr}} """ ) NAV = [ - ("", "首页", "home"), - ("guide/", "报告解读", "guide"), - ("changes/", "变更记录", "changes"), + ("", "Browser Use", "home"), ("datasets/", "评测集", "datasets"), ("methodology/", "评测方法", "methodology"), ("analysis/", "结果分析", "analysis"), ("review/", "核验记录", "review"), + ("guide/", "报告解读", "guide"), ("adoption/", "方法介绍", "adoption"), ("reproduce/", "环境与复现", "reproduce"), - ("accounts/", "账号", "accounts"), + ("changes/", "变更记录", "changes"), + ("accounts/", "账号与安装", "accounts"), ("en/", "English", "en"), ] @@ -300,7 +471,7 @@ def _group_rows( def _page(*, title: str, active: str, body: str, depth: int = 0) -> str: local_base = "../" * depth or "./" links = "".join( - f'{_e(label)}" for href, label, key in NAV @@ -502,9 +673,19 @@ def _artifact_links(attempt_id: str) -> str: def _article_body( paired: dict[str, Any], attempts: list[dict[str, Any]], tasks: list[dict[str, Any]] ) -> str: - failures = [[f"#{task_id}", FAILURE_NOTES[task_id]] for task_id in sorted(FAILURE_NOTES)] + summary = _primary_summary(attempts) + chromium, moli = summary["chromium"], summary["moli"] + failures = [ + [f"#{task_id}", FAILURE_OBSERVATIONS[task_id]] + for task_id in sorted(FAILURE_OBSERVATIONS) + ] return ( - '
    Browser Agent 实测解读

    25 比 25 不是“内核等价”:它真正暴露的是共同瓶颈

    同一 CowAgent 与同一模型替换浏览器后,30 个配对任务没有一个出现单边胜出。这个结果证明对照流程跑通,却没有为内核优劣提供方向性证据。

    测量口径:WebArena‑Verified 1.2.3;首轮 pass@1;失败最多一次诊断重试;官方确定性 evaluator;无 LLM judge。

    一、最重要的结果不是平分,而是零个不一致配对

    Chromium 与 Moli 都通过 25/30,首轮成功率都是 83.3%。逐题配对后,25 题共同通过、5 题共同失败,两个浏览器单边通过都是 0。精确 McNemar 双侧 p=1.000,但这里最有解释力的不是 p 值,而是检验没有拿到任何不一致样本。

    ' + '
    Browser Agent 实测解读
    ' + f'

    {chromium["passed"]} 比 {moli["passed"]} 不是“内核等价”:当前样本没有产生分歧

    ' + f'

    同一 CowAgent 与同一模型替换浏览器后,{paired["paired"]} 个配对任务没有一个出现单边胜出。这个结果证明对照流程跑通,却没有为内核优劣提供方向性证据。

    ' + '

    测量口径:WebArena‑Verified 1.2.3;首轮 pass@1;失败最多一次诊断重试;官方确定性 evaluator;无 LLM judge。

    ' + '

    一、最重要的结果不是平分,而是零个不一致配对

    ' + f'

    Chromium 与 Moli 分别通过 {chromium["passed"]}/{chromium["completed"]} 和 {moli["passed"]}/{moli["completed"]}。逐题配对后,{paired["both_pass"]} 题共同通过、{paired["both_fail"]} 题共同失败,两个浏览器单边通过都是 0。精确 McNemar 双侧 p={_probability(paired["mcnemar_exact_p"])},但这里最有解释力的不是 p 值,而是检验没有拿到任何不一致样本。

    ' + _table( ["配对结果", "任务数", "可支持的判断"], [ @@ -515,13 +696,15 @@ def _article_body( ], caption="表 1|30 个首轮任务的逐题配对;重试不并入主分。", ) - + "

    严谨表述只能是“本轮没有观察到成功率差异”,不能写成“两种内核等价”。30 题对小效应检出力有限;更关键的是,样本没有诱发内核分歧。

    二、五个共同失败指向检索聚合与最终状态,不是重试次数

    失败集中在四个信息聚合任务和一个购物车状态任务。十个失败试次各获得一次重试,恢复数仍为 0。继续增加重试只会提高成本并模糊 pass@1。

    " + + f'

    严谨表述只能是“本轮没有观察到成功率差异”,不能写成“两种内核等价”。{paired["paired"]} 题对小效应检出力有限;更关键的是,样本没有诱发内核分歧。

    ' + '

    二、共同失败是待核验线索,不是已证实根因

    ' + f'

    {paired["both_fail"]} 个共同失败任务的 {chromium["retry_count"] + moli["retry_count"]} 次诊断重试均未恢复。下表只陈述官方期望与实际结果的差异,不把差异直接归因给 Agent、浏览器或站点。

    ' + _table( ["任务", "公开诊断"], failures, caption="表 2|任务详情公开官方合同、实际回答、逐规则判定与执行轨迹。", ) - + "

    共同失败不能单独证明模型是唯一根因,因为 CowAgent 提示、工具抽象和步数上限也是共同变量;但它足以改变下一步优先级:先检查跨页收集、去重、范围约束和提交后状态验证。

    三、简单、中等、困难都覆盖了,但“难度”只是操作长度

    WebArena‑Verified 没有官方 easy/medium/hard 三档。本项目在运行前按最少可见 UI 操作数注册为 1–4、5–7、8+ 三档,配额 5/10/15。它控制操作链覆盖,不等同于推理难度。

    " + + "

    这五题在本轮没有任何成功轨迹,因此当前站点快照的可完成性也未被本报告独立证明。下一步应先做不参与计分的人工或 oracle 可完成性核验,再区分 Agent、工具抽象、步数上限、站点状态和浏览器因素。

    三、简单、中等、困难都覆盖了,但“难度”只是操作长度

    WebArena‑Verified 没有官方 easy/medium/hard 三档。本项目在运行前按最少可见 UI 操作数注册为 1–4、5–7、8+ 三档,配额 5/10/15。它控制操作链覆盖,不等同于推理难度。

    " + _table( ["本项目分层", "Chromium", "Moli"], _group_rows(attempts, tasks, "tier"), @@ -708,7 +891,7 @@ def _failure_inventory(attempts: list[dict[str, Any]], tasks: list[dict[str, Any Safe(f'任务与两浏览器对照'), ] ) - return _table(["首轮试次", "任务", "失败规则", "直接原因", "同题对照"], rows) + return _table(["首轮试次", "任务", "失败规则", "记录中的验收差异", "同题对照"], rows) def _engine_attempts( @@ -742,13 +925,16 @@ def _write_json(path: Path, value: Any) -> None: def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: selection = load_pilot(selection_path) - tasks = _resolve_tasks(selection) attempts = _read_attempts(evidence) public_attempts = [_public_attempt(r) for r in attempts] manifest, config, review = ( _read_json(evidence / name) for name in ("manifest.json", "run-config.json", "result-review.json") ) + official = (manifest.get("environment") or {}).get("official_evaluator") or {} + tasks = _resolve_tasks( + selection, expected_data_checksum=str(official.get("data_checksum") or "") or None + ) summary, paired = _primary_summary(attempts), _paired_summary(attempts) measured_solvable_ids = { int(row["task_id"]) for row in attempts if float(row.get("score") or 0) == 1 @@ -761,8 +947,14 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: str(protocol.get("model") or "未记录"), str(manifest.get("created_at") or "")[:10] or "未记录", ) + adoption_evidence = _read_json(Path("research/browser-agent-benchmark-adoption.json")) parent = output.parent parent.mkdir(parents=True, exist_ok=True) + marker = output / ".generated-report" + if output.exists() and ( + not marker.is_file() or marker.read_text(encoding="utf-8") != "browser-agent-report/2\n" + ): + raise ValueError(f"refusing to replace an unmarked report directory: {output}") stage = Path(tempfile.mkdtemp(prefix=f".{output.name}-", dir=parent)) try: for folder in ( @@ -778,7 +970,6 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: "adoption", "reproduce", "accounts", - "en", "data", "artifacts", ): @@ -787,6 +978,19 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: (stage / "assets/report.js").write_text(REPORT_JS, encoding="utf-8") article = _article_body(paired, attempts, tasks) c, m = summary["chromium"], summary["moli"] + c_ci = _wilson_interval(c["passed"], c["completed"]) or (None, None) + m_ci = _wilson_interval(m["passed"], m["completed"]) or (None, None) + families = _task_family_summary(tasks, attempts) + coverage = review.get("coverage") or {} + family_cards = "".join( + '
    ' + f'{_e(family["kind"])}' + f'

    {family["count"]} 个任务

    ' + f'

    Chromium {family["scores"]["chromium"]}/{family["count"]} · ' + f'Moli {family["scores"]["moli"]}/{family["count"]}

    ' + f'查看这类任务 →
    ' + for family in families + ) home = _hero( f"Browser Agent Evaluation · {run_date}", "同一自主 Agent、同一模型,只替换浏览器", @@ -794,7 +998,20 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) home += _section( "先看结论", - f'
    Chromium pass@1
    {c["passed"]}/{c["completed"]}

    {_pct(c["score"])}

    Moli pass@1
    {m["passed"]}/{m["completed"]}

    {_pct(m["score"])}

    单边胜出
    {paired["chromium_only"] + paired["moli_only"]}

    {paired["paired"]} 个完整配对

    失败重试恢复
    {c["recovered"] + m["recovered"]}/{c["retry_count"] + m["retry_count"]}

    不回写主分

    当前结论:本轮没有观察到浏览器成功率差异;这证明对照流程可比,不证明两个内核等价。五个共同失败更值得优先检查 Agent 的跨页聚合与最终状态验证。

    ', + f'
    Chromium pass@1
    {c["passed"]}/{c["completed"]}

    {_pct(c["score"])} · 95% CI {_pct(c_ci[0])}–{_pct(c_ci[1])}

    Moli pass@1
    {m["passed"]}/{m["completed"]}

    {_pct(m["score"])} · 95% CI {_pct(m_ci[0])}–{_pct(m_ci[1])}

    单边胜出
    {paired["chromium_only"] + paired["moli_only"]}

    {paired["paired"]} 个完整配对

    失败重试恢复
    {c["recovered"] + m["recovered"]}/{c["retry_count"] + m["retry_count"]}

    不回写主分

    当前结论:本轮没有观察到浏览器成功率差异;这证明对照流程可比,不证明两个内核等价。

    归因边界:{paired["both_fail"]} 个共同失败任务没有任何成功轨迹。报告只能确认结果未满足官方合同,尚不能证明任务在当前快照可完成,也不能归因给 Agent 或浏览器。

    ', + ) + home += _section( + "任务族覆盖", + '

    样本同时包含信息提取、页面导航和状态变更任务;类别来自每题官方最终答案合同。

    ' + f'
    {family_cards}
    ', + ) + home += _section( + "运行与证据是否完整", + '
    ' + f'

    正式调用:{len(attempts)} 个试次均保留回答、官方 evaluator 结果、Agent 事件和证据哈希;每题每浏览器首轮一次,只有失败任务再诊断重试一次。

    ' + f'

    证据完整性审计:{coverage.get("reviewed_attempts", 0)}/{coverage.get("recorded_attempts", len(attempts))} 个试次完成确定性一致性检查,{len(review.get("unresolved") or [])} 个未决。它不重新判题,也不是独立语义复核。

    ' + f'

    可完成性覆盖:{len(measured_solvable_ids)}/{len(tasks)} 题至少有一条自主成功轨迹;其余 {len(unproven_ids)} 题仅有官方验收合同。

    ' + '
    ', ) home += _section( "从结论进入证据", @@ -812,60 +1029,12 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ], caption="同一任务的两个首轮结果;重试不参与配对主分。", ) - + '

    精确 McNemar 双侧 p=1.000。零个不一致配对意味着当前样本无法估计内核方向性效应。

    ', + + f'

    精确 McNemar 双侧 p={_probability(paired["mcnemar_exact_p"])}。零个不一致配对意味着当前样本无法估计内核方向性效应。

    ', ) (stage / "index.html").write_text( _page(title="首页", active="home", body=home), encoding="utf-8" ) - english = _hero( - f"Browser Agent Evaluation · {run_date}", - "One autonomous agent and model; only the browser changes", - f"CowAgent with {model} (reasoning=low) ran the same 30 preregistered WebArena-Verified tasks on Chromium and Moli.", - ) - english += '
    ' + _section( - "Results", - _table( - ["Browser", "Primary passes", "pass@1", "Retry recoveries", "Median latency"], - [ - [ - ENGINE_LABELS[engine], - f"{summary[engine]['passed']}/{summary[engine]['completed']}", - _pct(summary[engine]["score"]), - f"{summary[engine]['recovered']}/{summary[engine]['retry_count']}", - ( - f"{summary[engine]['median_duration_ms'] / 1000:.1f} s" - if summary[engine]["median_duration_ms"] is not None - else "—" - ), - ] - for engine in ENGINE_LABELS - ], - ) - + f'

    Observed result: {paired["both_pass"]} tasks passed on both browsers, {paired["both_fail"]} failed on both, and {paired["chromium_only"] + paired["moli_only"]} had a one-browser-only pass. This run found no directional success-rate difference; it does not establish browser equivalence.

    ', - ) - english += '
    ' + _section( - "Scope", - "

    The suite contains 5 easy, 10 medium, and 15 hard project tiers, defined before execution by estimated minimum visible UI actions. These are operation-length strata, not official WebArena difficulty labels. Every primary failure receives at most one diagnostic retry, which never changes pass@1.

    ", - ) - english += '
    ' + _section( - "How tasks are judged", - "

    WebArena-Verified does not prescribe one canonical click path. A task passes only when every configured deterministic evaluator scores 1.0: AgentResponseEvaluator checks the normalized final JSON, while NetworkEventEvaluator checks required observable requests or side effects. There is no LLM-as-a-judge and no human score override.

    ", - ) - english += '
    ' + _section( - "Task and attempt evidence", - '

    Each task page publishes the official evaluation contract, whether this run contains a successful trajectory, every Chromium and Moli result, exact failed assertions, and links to the actual autonomous trajectory. Each attempt page publishes the final response, rule-level expected/actual values, BrowserTool calls, output excerpts, and downloadable sanitized records.

    ', - ) - english += '
    ' + _section( - "Independent integrity review", - f'

    {(review.get("coverage") or {}).get("reviewed_attempts", 0)}/{(review.get("coverage") or {}).get("recorded_attempts", 0)} attempts passed deterministic evidence checks, with {len(review.get("disagreements") or [])} score disagreements and {len(review.get("unresolved") or [])} unresolved records. The official evaluator remains the only scoring authority.

    Open the review inventory →

    ', - ) - english += '
    ' + _section( - "Access, privacy, and reproduction", - '

    The four benchmark sites are self-hosted and reset before each task. Model access is separate. The public bundle includes sanitized responses, evaluator records, events, trajectories, hashes, and runtime identities. Raw HAR and Playwright traces remain private because they may contain cookies and session state.

    ', - ) - (stage / "en/index.html").write_text(_english_page(english), encoding="utf-8") - guide = ( _hero( "Report interpretation", @@ -907,7 +1076,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) datasets += _section( "公开采用信号 Top 3", - '

    Mind2Web 家族 · 5 家

    OpenAI、Anthropic、Google DeepMind、THUDM / ChatGLM、Browser Use。原版是离线动作预测,Online 与 2.0 的目标和判分不同。

    WebArena 家族 · 3 家

    OpenAI、Anthropic、THUDM / ChatGLM。自托管、可重置、结果验收,更适合浏览器 A/B。

    WebVoyager · 2 家

    OpenAI、Google DeepMind。真实网站直观,但网页漂移与 LLM judge 降低精确复现性。

    采用次数是公开一手材料中的机构数,不是质量分;未找到 DeepSeek、Kimi 公开的一手 UI browser-use benchmark,不等于内部没有评测。

    ', + _adoption_top3(adoption_evidence), ) datasets += ( _section( @@ -979,8 +1148,8 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: "两种浏览器的全部正式试次", _table(["浏览器", "口径", "官方结果", "端到端耗时", "证据"], task_rows) + ( - f'

    共同失败诊断:{_e(FAILURE_NOTES[task_id])}

    ' - if task_id in FAILURE_NOTES + f'

    已观察到的验收差异:{_e(FAILURE_OBSERVATIONS[task_id])} 这不是根因归属。

    ' + if task_id in FAILURE_OBSERVATIONS else "" ), ) @@ -1160,9 +1329,14 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) + '

    耗时包含模型响应、页面等待、站点重置与浏览器执行,不能解释为纯内核速度。

    ', ) + analysis += '
    ' + _section( + "同题首轮关系对比", + "

    每行固定为同一任务的 Chromium 与 Moli 首轮。关系筛选只改变可见行,不产生新分数;步数、模型请求、tokens 和端到端耗时只用于诊断。

    " + + _paired_task_table(tasks, attempts), + ) analysis += _section( "首轮失败定位", - "

    两种浏览器在同五题失败。下表不是人工猜测:失败环节来自官方 evaluator,补充说明来自 Agent 最终错误和执行记录。

    " + f'

    两种浏览器在同 {paired["both_fail"]} 题失败。失败规则来自官方 evaluator;补充文字只复述期望、实际值和记录中的错误,不代表已确认根因。

    ' + _failure_inventory(attempts, tasks), ) analysis += _section( @@ -1217,7 +1391,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) detail += _section( "结果与运行", - f'
    官方结果{_status(row)}
    失败定位{_e(_failure_diagnosis(evaluation_record, row))}
    自主操作{len(steps)} 次 BrowserTool 调用
    端到端耗时{_seconds(public["duration_ms"])}
    模型 tokens{_number(usage["total_tokens"])}
    模型请求{usage["request_count"]} 次调用 / {usage["request_attempt_count"]} 次传输尝试
    传输错误{public["transport_error_count"]}
    证据复核{_e(item.get("outcome") or "未记录")}
    ', + f'
    官方结果{_status(row)}
    失败定位{_e(_failure_diagnosis(evaluation_record, row))}
    自主操作{len(steps)} 次 BrowserTool 调用
    端到端耗时{_seconds(public["duration_ms"])}
    模型 tokens{_number(usage["total_tokens"])}
    模型请求{usage["request_count"]} 次调用 / {usage["request_attempt_count"]} 次传输尝试
    传输错误{public["transport_error_count"]}
    Run ID{_e(public["run_id"] or "未记录")}
    证据完整性审计{_e(item.get("outcome") or "未记录")}
    ', ) detail += '
    ' + _section( "Ground truth 与实际最终回答", @@ -1282,13 +1456,13 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: for x in review.get("attempts") or [] ] review_page = _hero( - "Result review", - "70/70 个试次均完成确定性证据复核", - "复核不重新判题,也不是 LLM-as-a-judge;官方 evaluator 仍是唯一成绩来源。", + "Evidence integrity audit", + f'{coverage.get("reviewed_attempts", 0)}/{coverage.get("recorded_attempts", len(attempts))} 个试次完成证据完整性审计', + "这是确定性记录一致性检查,不是独立语义复核,也不是 LLM-as-a-judge;官方 evaluator 仍是唯一成绩来源。", ) review_page += _section( - "核验结论", - f'
    记录试次
    {coverage.get("recorded_attempts", 0)}
    已复核
    {coverage.get("reviewed_attempts", 0)}
    记分分歧
    {len(review.get("disagreements") or [])}
    未决
    {len(review.get("unresolved") or [])}

    检查回答、HAR、Agent trace、事件和 evaluator 明细是否存在,并核对哈希、分数、状态、官方版本和模型传输尝试。

    ', + "审计结论", + f'
    记录试次
    {coverage.get("recorded_attempts", 0)}
    已审计
    {coverage.get("reviewed_attempts", 0)}
    记录不一致
    {len(review.get("disagreements") or [])}
    未决
    {len(review.get("unresolved") or [])}

    检查回答、HAR、Agent trace、事件和 evaluator 明细是否存在,并核对哈希、分数、状态、官方版本和模型传输尝试。

    没有执行的工作:本轮没有第二名人工或模型对任务结果做盲审,因此不能把本页称为“独立结果复核”。

    ', ) review_page += _section( "复核协议", @@ -1420,7 +1594,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: accounts = _hero( "Accounts", "本地站点账号、模型权限与发布权限分开处理", - "这里只说明权限边界,不展示密钥、Cookie、用户名或个人信息。", + "不展示密钥、Cookie 或真实个人信息;官方合同中的内置测试邮箱和用户名作为合成 ground truth 保留。", ) + _section( "权限与费用", _table( @@ -1455,7 +1629,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: for e in ENGINE_LABELS }, }, - {"claim": "五个失败完全重合", "basis": sorted(FAILURE_NOTES)}, + {"claim": "首轮失败任务完全重合", "basis": sorted(FAILURE_OBSERVATIONS)}, ], "bound_attempts_sha256": _sha256(evidence / "attempts.jsonl"), "selection_sha256": selection.sha256, @@ -1590,12 +1764,27 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: "local service origins replaced with benchmark URL placeholders", "machine-local filesystem paths", ], + "retained_test_fixtures": [ + "synthetic usernames and email addresses that are part of official task contracts", + "public benchmark task identifiers and expected values", + ], }, ) + write_english_report(stage) (stage / ".generated-report").write_text("browser-agent-report/2\n", encoding="utf-8") + backup: Path | None = None if output.exists(): - shutil.rmtree(output) - stage.rename(output) + backup = Path(tempfile.mkdtemp(prefix=f".{output.name}-backup-", dir=parent)) + backup.rmdir() + output.rename(backup) + try: + stage.rename(output) + except Exception: + if backup is not None and backup.exists() and not output.exists(): + backup.rename(output) + raise + if backup is not None: + shutil.rmtree(backup) except Exception: shutil.rmtree(stage, ignore_errors=True) raise diff --git a/harness/browser_agent/report_i18n.py b/harness/browser_agent/report_i18n.py new file mode 100644 index 000000000..5fa8d2f85 --- /dev/null +++ b/harness/browser_agent/report_i18n.py @@ -0,0 +1,491 @@ +"""Build a route-for-route English mirror of the browser-agent report.""" + +from __future__ import annotations + +import html +import posixpath +import re +import shutil +from html.parser import HTMLParser +from pathlib import Path, PurePosixPath +from urllib.parse import urlsplit, urlunsplit + +from harness.web_fetch.report_i18n import translate_text as translate_common_text + +NAV = ( + ("index.html", "Browser Use"), + ("datasets/index.html", "Dataset"), + ("methodology/index.html", "Methodology"), + ("analysis/index.html", "Results"), + ("review/index.html", "Review"), + ("guide/index.html", "Interpretation"), + ("adoption/index.html", "Methods"), + ("reproduce/index.html", "Reproduction"), + ("changes/index.html", "Changes"), + ("accounts/index.html", "Accounts and setup"), +) + + +TEXT = { + "首页 · Browser Use 评测": "Home · Browser Use evaluation", + "Browser Use 评测": "Browser Use evaluation", + "同一自主 Agent、同一模型,只替换浏览器": "One autonomous agent and model; only the browser changes", + "本轮没有观察到浏览器成功率差异;这证明对照流程可比,不证明两个内核等价。": "This run found no browser success-rate difference. It establishes a comparable controlled run, not browser equivalence.", + "归因边界:": "Attribution boundary:", + "任务族覆盖": "Task-family coverage", + "样本同时包含信息提取、页面导航和状态变更任务;类别来自每题官方最终答案合同。": "The sample covers retrieval, navigation, and state-changing tasks. Each category comes from the task's official outcome contract.", + "查看这类任务 →": "View this task family →", + "运行与证据是否完整": "Run and evidence completeness", + "正式调用:": "Formal executions:", + "证据完整性审计:": "Evidence integrity audit:", + "可完成性覆盖:": "Measured-solvability coverage:", + "任务数": "Tasks", + "打开逐题横向对照 →": "Open the task-by-task comparison →", + "记录中的验收差异": "Observed acceptance mismatch", + "已观察到的验收差异:": "Observed acceptance mismatch:", + "证据完整性审计": "Evidence integrity audit", + "已审计": "Audited", + "记录不一致": "Record inconsistencies", + "审计结论": "Audit result", + "先看结论": "Headline result", + "当前结论:": "Current conclusion:", + "本轮没有观察到浏览器成功率差异;这证明对照流程可比,不证明两个内核等价。五个共同失败更值得优先检查 Agent 的跨页聚合与最终状态验证。": "This run found no browser success-rate difference. It establishes a comparable controlled run, not browser equivalence. The five shared failures make cross-page aggregation and final-state verification the first agent-level issues to investigate.", + "单边胜出": "One-browser-only wins", + "个完整配对": "complete pairs", + "失败重试恢复": "Failure retry recoveries", + "不回写主分": "Excluded from pass@1", + "调用与核验状态": "Execution and review status", + "调用完整:": "Execution complete:", + "次计划首轮均有结果;失败项的唯一诊断重试也全部完成。": "planned primary runs have results; every single diagnostic retry after a failure is also complete.", + "独立核验:": "Independent review:", + "个试次;未决": "attempts; unresolved", + "。查看核验轮次 →": ". View review history →", + "三类任务概览": "Three task families", + "只在相同任务族与同一批任务内比较;RETRIEVE、NAVIGATE 与 MUTATE 的合同不同。": "Compare browsers within the same task family and task cohort. RETRIEVE, NAVIGATE, and MUTATE use different acceptance contracts.", + "个任务": "tasks", + "两种浏览器": "Both browsers", + "本类首轮": "primary runs in this family", + "浏览器总览": "Browser overview", + "首轮完成": "Primary coverage", + "首轮通过": "Primary passes", + "失败重试": "Failure retries", + "审查结果": "Review result", + "已核验": "reviewed", + "从结论进入证据": "From result to evidence", + "对零分歧、共同失败和下一轮设计的完整论证。": "A complete interpretation of zero disagreements, shared failures, and the next evaluation design.", + "阅读完整解读 →": "Read the interpretation →", + "筛选并下钻两种浏览器试次。": "Filter tasks and inspect both browsers' attempts.", + "打开任务库 →": "Open the task library →", + "首轮、唯一重试和核验状态。": "Primary runs, the single retry, and review status.", + "打开结果库 →": "Open all results →", + "逐题配对比总分更重要": "Paired tasks are more informative than the aggregate score", + "同一任务的两个首轮结果;重试不参与配对主分。": "Both primary results for each task; retries do not enter the paired primary score.", + "配对结果": "Pair outcome", + "两者都通过": "Both pass", + "仅 Chromium": "Chromium only", + "仅 Moli": "Moli only", + "两者都失败": "Both fail", + "本轮未观察到": "Not observed", + "优先排查共同链路": "Inspect the shared stack first", + "精确 McNemar 双侧 p=1.000。零个不一致配对意味着当前样本无法估计内核方向性效应。": "Exact two-sided McNemar p=1.000. With no discordant pairs, this sample cannot estimate a directional browser effect.", + "结论只适用于本轮预注册样本、Agent、模型与运行配置;展示调整不改变测量结果。": "Conclusions apply only to this preregistered sample, agent, model, and runtime configuration. Presentation changes do not alter measured results.", + "候选 benchmark、选择依据与 30 个预注册任务": "Benchmark candidates, selection rationale, and 30 preregistered tasks", + "先区分最终结果、动作过程和模型裁判,再谈采用率;同一家族不同变体的成绩不能合并。": "First separate final-outcome evaluation, action prediction, and model judging. Scores from different variants of one benchmark family are not interchangeable.", + "公开采用信号 Top 3": "Top three public adoption signals", + "采用次数是公开一手材料中的机构数,不是质量分;未找到 DeepSeek、Kimi 公开的一手 UI browser-use benchmark,不等于内部没有评测。": "Counts are institutions found in primary public sources, not quality scores. No primary public UI browser-use benchmark was found for DeepSeek or Kimi; that does not show they have no internal evaluation.", + "Mind2Web 的拆分不是 easy / medium / hard": "Mind2Web splits are not easy, medium, and hard", + "原始 Mind2Web 使用 cross-task、cross-website、cross-domain 衡量泛化;Online‑Mind2Web 没有统一三档;Mind2Web 2 的 short / medium / long 按动作跨度划分。本报告的简单 5、中等 10、困难 15 是预注册的操作长度层级。": "Original Mind2Web uses cross-task, cross-website, and cross-domain splits. Online-Mind2Web has no common three-level difficulty split. Mind2Web 2 uses short, medium, and long action spans. This report's 5/10/15 easy, medium, and hard groups were preregistered by estimated visible operation length.", + "怎样阅读任务 ground truth": "How to read task ground truth", + "参考状态与可完成性证据": "Reference status and solvability evidence", + "“有成功路径证据”不等于存在唯一标准路径;WebArena‑Verified 不要求复刻人工动作序列。": "Evidence of a successful path does not imply one canonical path. WebArena-Verified does not require replaying a human action sequence.", + "30 个任务": "30 tasks", + "全文搜索": "Full-text search", + "任务、意图、答案、错误": "Task, intent, answer, or error", + "层级": "Tier", + "站点": "Site", + "类型": "Type", + "可完成性": "Solvability", + "本轮已有成功轨迹": "Successful trajectory observed", + "仅官方合同": "Official contract only", + "任务": "Task", + "最少步": "Minimum steps", + "入选理由": "Selection rationale", + "官方任务意图": "Official task intent", + "可完成性证据": "Solvability evidence", + "简单": "Easy", + "中等": "Medium", + "困难": "Hard", + "通过": "Pass", + "失败": "Fail", + "首轮": "Primary", + "重试": "Retry", + "失败诊断重试": "Diagnostic retry", + "无": "None", + "官方任务合同、两种浏览器的实际结果与完整自主执行证据。": "Official task contract, both browsers' measured outcomes, and complete autonomous execution evidence.", + "任务身份": "Task identity", + "本项目层级": "Project tier", + "任务类型": "Task type", + "官方 hard subset": "Official hard subset", + "本轮可完成性证据": "Solvability evidence in this run", + "已有成功轨迹": "Successful trajectory observed", + "尚无成功轨迹": "No successful trajectory observed", + "公开任务意图": "Public task intent", + "官方来源:": "Official source:", + "Ground truth · 官方验收合同": "Ground truth · official acceptance contract", + "没有唯一参考点击路径。": "There is no single reference click path.", + "WebArena‑Verified 允许 Agent 自主选择任意 UI 路径;ground truth 是以下最终回答与必要网络副作用的合取。报告因此比较结果合同,而不把某条人工路径当作标准答案。": "WebArena-Verified lets the agent choose any UI path. Ground truth is the conjunction of the final-answer and required network-side-effect contracts below. The report therefore compares outcome contracts rather than treating a human path as the answer key.", + "程序化验收": "Programmatic acceptance", + "该规则必须得分 1.0。": "This rule must score 1.0.", + "匹配选项与结果 schema": "Matching options and result schema", + "参考状态 · 当前环境是否有成功路径证据": "Reference status · evidence of a successful path in the current environment", + "有。": "Yes.", + "本轮至少一个自主试次通过全部官方合同,因此当前站点快照存在已实测的可完成路径。成功轨迹不是唯一标准路径,也不要求另一浏览器复刻。": "At least one autonomous attempt passed every official contract, so this site snapshot has a measured successful path. That trajectory is not the only valid path, and the other browser need not reproduce it.", + "本轮没有。": "Not in this run.", + "官方任务包给出了确定性验收合同,但两种浏览器的首轮和唯一诊断重试都未通过。因此本报告不能独立证明当前快照存在可完成路径,也不能把失败直接归因给浏览器;需要另做不参与计分的人工或 oracle 可完成性核验。": "The official task bundle provides a deterministic acceptance contract, but both browsers failed the primary run and the single diagnostic retry. This report therefore cannot independently prove that the current snapshot is solvable or attribute the failure directly to the browser. A separate non-scoring human or oracle readiness check is required.", + "两种浏览器的全部正式试次": "All formal attempts from both browsers", + "口径": "Attempt class", + "官方结果": "Official verdict", + "端到端耗时": "End-to-end latency", + "证据": "Evidence", + "查看试次": "View attempt", + "共同失败诊断:": "Shared-failure diagnosis:", + "全部试次结果对照": "All-attempt comparison", + "实际最终回答": "Actual final answer", + "失败规则": "Failed rules", + "为什么这样判": "Verdict rationale", + "执行过程": "Execution", + "评最终结果,不评参考点击路径": "Evaluate final outcomes, not a reference click path", + "唯一成绩来源是未修改的 WebArena‑Verified 1.2.3 evaluator;确定性证据审计只验证记录一致性。": "The unmodified WebArena-Verified 1.2.3 evaluator is the only scoring authority. Deterministic evidence review checks record integrity only.", + "候选 benchmark 的验收边界": "Acceptance boundaries of candidate benchmarks", + "评什么": "What it evaluates", + "验收对象": "Acceptance target", + "判分": "Scoring", + "问题": "Limit", + "本轮判分链": "Scoring chain for this run", + "预注册 30 题": "Preregister 30 tasks", + "逐题重置站点": "Reset sites per task", + "CowAgent 自主规划": "CowAgent plans autonomously", + "只替换浏览器": "Change only the browser", + "官方 evaluator 验收": "Run official evaluators", + "确定性证据复核": "Review deterministic evidence", + "固定协议": "Fixed protocol", + "项目": "Item", + "规则": "Rule", + "唯一变量": "Only changed variable", + "主分": "Primary metric", + "排除": "Exclusions", + "裁判": "Judge", + "两类程序化 evaluator 到底检查什么": "What the two programmatic evaluators check", + "输入": "Input", + "比较方式": "Comparison", + "能证明什么": "What it establishes", + "不能证明什么": "What it does not establish", + "合取规则:": "Conjunction rule:", + "从总体配对下钻到每一次执行": "Drill down from paired outcomes to every execution", + "先定位十个首轮失败的规则与直接原因,再筛选全部 70 次执行并下钻回答、轨迹和日志。": "First locate the exact failed rules for ten primary failures, then filter all 70 executions and inspect answers, trajectories, and logs.", + "汇总": "Summary", + "浏览器": "Browser", + "耗时中位数": "Median latency", + "耗时 P95": "P95 latency", + "Tokens 中位数": "Median tokens", + "耗时包含模型响应、页面等待、站点重置与浏览器执行,不能解释为纯内核速度。": "Latency includes model responses, page waits, site resets, and browser execution. It is not isolated browser-core speed.", + "同题首轮关系对比": "Paired primary-outcome comparison", + "每行固定为同一任务的 Chromium 与 Moli 首轮。关系筛选只改变可见行,不产生新分数。": "Each row fixes Chromium and Moli primary runs for the same task. Relationship filters change visible rows only and do not create new scores.", + "关系": "Relationship", + "共同通过": "Both pass", + "共同失败": "Both fail", + "有分歧": "Disagreement", + "找到": "Showing", + "个配对任务": "paired tasks", + "对照结论": "Pair outcome", + "查看两种浏览器全部试次": "View all attempts for both browsers", + "首轮失败定位": "Primary-failure inventory", + "两种浏览器在同五题失败。下表不是人工猜测:失败环节来自官方 evaluator,补充说明来自 Agent 最终错误和执行记录。": "Both browsers failed the same five tasks. The failure stage comes from the official evaluators; the additional diagnosis comes from the agent's final error and execution record.", + "首轮试次": "Primary attempt", + "直接原因": "Recorded mismatch", + "同题对照": "Same-task comparison", + "任务与两浏览器对照": "Task and both browsers", + "70 个试次": "70 attempts", + "默认只显示失败;可按浏览器、层级、站点、任务类型、首轮/重试、失败环节和全文关键词筛选。": "Failures are shown by default. Filter by browser, tier, site, task type, primary/retry, failure stage, or full-text query.", + "失败环节": "Failure stage", + "最终回答": "Final answer", + "网络副作用": "Network side effect", + "多项": "Multiple stages", + "试次": "Attempt", + "传输重试": "Transport retries", + "从官方 ground truth、实际回答、规则级判定下钻到 CowAgent 的每一步 BrowserTool 调用。": "Follow the official ground truth, actual answer, and rule-level verdict down to every CowAgent BrowserTool call.", + "答案对照": "Answer comparison", + "规则判定": "Rule verdicts", + "执行轨迹": "Trajectory", + "运行与证据": "Runtime and evidence", + "结果与运行": "Outcome and runtime", + "失败定位": "Failure diagnosis", + "自主操作": "Autonomous actions", + "模型 tokens": "Model tokens", + "模型请求": "Model requests", + "传输错误": "Transport errors", + "证据复核": "Evidence review", + "Ground truth 与实际最终回答": "Ground truth and actual final answer", + "官方任务合同": "Official task contract", + "Agent 实际回答": "Agent's actual answer", + "官方 evaluator 逐规则判定": "Official evaluator verdicts by rule", + "所有规则都必须得分 1.0;这里使用 evaluator 已归一化的实际值与期望值直接对照。": "Every rule must score 1.0. This section directly compares the evaluator's normalized actual value with the expected value.", + "官方期望": "Official expectation", + "实际归一化结果": "Normalized actual result", + "断言与错误": "Assertions and errors", + "CowAgent 自主执行轨迹": "CowAgent autonomous trajectory", + "以下顺序来自本次执行的 Agent 事件流,不是固定动作回放。展开可查看调用参数和输出摘录;完整脱敏日志可下载。": "This sequence comes from the run's agent event stream, not a fixed action replay. Expand a step for arguments and an output excerpt; the complete sanitized log is downloadable.", + "调用参数": "Call arguments", + "执行输出节选": "Execution output excerpt", + "运行、日志与证据": "Runtime, logs, and evidence", + "最终回答 JSON": "Final answer JSON", + "Evaluator 明细 JSON": "Evaluator details JSON", + "动作轨迹 JSON": "Trajectory JSON", + "脱敏事件日志 JSON": "Sanitized event log JSON", + "原始证据哈希": "Raw-evidence hashes", + "指标": "Metric", + "值": "Value", + "证据完整性检查": "Evidence completeness checks", + "检查": "Check", + "原始证据文件绑定": "Raw evidence file bindings", + "存在": "Present", + "字节": "Bytes", + "公开策略": "Publication policy", + "返回任务页": "Return to task", + "个试次均完成确定性证据复核": "attempts received deterministic evidence review", + "复核不重新判题,也不是 LLM-as-a-judge;官方 evaluator 仍是唯一成绩来源。": "Review does not rescore tasks and is not LLM-as-a-judge. The official evaluator remains the only scoring authority.", + "核验结论": "Review result", + "记录试次": "Recorded attempts", + "已复核": "Reviewed", + "记分分歧": "Score disagreements", + "未决": "Unresolved", + "复核协议": "Review protocol", + "逐试次覆盖": "Per-attempt coverage", + "结论": "Outcome", + "失败检查数": "Failed check count", + "失败检查项": "Failed checks", + "两条轨道回答不同问题,不能混分": "Two tracks answer different questions and cannot share a score", + "固定 BrowserTool 动作轨道与自主 Agent 轨道都有效,但控制变量和结论边界不同。": "A fixed BrowserTool action track and an autonomous-agent track are both useful, but they control different variables and support different conclusions.", + "仓库中的两个 harness": "The repository's two harnesses", + "轨道": "Track", + "Agent 是否自主": "Agent autonomy", + "回答的问题": "Question answered", + "Browser Use 官方实践": "Browser Use's published evaluation practice", + "公开依据": "Public sources", + "本轮浏览器运行身份、完整任务结果与失败证据入口。": "Runtime identity, complete task results, and failure evidence for this browser.", + "运行身份": "Runtime identity", + "架构": "Architecture", + "镜像 ID": "Image ID", + "首轮成绩": "Primary score", + "首轮耗时中位数": "Median primary latency", + "全部执行记录": "All execution records", + "首轮决定主分;仅失败任务有一次诊断重试,重试不回写主分。": "The primary run determines pass@1. Only failed tasks receive one diagnostic retry, and retries never replace the primary score.", + "复现从公开仓库、预注册题集和固定运行身份开始": "Reproduction starts with the public repository, preregistered cohort, and fixed runtime identities", + "公开包提供脱敏投影;模型回答、Cookie 和完整网络正文不进入 Git 或站点。": "The public bundle provides sanitized projections. Model credentials, cookies, and complete network bodies do not enter Git or the site.", + "浏览器与代码身份": "Browser and code identities", + "公开复现入口": "Public reproduction entry points", + "是代码、题集和锁文件的唯一来源。": "is the canonical source for code, task definitions, and lockfiles.", + "本地站点账号、模型权限与发布权限分开处理": "Local site accounts, model access, and publication access are separate", + "这里只说明权限边界,不展示密钥、Cookie、用户名或个人信息。": "This page describes access boundaries without exposing keys, cookies, usernames, or personal information.", + "权限与安装": "Access and setup", + "范围": "Scope", + "本轮做法": "Configuration in this run", + "取得与安装": "Acquisition and setup", + "外部付费账号": "Paid external account", + "当前发布": "Current release", + "变更": "Change", + "原因": "Reason", + "处理": "Resolution", + "影响": "Impact", + "报告解读": "Interpretation", + "评测集": "Dataset", + "评测方法": "Methodology", + "结果分析": "Results", + "核验记录": "Review", + "方法介绍": "Methods", + "环境与复现": "Reproduction", + "变更记录": "Changes", + "账号与安装": "Accounts and setup", + "切换主题": "Toggle theme", + "中文": "中文", +} + + +PHRASES = { + "规则 ": "Rule ", + " 秒": " s", + " 次 BrowserTool 调用": " BrowserTool calls", + " 次调用 / ": " calls / ", + " 次传输尝试": " transport attempts", + "查看 ": "View ", + " 步轨迹": "-step trajectory", + "成功轨迹": "successful trajectory", + "任务 #": "Task #", + "个试次": " attempts", + "个任务": " tasks", + "实际": "actual", + "期望": "expected", + "暂无发布记录。": "No release has been recorded.", +} + + +def translate_text(value: str) -> str: + leading = value[: len(value) - len(value.lstrip())] + trailing = value[len(value.rstrip()) :] + core = value.strip() + if not core: + return value + translated = TEXT.get(core) + if translated is None: + patterns = ( + ( + r"CowAgent \+ (.+?)(reasoning=low)在预注册的 (\d+) 题 WebArena‑Verified 样本上对比 Chromium 与 Moli。", + lambda match: f"CowAgent + {match.group(1)} (reasoning=low) compares Chromium and Moli on a preregistered {match.group(2)}-task WebArena-Verified sample.", + ), + (r"(\d+) 个完整配对", lambda match: f'{match.group(1)} complete pairs'), + ( + r"(\d+) 个共同失败任务没有任何成功轨迹。报告只能确认结果未满足官方合同,尚不能证明任务在当前快照可完成,也不能归因给 Agent 或浏览器。", + lambda match: f"The {match.group(1)} shared-failure tasks have no successful trajectory in this run. The report establishes only that their official contracts were not met; it does not prove current-snapshot solvability or attribute failure to the agent or browser.", + ), + ( + r"(\d+) 个试次均保留回答、官方 evaluator 结果、Agent 事件和证据哈希;每题每浏览器首轮一次,只有失败任务再诊断重试一次。", + lambda match: f"All {match.group(1)} attempts retain the answer, official evaluator result, agent events, and evidence hashes. Each task-browser pair has one primary run; only failures receive one diagnostic retry.", + ), + ( + r"(\d+)/(\d+) 个试次完成确定性一致性检查,(\d+) 个未决。它不重新判题,也不是独立语义复核。", + lambda match: f"{match.group(1)}/{match.group(2)} attempts completed deterministic consistency checks, with {match.group(3)} unresolved. This does not rescore tasks and is not an independent semantic review.", + ), + ( + r"(\d+)/(\d+) 题至少有一条自主成功轨迹;其余 (\d+) 题仅有官方验收合同。", + lambda match: f"{match.group(1)}/{match.group(2)} tasks have at least one autonomous successful trajectory; the remaining {match.group(3)} have only an official acceptance contract.", + ), + ) + for pattern, replacement in patterns: + match = re.fullmatch(pattern, core) + if match: + translated = replacement(match) + break + if translated is None: + translated = translate_common_text(core) + if translated == core: + for source, target in sorted(PHRASES.items(), key=lambda item: len(item[0]), reverse=True): + translated = translated.replace(source, target) + if translated == core: + translated = None + return leading + (translated if translated is not None else core) + trailing + + +class Localizer(HTMLParser): + def __init__(self) -> None: + super().__init__(convert_charrefs=False) + self.parts: list[str] = [] + self.stack: list[str] = [] + + def handle_decl(self, decl: str) -> None: + self.parts.append(f"") + + def handle_starttag(self, tag: str, attrs) -> None: + self.stack.append(tag) + self.parts.append(self.get_starttag_text()) + + def handle_startendtag(self, tag: str, attrs) -> None: + self.parts.append(self.get_starttag_text()) + + def handle_endtag(self, tag: str) -> None: + if tag in self.stack: + self.stack = self.stack[: len(self.stack) - 1 - self.stack[::-1].index(tag)] + self.parts.append(f"") + + def handle_data(self, data: str) -> None: + if any(tag in self.stack for tag in ("pre", "code", "script", "style")): + self.parts.append(data) + else: + self.parts.append(translate_text(data)) + + def handle_entityref(self, name: str) -> None: + self.parts.append(f"&{name};") + + def handle_charref(self, name: str) -> None: + self.parts.append(f"&#{name};") + + def handle_comment(self, data: str) -> None: + self.parts.append(f"") + + +def _relative(source: PurePosixPath, target: PurePosixPath) -> str: + return posixpath.relpath(str(target), str(source.parent)) + + +def _rewrite_url(value: str, source: PurePosixPath, english: PurePosixPath) -> str: + parsed = urlsplit(html.unescape(value)) + if parsed.scheme or parsed.netloc or value.startswith(("mailto:", "data:", "javascript:")): + return value + if not parsed.path: + return value + # Generated report pages declare a root-relative , so local links are + # resolved from the report root even when the source page is nested. + resolved = PurePosixPath(posixpath.normpath(parsed.path.lstrip("/"))) + is_page = resolved.suffix.lower() in {".html", ".htm"} or not resolved.suffix + target = PurePosixPath("en") / resolved if is_page else resolved + return html.escape(urlunsplit(("", "", str(target), parsed.query, parsed.fragment)), quote=True) + + +def _rewrite_links(page: str, source: PurePosixPath, english: PurePosixPath) -> str: + pattern = re.compile(r'(?P\b(?:href|src)=)(?P["\'])(?P.*?)(?P=quote)') + return pattern.sub( + lambda match: match.group("prefix") + + match.group("quote") + + _rewrite_url(match.group("url"), source, english) + + match.group("quote"), + page, + ) + + +def _header(source: PurePosixPath, english: PurePosixPath) -> str: + links = "".join( + f'{label}' + for path, label in NAV + ) + chinese = str(source) + return ( + '
    ' + ) + + +def localize_html(page: str, source: PurePosixPath) -> str: + english = PurePosixPath("en") / source + page = page.replace('', '', 1) + page = page.replace('placeholder="任务、意图、答案、错误"', 'placeholder="Task, intent, answer, or error"') + page = re.sub( + r'
    .*?
    ', '
    ', page, count=1, flags=re.S + ) + had_base = bool(re.search(r'', page, flags=re.I)) + page = re.sub(r'', '', page, count=1, flags=re.I) + parser = Localizer() + parser.feed(page) + parser.close() + localized = _rewrite_links("".join(parser.parts), source, english) + localized = localized.replace('
    ', _header(source, english), 1) + if had_base: + base = _relative(english, PurePosixPath(".")) + "/" + localized = localized.replace('', f'', 1) + return localized + + +def write_english_report(out: Path) -> int: + """Mirror every Chinese HTML route and return the route count.""" + english_root = out / "en" + if english_root.exists(): + shutil.rmtree(english_root) + sources = sorted(path for path in out.rglob("*.html") if "en" not in path.relative_to(out).parts) + for source_path in sources: + source = PurePosixPath(source_path.relative_to(out).as_posix()) + target = out / "en" / Path(str(source)) + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text( + localize_html(source_path.read_text(encoding="utf-8"), source), encoding="utf-8" + ) + return len(sources) diff --git a/pyproject.toml b/pyproject.toml index f983fdfba..19d4fc692 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -58,6 +58,7 @@ select = ["B", "E", "F", "I", "UP"] [tool.ruff.lint.per-file-ignores] "harness/browser_kernel/report.py" = ["E501"] "harness/browser_agent/report.py" = ["E501"] +"harness/browser_agent/report_i18n.py" = ["E501"] [build-system] requires = ["setuptools>=64"] diff --git a/research/report-changes.json b/research/report-changes.json index e4441cc01..478d57f75 100644 --- a/research/report-changes.json +++ b/research/report-changes.json @@ -2,6 +2,40 @@ "schema_version": 1, "history_scope": "本页从有完整审计依据的修订开始记录。更早的站点版本仍保留在 Artifact Hub,但没有逐项变更说明,故不补编历史。", "browser_agent_releases": [ + { + "id": "2026-09-10-report-evidence-restructure", + "date": "2026-09-10", + "title": "按证据链重构 Browser Agent 报告", + "summary": "报告新增任务族覆盖、95% Wilson 区间、30 题同题横向对照和全路由英文镜像;将原“独立复核”纠正为确定性证据完整性审计,并明确五个共同失败尚无本轮成功路径,不能直接归因 Agent 或浏览器。测量结果、分母和原始证据未改变。", + "attempts_sha256": "517b613adcc40fc0ebe41af2ab2115d76f49faaba5378b426cc196b5b633e9c7", + "selection_sha256": "ba3c344df003dd4eba3a5d1ef97162ee98df2ed728f7c29c4188bc4247982d38", + "changes": [ + [ + "首页与结果分析", + "原首页只有总分和入口,无法同时判断样本覆盖、统计不确定性、调用完整性和同题关系。", + "增加三类任务族、95% Wilson 区间、可完成性覆盖、证据审计状态,以及可筛选的 30 题 Chromium/Moli 首轮横向对照。", + "只改变解释和下钻结构,不改变 25/30、30 个配对或重试结果。" + ], + [ + "核验命名与失败归因", + "原文把确定性记录一致性检查称为独立结果复核,并把共同失败过早归因给 Agent。", + "改名为证据完整性审计;五个没有成功轨迹的任务仅陈述官方期望与实际结果差异,要求另做非计分 oracle 或人工可完成性核验。", + "官方 evaluator 仍是唯一计分来源;共同失败不再被表述为已确认根因。" + ], + [ + "双语与移动端", + "英文只有摘要页,窄屏入口卡片会被固定跨列布局裁切。", + "为中文页面生成逐路由英文对应页并校验链接;移动端强制卡片单列,移除用 overflow 隐藏问题的做法。", + "英文与中文共享同一测量数据;226 个逻辑页面当前为零断链。" + ], + [ + "构建与发布门禁", + "正式报告可能在官方数据不可用时静默退化,日常检查也未默认要求完整证据。", + "正式构建绑定官方数据 checksum 并 fail closed;增加 current-report 发布 profile、完整证据默认检查和带 marker 的原子目录替换。", + "缺失或错配 selection、模型、reasoning、官方数据或证据审计时禁止发布。" + ] + ] + }, { "id": "2026-09-09-webarena-verified-pilot", "date": "2026-09-09", diff --git a/scripts/check_browser_agent_report.py b/scripts/check_browser_agent_report.py index 342bbe391..a48b26e06 100644 --- a/scripts/check_browser_agent_report.py +++ b/scripts/check_browser_agent_report.py @@ -6,6 +6,7 @@ import argparse import hashlib import json +import re from pathlib import Path from harness.browser_agent.selection import load_pilot @@ -34,7 +35,6 @@ def check( "adoption/index.html", "reproduce/index.html", "accounts/index.html", - "en/index.html", "engines/chromium.html", "engines/moli.html", ) @@ -62,6 +62,10 @@ def check( else [] ) ) + chinese_pages = (*pages, *task_pages, *attempt_pages) + english_pages = tuple( + report_dir / "en" / page.relative_to(report_dir) for page in chinese_pages + ) data_files = tuple( report_dir / "data" / name for name in ( @@ -87,7 +91,13 @@ def check( ) if not all( path.is_file() - for path in (*pages, *task_pages, *attempt_pages, *data_files, *artifact_files, audit_path) + for path in ( + *chinese_pages, + *english_pages, + *data_files, + *artifact_files, + audit_path, + ) ): raise ValueError("report is missing one or more required report pages") selection = load_pilot(selection_path) @@ -123,8 +133,10 @@ def check( f"retry is allowed only after a failed primary: {(engine, task_id)}" ) rendered_pages = { - page: page.read_text(encoding="utf-8") for page in (*pages, *task_pages, *attempt_pages) + page: page.read_text(encoding="utf-8") for page in (*chinese_pages, *english_pages) } + if any('' not in rendered_pages[page] for page in english_pages): + raise ValueError("every Chinese report route must have an English mirror") if task_pages and any( "Ground truth · 官方验收合同" not in rendered_pages[page] or "参考状态 · 当前环境是否有成功路径证据" not in rendered_pages[page] @@ -149,9 +161,10 @@ def check( standalone = (report_dir / "guide/article.html").read_text(encoding="utf-8") if " None: diff --git a/scripts/publish_browser_agent_artifact_hub.py b/scripts/publish_browser_agent_artifact_hub.py index cf12fe262..a354892a7 100644 --- a/scripts/publish_browser_agent_artifact_hub.py +++ b/scripts/publish_browser_agent_artifact_hub.py @@ -3,18 +3,29 @@ from __future__ import annotations +import argparse import json import os -import urllib.request import urllib.parse +import urllib.request import uuid from pathlib import Path from harness.web_fetch.artifact_auth import load_token -BASE = "https://artifact-hub.dev.lexmount.net" ARCHIVE = Path("publish/browser-agent-sites.zip") RECEIPT = Path("publish/browser-agent-artifact-hub-receipt.json") +PROFILE = Path("evaluation/browser-agent/current-report.json") + + +def publication_target() -> tuple[str, str]: + profile = json.loads(PROFILE.read_text(encoding="utf-8")) + publication = profile.get("publication") or {} + base = str(publication.get("base_url") or "").rstrip("/") + slug = str(publication.get("site_slug") or "") + if not base.startswith("https://") or not slug: + raise ValueError("current Browser Agent report has no valid publication target") + return base, slug def request( @@ -24,13 +35,14 @@ def request( *, method: str | None = None, ): + base, _ = publication_target() headers = { "Authorization": "Bearer " + load_token(), "User-Agent": "Browser-Agent-Report-Publisher/1.0", } if content_type: headers["Content-Type"] = content_type - req = urllib.request.Request(BASE + path, data=data, headers=headers, method=method) + req = urllib.request.Request(base + path, data=data, headers=headers, method=method) with urllib.request.urlopen(req, timeout=240) as response: return response.status, response.read() @@ -69,10 +81,23 @@ def save_receipt(result: dict) -> None: os.chmod(RECEIPT, 0o600) -def main() -> None: +def main(*, create: bool = False) -> None: payload, content_type = multipart_archive() - if RECEIPT.exists(): + base, expected_slug = publication_target() + if create: + status, raw = request("/api/sites", payload, content_type) + result = json.loads(raw) + if status != 200 or not result.get("slug"): + raise RuntimeError(result) + save_receipt(result) + elif not RECEIPT.exists(): + raise ValueError( + "bound Browser Agent site receipt is missing; refusing to create a new site" + ) + else: result = json.loads(RECEIPT.read_text(encoding="utf-8")) + if result.get("slug") != expected_slug: + raise ValueError("Artifact Hub receipt differs from the bound Browser Agent site") status, raw = request(f"/api/sites/{result['slug']}") current = json.loads(raw) version_id = (current.get("site") or {}).get("currentVersionId") @@ -85,16 +110,13 @@ def main() -> None: updated = json.loads(raw) if not 200 <= status < 300: raise RuntimeError(updated) - result.update({key: value for key, value in updated.items() if key not in {"editToken", "claimToken"}}) - else: - status, raw = request("/api/sites", payload, content_type) - result = json.loads(raw) - if status != 200 or not result.get("slug"): - raise RuntimeError(result) - # Persist the remote identity before the separate share request. If that - # request fails, the next run must update this site rather than create - # an orphaned duplicate. - save_receipt(result) + result.update( + { + key: value + for key, value in updated.items() + if key not in {"editToken", "claimToken"} + } + ) if not result.get("share_url"): status, raw = request( f"/api/sites/{result['slug']}/shares", @@ -112,7 +134,7 @@ def main() -> None: if response.status != 200: raise RuntimeError(f"public report returned {response.status}") preview = urllib.request.Request( - f"{BASE}/api/preview/{result['slug']}", + f"{base}/api/preview/{result['slug']}", headers={"User-Agent": "Browser-Agent-Report-Publisher/1.0"}, ) with urllib.request.urlopen(preview, timeout=60) as response: @@ -123,4 +145,10 @@ def main() -> None: if __name__ == "__main__": - main() + parser = argparse.ArgumentParser() + parser.add_argument( + "--create", + action="store_true", + help="explicitly create a new owned site when the bound site cannot be updated", + ) + main(create=parser.parse_args().create) diff --git a/tests/browser_agent/test_report.py b/tests/browser_agent/test_report.py index b34b52a62..e463cead7 100644 --- a/tests/browser_agent/test_report.py +++ b/tests/browser_agent/test_report.py @@ -112,7 +112,6 @@ def test_report_renders_measured_runtime_identities(tmp_path: Path) -> None: "webarena_verified_version": "1.2.3", "official_evaluator": { "evaluator_checksum": "eval-checksum", - "data_checksum": "data-checksum", }, "browser_versions": { "chromium": {"Browser": "Chrome/151"}, diff --git a/tests/browser_agent/test_report_ui.mjs b/tests/browser_agent/test_report_ui.mjs index a9cbb858f..b75d43bf9 100644 --- a/tests/browser_agent/test_report_ui.mjs +++ b/tests/browser_agent/test_report_ui.mjs @@ -19,6 +19,15 @@ const rows = [ {dataset: {tier: 'easy', result: 'pass', engine: 'chromium'}, textContent: 'complete'}, ]; const count = {textContent: ''}; +const pairControls = [ + {dataset: {pairFilter: 'relation'}, value: 'both_fail', addEventListener() {}}, +]; +const pairedRows = [ + {dataset: {relation: 'both_fail'}}, + {dataset: {relation: 'chromium_only'}}, + {dataset: {relation: 'moli_only'}}, +]; +const pairCount = {textContent: ''}; const listeners = {}; const storage = new Map(); let replaced = ''; @@ -36,6 +45,9 @@ const document = { if (selector === '[data-search]') return [search]; if (selector === 'tbody tr[data-tier]') return rows; if (selector === '[data-visible-count]') return [count]; + if (selector === '[data-pair-filter]') return pairControls; + if (selector === '#pair-rows tr') return pairedRows; + if (selector === '[data-pair-count]') return [pairCount]; return []; }, querySelector(selector) { return selector === '[data-search]' ? search : null; }, @@ -64,6 +76,13 @@ assert.equal(search.value, 'missing'); assert.equal(controls[0].value, 'fail'); assert.deepEqual(rows.map(row => row.hidden), [false, true]); assert.equal(count.textContent, '1 / 2'); +assert.deepEqual(pairedRows.map(row => row.hidden), [false, true, true]); +assert.equal(pairCount.textContent, '找到 1 个配对任务'); + +pairControls[0].value = 'discordant'; +vm.runInContext('pairRows()', context); +assert.deepEqual(pairedRows.map(row => row.hidden), [true, false, false]); +assert.match(replaced, /p_relation=discordant/); controls[0].value = 'pass'; search.value = ''; From 7efac70a0faa660ce16390cd5a9498bae635d1b0 Mon Sep 17 00:00:00 2001 From: kevin Date: Thu, 10 Sep 2026 13:10:28 +0800 Subject: [PATCH 031/127] Document browser agent deployment flow --- .../webarena-autonomous-evaluation-flow.png | Bin 0 -> 1304866 bytes harness/browser_agent/report.py | 108 +++++++++----- harness/browser_agent/report_i18n.py | 137 +++++++++++++++--- research/report-changes.json | 22 +++ scripts/publish_browser_agent_artifact_hub.py | 4 +- tests/browser_agent/test_report.py | 7 + 6 files changed, 224 insertions(+), 54 deletions(-) create mode 100644 harness/browser_agent/assets/webarena-autonomous-evaluation-flow.png diff --git a/harness/browser_agent/assets/webarena-autonomous-evaluation-flow.png b/harness/browser_agent/assets/webarena-autonomous-evaluation-flow.png new file mode 100644 index 0000000000000000000000000000000000000000..1fb41ad08fd25c592501a02eb63886864b16cfc8 GIT binary patch literal 1304866 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 z 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

    !Sn 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≫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 k(?VDfw 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̹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 z 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 z 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%0D>K@cL2p_G%=|7= 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‰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%0AVkEo;DlQ+)7{gyO)|-7qaXJZRA9YLY@VsQ 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 str: @@ -295,7 +296,9 @@ def _task_family_summary( "kind": kind, "count": len(selected), "scores": { - engine: sum(primary.get((engine, int(task["task_id"])), False) for task in selected) + engine: sum( + primary.get((engine, int(task["task_id"])), False) for task in selected + ) for engine in ENGINE_LABELS }, } @@ -319,14 +322,14 @@ def _adoption_top3(evidence: dict[str, Any]) -> str: }.get(str(item.get("family")), "采用数不代表评测质量。") cards.append( '
    ' - f'

    {_e(item.get("family"))} 家族 · {len(adopters)} 家

    ' - f'

    {_e("、".join(adopters))}。{_e(note)}

    ' + f"

    {_e(item.get('family'))} 家族 · {len(adopters)} 家

    " + f"

    {_e('、'.join(adopters))}。{_e(note)}

    " ) return ( '
    ' + "".join(cards) + '

    采用次数来自公开一手材料中的机构去重计数,不是质量分;' - f'证据核对日期 {_e(evidence.get("verified_at") or "未记录")}。未找到 DeepSeek、Kimi ' + f"证据核对日期 {_e(evidence.get('verified_at') or '未记录')}。未找到 DeepSeek、Kimi " "公开的一手 UI browser-use benchmark,不等于内部没有评测。

    " ) @@ -368,9 +371,9 @@ def metrics(row: dict[str, Any]) -> str: usage = row.get("model_usage") or {} return ( f'{_status(row)}
    {step_count} 步 · ' - f'{int(usage.get("request_count") or 0)} 次模型请求 · ' - f'{_number(int(usage.get("total_tokens") or 0))} tokens · ' - f'{_seconds(float(row.get("duration_ms") or 0))}' + f"{int(usage.get('request_count') or 0)} 次模型请求 · " + f"{_number(int(usage.get('total_tokens') or 0))} tokens · " + f"{_seconds(float(row.get('duration_ms') or 0))}" ) search = " ".join( @@ -382,7 +385,7 @@ def metrics(row: dict[str, Any]) -> str: f' data-search="{_e(search)}"' ) rows.append( - f"
    #{task_id}" + f'#{task_id}' f'
    {_e(task["rationale"])}
    ' f"{_e(TIER_LABELS[task['tier']])}" f"{_e(SITE_LABELS.get(site, site))}{_e(task['task_type'])}" @@ -396,23 +399,21 @@ def metrics(row: dict[str, Any]) -> str: '' '' '' - '' + "" '' '' '' + "" '' ) return ( controls + '
    ' - '' - '' - + "".join(rows) - + "
    任务层级站点类型ChromiumMoli 1.1.3对照结论证据
    " + "站点类型ChromiumMoli 1.1.3对照结论证据" + '' + "".join(rows) + "" ) @@ -450,7 +451,7 @@ def _group_rows( SHARED_STYLE + """ .hero h1,.publication-article h1,.publication-article h2{overflow-wrap:anywhere;word-break:break-word} -.top nav{gap:18px;flex-wrap:nowrap;overflow-x:auto;overscroll-behavior-inline:contain;scrollbar-width:thin}.top nav>a,.top nav>button{flex:0 0 auto}.top nav .brand{font-weight:850;color:var(--ink)}.top nav a[aria-current=page]{color:var(--ink);font-weight:800;text-decoration:underline;text-decoration-thickness:2px;text-underline-offset:8px}.top .theme-toggle{margin-left:auto}.page-main{min-height:78vh}.hero.compact{padding-bottom:22px}.hero.compact h1{font-size:clamp(34px,4.5vw,52px)}.fine{font-size:13px;color:var(--muted)}.result-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px}.result-grid .card{border-top:3px solid var(--accent)}.overview-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.overview-card{border:1px solid var(--line);border-radius:14px;background:var(--panel);padding:20px;min-width:0}.overview-card h3{margin:8px 0}.audit-strip{display:grid;gap:10px}.audit-strip .callout{margin:0}.filters,.pair-controls{display:flex;gap:10px;flex-wrap:wrap;align-items:end;margin:18px 0}.filters select,.filters input,.pair-controls select{padding:9px 12px;border:1px solid var(--line);border-radius:9px;background:var(--panel);color:var(--ink)}.filters input{min-width:260px}.comparison-count{margin-left:auto;padding:8px 12px;border-radius:999px;background:var(--wash);color:var(--accent);font-weight:750}.status-pass{color:var(--good);font-weight:750}.status-fail{color:var(--bad);font-weight:750}.status-unproven{color:var(--warn,var(--bad));font-weight:750}.flow{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:9px;list-style:none;padding:0}.flow li{padding:14px;border:1px solid var(--line);border-radius:11px;background:var(--panel);font-size:13px;min-width:0}.flow li+li:before{content:'→';color:var(--accent);margin-right:7px}.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:12px}.detail-grid .kv{min-height:92px;min-width:0}.page-links{display:flex;gap:10px;flex-wrap:wrap}.page-links a{border:1px solid var(--line);background:var(--panel);padding:8px 12px;border-radius:9px}.rank-list{counter-reset:rank}.rank-item{counter-increment:rank;display:grid;grid-template-columns:54px minmax(0,1fr);gap:16px;padding:20px 0;border-top:1px solid var(--line)}.rank-item:before{content:counter(rank);font-size:34px;font-weight:850;color:var(--accent)}.rank-item h3{margin:4px 0}.article-entry{border-left:4px solid var(--accent)}.source-list li{margin:9px 0}.table-wrap{margin:18px 0}.table-wrap table{min-width:760px}#task-table{min-width:1280px}#pair-table{min-width:1180px}.table-wrap caption{text-align:left;padding:10px 12px;color:var(--muted);caption-side:bottom}.footer{border-top:1px solid var(--line)}.evidence-pair{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:14px}.evidence-card{border:1px solid var(--line);border-radius:12px;background:var(--panel);padding:16px;min-width:0}.evidence-card h3{margin-top:0}.json-view,.log-view{white-space:pre-wrap;overflow-wrap:anywhere;max-height:540px;overflow:auto;padding:14px;border:1px solid var(--line);border-radius:9px;background:var(--bg);font-size:12px;line-height:1.55}.trajectory{list-style:none;padding:0;counter-reset:step}.trajectory>li{counter-increment:step;display:grid;grid-template-columns:46px minmax(0,1fr);gap:12px;padding:15px 0;border-top:1px solid var(--line)}.trajectory>li:before{content:counter(step);display:grid;place-items:center;width:34px;height:34px;border-radius:50%;background:var(--accent);color:white;font-weight:800}.trajectory summary{cursor:pointer;font-weight:750}.rule-fail{border-left:4px solid var(--bad)}.rule-pass{border-left:4px solid var(--good)}.count-note{margin-left:auto;color:var(--muted);font-size:13px}.badge{display:inline-block;padding:3px 8px;border-radius:999px;background:var(--wash);font-size:12px;margin-right:6px}.muted-box{padding:14px;border:1px dashed var(--line);border-radius:10px;color:var(--muted)}@media(max-width:900px){.result-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.overview-grid{grid-template-columns:1fr}.flow{grid-template-columns:repeat(3,minmax(0,1fr))}.evidence-pair{grid-template-columns:1fr}.grid>.card{grid-column:1/-1!important}}@media(max-width:600px){.wrap{width:calc(100% - 28px)}.top nav{gap:14px}.result-grid,.flow{grid-template-columns:1fr}.flow li+li:before{content:'↓';display:block}.hero{padding-top:28px}.hero.compact h1{font-size:30px;line-height:1.18}.filters input{min-width:0;width:100%}.comparison-count{margin-left:0}.rank-item{grid-template-columns:36px minmax(0,1fr)}}@media print{.top,.filters,.pair-controls{display:none!important}.card,.table-wrap{box-shadow:none}.json-view,.log-view{max-height:none;overflow:visible}.evidence-pair{grid-template-columns:1fr}} +.top nav{gap:18px;flex-wrap:nowrap;overflow-x:auto;overscroll-behavior-inline:contain;scrollbar-width:thin}.top nav>a,.top nav>button{flex:0 0 auto}.top nav .brand{font-weight:850;color:var(--ink)}.top nav a[aria-current=page]{color:var(--ink);font-weight:800;text-decoration:underline;text-decoration-thickness:2px;text-underline-offset:8px}.top .theme-toggle{margin-left:auto}.page-main{min-height:78vh}.hero.compact{padding-bottom:22px}.hero.compact h1{font-size:clamp(34px,4.5vw,52px)}.fine{font-size:13px;color:var(--muted)}.result-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px}.result-grid .card{border-top:3px solid var(--accent)}.overview-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.overview-card{border:1px solid var(--line);border-radius:14px;background:var(--panel);padding:20px;min-width:0}.overview-card h3{margin:8px 0}.audit-strip{display:grid;gap:10px}.audit-strip .callout{margin:0}.filters,.pair-controls{display:flex;gap:10px;flex-wrap:wrap;align-items:end;margin:18px 0}.filters select,.filters input,.pair-controls select{padding:9px 12px;border:1px solid var(--line);border-radius:9px;background:var(--panel);color:var(--ink)}.filters input{min-width:260px}.comparison-count{margin-left:auto;padding:8px 12px;border-radius:999px;background:var(--wash);color:var(--accent);font-weight:750}.status-pass{color:var(--good);font-weight:750}.status-fail{color:var(--bad);font-weight:750}.status-unproven{color:var(--warn,var(--bad));font-weight:750}.flow{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:9px;list-style:none;padding:0}.flow li{padding:14px;border:1px solid var(--line);border-radius:11px;background:var(--panel);font-size:13px;min-width:0}.flow li+li:before{content:'→';color:var(--accent);margin-right:7px}.method-figure{margin:24px 0}.method-figure img{display:block;width:100%;height:auto;border:1px solid var(--line);border-radius:16px;background:#f6faff}.method-figure figcaption{margin-top:10px;color:var(--muted);font-size:13px;line-height:1.55}.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:12px}.detail-grid .kv{min-height:92px;min-width:0}.page-links{display:flex;gap:10px;flex-wrap:wrap}.page-links a{border:1px solid var(--line);background:var(--panel);padding:8px 12px;border-radius:9px}.rank-list{counter-reset:rank}.rank-item{counter-increment:rank;display:grid;grid-template-columns:54px minmax(0,1fr);gap:16px;padding:20px 0;border-top:1px solid var(--line)}.rank-item:before{content:counter(rank);font-size:34px;font-weight:850;color:var(--accent)}.rank-item h3{margin:4px 0}.article-entry{border-left:4px solid var(--accent)}.source-list li{margin:9px 0}.table-wrap{margin:18px 0}.table-wrap table{min-width:760px}#task-table{min-width:1280px}#pair-table{min-width:1180px}.table-wrap caption{text-align:left;padding:10px 12px;color:var(--muted);caption-side:bottom}.footer{border-top:1px solid var(--line)}.evidence-pair{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:14px}.evidence-card{border:1px solid var(--line);border-radius:12px;background:var(--panel);padding:16px;min-width:0}.evidence-card h3{margin-top:0}.json-view,.log-view{white-space:pre-wrap;overflow-wrap:anywhere;max-height:540px;overflow:auto;padding:14px;border:1px solid var(--line);border-radius:9px;background:var(--bg);font-size:12px;line-height:1.55}.trajectory{list-style:none;padding:0;counter-reset:step}.trajectory>li{counter-increment:step;display:grid;grid-template-columns:46px minmax(0,1fr);gap:12px;padding:15px 0;border-top:1px solid var(--line)}.trajectory>li:before{content:counter(step);display:grid;place-items:center;width:34px;height:34px;border-radius:50%;background:var(--accent);color:white;font-weight:800}.trajectory summary{cursor:pointer;font-weight:750}.rule-fail{border-left:4px solid var(--bad)}.rule-pass{border-left:4px solid var(--good)}.count-note{margin-left:auto;color:var(--muted);font-size:13px}.badge{display:inline-block;padding:3px 8px;border-radius:999px;background:var(--wash);font-size:12px;margin-right:6px}.muted-box{padding:14px;border:1px dashed var(--line);border-radius:10px;color:var(--muted)}@media(max-width:900px){.result-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.overview-grid{grid-template-columns:1fr}.flow{grid-template-columns:repeat(3,minmax(0,1fr))}.evidence-pair{grid-template-columns:1fr}.grid>.card{grid-column:1/-1!important}}@media(max-width:600px){.wrap{width:calc(100% - 28px)}.top nav{gap:14px}.result-grid,.flow{grid-template-columns:1fr}.flow li+li:before{content:'↓';display:block}.hero{padding-top:28px}.hero.compact h1{font-size:30px;line-height:1.18}.filters input{min-width:0;width:100%}.comparison-count{margin-left:0}.rank-item{grid-template-columns:36px minmax(0,1fr)}}@media print{.top,.filters,.pair-controls{display:none!important}.card,.table-wrap{box-shadow:none}.json-view,.log-view{max-height:none;overflow:visible}.evidence-pair{grid-template-columns:1fr}} """ ) NAV = [ @@ -471,7 +472,8 @@ def _group_rows( def _page(*, title: str, active: str, body: str, depth: int = 0) -> str: local_base = "../" * depth or "./" links = "".join( - f'{_e(label)}" for href, label, key in NAV @@ -676,16 +678,15 @@ def _article_body( summary = _primary_summary(attempts) chromium, moli = summary["chromium"], summary["moli"] failures = [ - [f"#{task_id}", FAILURE_OBSERVATIONS[task_id]] - for task_id in sorted(FAILURE_OBSERVATIONS) + [f"#{task_id}", FAILURE_OBSERVATIONS[task_id]] for task_id in sorted(FAILURE_OBSERVATIONS) ] return ( '
    Browser Agent 实测解读
    ' - f'

    {chromium["passed"]} 比 {moli["passed"]} 不是“内核等价”:当前样本没有产生分歧

    ' + f"

    {chromium['passed']} 比 {moli['passed']} 不是“内核等价”:当前样本没有产生分歧

    " f'

    同一 CowAgent 与同一模型替换浏览器后,{paired["paired"]} 个配对任务没有一个出现单边胜出。这个结果证明对照流程跑通,却没有为内核优劣提供方向性证据。

    ' '

    测量口径:WebArena‑Verified 1.2.3;首轮 pass@1;失败最多一次诊断重试;官方确定性 evaluator;无 LLM judge。

    ' - '

    一、最重要的结果不是平分,而是零个不一致配对

    ' - f'

    Chromium 与 Moli 分别通过 {chromium["passed"]}/{chromium["completed"]} 和 {moli["passed"]}/{moli["completed"]}。逐题配对后,{paired["both_pass"]} 题共同通过、{paired["both_fail"]} 题共同失败,两个浏览器单边通过都是 0。精确 McNemar 双侧 p={_probability(paired["mcnemar_exact_p"])},但这里最有解释力的不是 p 值,而是检验没有拿到任何不一致样本。

    ' + "

    一、最重要的结果不是平分,而是零个不一致配对

    " + f"

    Chromium 与 Moli 分别通过 {chromium['passed']}/{chromium['completed']} 和 {moli['passed']}/{moli['completed']}。逐题配对后,{paired['both_pass']} 题共同通过、{paired['both_fail']} 题共同失败,两个浏览器单边通过都是 0。精确 McNemar 双侧 p={_probability(paired['mcnemar_exact_p'])},但这里最有解释力的不是 p 值,而是检验没有拿到任何不一致样本。

    " + _table( ["配对结果", "任务数", "可支持的判断"], [ @@ -696,9 +697,9 @@ def _article_body( ], caption="表 1|30 个首轮任务的逐题配对;重试不并入主分。", ) - + f'

    严谨表述只能是“本轮没有观察到成功率差异”,不能写成“两种内核等价”。{paired["paired"]} 题对小效应检出力有限;更关键的是,样本没有诱发内核分歧。

    ' - '

    二、共同失败是待核验线索,不是已证实根因

    ' - f'

    {paired["both_fail"]} 个共同失败任务的 {chromium["retry_count"] + moli["retry_count"]} 次诊断重试均未恢复。下表只陈述官方期望与实际结果的差异,不把差异直接归因给 Agent、浏览器或站点。

    ' + + f"

    严谨表述只能是“本轮没有观察到成功率差异”,不能写成“两种内核等价”。{paired['paired']} 题对小效应检出力有限;更关键的是,样本没有诱发内核分歧。

    " + "

    二、共同失败是待核验线索,不是已证实根因

    " + f"

    {paired['both_fail']} 个共同失败任务的 {chromium['retry_count'] + moli['retry_count']} 次诊断重试均未恢复。下表只陈述官方期望与实际结果的差异,不把差异直接归因给 Agent、浏览器或站点。

    " + _table( ["任务", "公开诊断"], failures, @@ -976,6 +977,12 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: (stage / folder).mkdir() (stage / "assets/style.css").write_text(REPORT_STYLE, encoding="utf-8") (stage / "assets/report.js").write_text(REPORT_JS, encoding="utf-8") + if not METHODOLOGY_FIGURE.is_file(): + raise FileNotFoundError(f"missing methodology figure: {METHODOLOGY_FIGURE}") + shutil.copy2( + METHODOLOGY_FIGURE, + stage / "assets/webarena-autonomous-evaluation-flow.png", + ) article = _article_body(paired, attempts, tasks) c, m = summary["chromium"], summary["moli"] c_ci = _wilson_interval(c["passed"], c["completed"]) or (None, None) @@ -985,16 +992,16 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: family_cards = "".join( '
    ' f'{_e(family["kind"])}' - f'

    {family["count"]} 个任务

    ' - f'

    Chromium {family["scores"]["chromium"]}/{family["count"]} · ' - f'Moli {family["scores"]["moli"]}/{family["count"]}

    ' + f"

    {family['count']} 个任务

    " + f"

    Chromium {family['scores']['chromium']}/{family['count']} · " + f"Moli {family['scores']['moli']}/{family['count']}

    " f'查看这类任务 →
    ' for family in families ) home = _hero( - f"Browser Agent Evaluation · {run_date}", - "同一自主 Agent、同一模型,只替换浏览器", - f"CowAgent + {model}(reasoning=low)在预注册的 30 题 WebArena‑Verified 样本上对比 Chromium 与 Moli。", + f"Browser Use Capability Evaluation · {run_date}", + "Chromium 与 Moli:Browser Use 能力实测", + f"在同一 CowAgent、同一 {model}(reasoning=low)和同一 WebArena‑Verified 30 题样本下,对比两种浏览器完成真实网页任务的能力。", ) home += _section( "先看结论", @@ -1002,7 +1009,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) home += _section( "任务族覆盖", - '

    样本同时包含信息提取、页面导航和状态变更任务;类别来自每题官方最终答案合同。

    ' + "

    样本同时包含信息提取、页面导航和状态变更任务;类别来自每题官方最终答案合同。

    " f'
    {family_cards}
    ', ) home += _section( @@ -1011,7 +1018,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: f'

    正式调用:{len(attempts)} 个试次均保留回答、官方 evaluator 结果、Agent 事件和证据哈希;每题每浏览器首轮一次,只有失败任务再诊断重试一次。

    ' f'

    证据完整性审计:{coverage.get("reviewed_attempts", 0)}/{coverage.get("recorded_attempts", len(attempts))} 个试次完成确定性一致性检查,{len(review.get("unresolved") or [])} 个未决。它不重新判题,也不是独立语义复核。

    ' f'

    可完成性覆盖:{len(measured_solvable_ids)}/{len(tasks)} 题至少有一条自主成功轨迹;其余 {len(unproven_ids)} 题仅有官方验收合同。

    ' - '', + "", ) home += _section( "从结论进入证据", @@ -1252,6 +1259,39 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: "本轮判分链", '
    1. 预注册 30 题
    2. 逐题重置站点
    3. CowAgent 自主规划
    4. 只替换浏览器
    5. 官方 evaluator 验收
    6. 确定性证据复核

    官方数据 812 题全部配置 AgentResponseEvaluator,其中 663 题还配置 NetworkEventEvaluator。所有已配置 evaluator 都返回 1.0 才通过。它检查最终结果和必要副作用,不要求复刻参考动作序列。

    ', ) + methodology += _section( + "这 30 题如何部署并由 Agent 执行", + '
    WebArena-Verified 本地部署与 CowAgent 自主评测流程 / local deployment and autonomous evaluation flow
    图 1 · 运行器从官方 WebArena‑Verified 镜像启动本地站点;每个试次重建该题所需站点、等待环境控制器与应用健康、清理浏览器状态并登录测试账号。图是流程说明,真实性证据来自下方运行身份及 70 个试次页的事件、HAR、trace 哈希。
    ' + "

    不是“先人工部署一次,再连接一个现成网页”。本轮运行器直接调用 WebArena‑Verified 的 ContainerManager.start();每次正式执行和诊断重试前都以 reset=True 启动该题所需的官方站点容器,并停止无关站点。环境控制服务报告 ready 后,还要通过连续应用级健康检查;随后才清 Cookie、登录内置测试账号、打开任务起始页并开始计入 Agent 执行。

    " + "

    也不是固定动作回放。预注册文件只含任务 ID、层级和入选理由,不含点击序列。CowAgent 收到自然语言目标后,在最多 30 步内循环读取页面观察、请求模型决策并生成下一次 BrowserTool 调用。Chromium 与 Moli 的试次分别连接到独立 CDP 浏览器容器;每个试次保存模型最终 JSON、Agent 事件流、网络 HAR 和 Playwright trace。

    " + + _table( + ["环节", "本轮实际做法", "可核验证据"], + [ + [ + "站点来源", + "四个官方自托管镜像:Shopping、Shopping Admin、Reddit、GitLab", + "运行清单记录镜像 ID、digest、架构与 WebArena‑Verified 1.2.3 checksum", + ], + [ + "逐试次准备", + "重建题目所需容器 → env-ctrl ready → 连续健康检查 → 清状态并登录", + "运行器实现与 manifest 的 environment_reset;站点准备失败不会产生可计分试次", + ], + [ + "自主操作", + "CowAgent + 模型依据当前页面逐步调用 BrowserTool;无动作脚本", + "每个试次页公开按时间排序的 BrowserTool 轨迹和脱敏事件下载", + ], + [ + "结果验收", + "最终 JSON 交给 AgentResponseEvaluator;需要副作用的任务同时检查 HAR", + "每个试次页公开规则级 expected / actual;原始 HAR 与 trace 仅公开 SHA-256 和字节数以避免泄露登录态", + ], + ], + ) + + '

    证据边界:事件流能证明 CowAgent 在运行时产生了不同的 BrowserTool 调用序列,站点镜像和运行配置能证明本地环境身份;它们不能公开模型隐藏思维,也不把成功轨迹当成唯一标准路径。

    ' + '', + ) methodology += _section( "固定协议", _table( @@ -1336,7 +1376,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) analysis += _section( "首轮失败定位", - f'

    两种浏览器在同 {paired["both_fail"]} 题失败。失败规则来自官方 evaluator;补充文字只复述期望、实际值和记录中的错误,不代表已确认根因。

    ' + f"

    两种浏览器在同 {paired['both_fail']} 题失败。失败规则来自官方 evaluator;补充文字只复述期望、实际值和记录中的错误,不代表已确认根因。

    " + _failure_inventory(attempts, tasks), ) analysis += _section( @@ -1457,7 +1497,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ] review_page = _hero( "Evidence integrity audit", - f'{coverage.get("reviewed_attempts", 0)}/{coverage.get("recorded_attempts", len(attempts))} 个试次完成证据完整性审计', + f"{coverage.get('reviewed_attempts', 0)}/{coverage.get('recorded_attempts', len(attempts))} 个试次完成证据完整性审计", "这是确定性记录一致性检查,不是独立语义复核,也不是 LLM-as-a-judge;官方 evaluator 仍是唯一成绩来源。", ) review_page += _section( diff --git a/harness/browser_agent/report_i18n.py b/harness/browser_agent/report_i18n.py index 5fa8d2f85..b0ab0423d 100644 --- a/harness/browser_agent/report_i18n.py +++ b/harness/browser_agent/report_i18n.py @@ -29,7 +29,78 @@ TEXT = { "首页 · Browser Use 评测": "Home · Browser Use evaluation", "Browser Use 评测": "Browser Use evaluation", - "同一自主 Agent、同一模型,只替换浏览器": "One autonomous agent and model; only the browser changes", + "Chromium 与 Moli:Browser Use 能力实测": "Chromium vs. Moli: a Browser Use capability evaluation", + "评测方法 · Browser Use 评测": "Methodology · Browser Use evaluation", + "这 30 题如何部署并由 Agent 执行": "How the 30 tasks were deployed and run by the agent", + "图 1 · 运行器从官方 WebArena‑Verified 镜像启动本地站点;每个试次重建该题所需站点、等待环境控制器与应用健康、清理浏览器状态并登录测试账号。图是流程说明,真实性证据来自下方运行身份及 70 个试次页的事件、HAR、trace 哈希。": "Figure 1. The runner starts local sites from official WebArena-Verified images. Every attempt rebuilds the sites required by the task, waits for environment-control and application health, clears browser state, and signs in with benchmark accounts. The figure explains the flow; the runtime identities and the events, HAR, and trace hashes on all 70 attempt pages provide the evidence.", + "不是“先人工部署一次,再连接一个现成网页”。": "This was not a one-time manual deployment followed by connections to an existing website.", + "本轮运行器直接调用 WebArena‑Verified 的 ContainerManager.start();每次正式执行和诊断重试前都以 reset=True 启动该题所需的官方站点容器,并停止无关站点。环境控制服务报告 ready 后,还要通过连续应用级健康检查;随后才清 Cookie、登录内置测试账号、打开任务起始页并开始计入 Agent 执行。": "The runner directly calls WebArena-Verified's ContainerManager.start(). Before every primary run and diagnostic retry, it starts the official site containers required by that task with reset=True and stops unrelated sites. After environment control reports ready, the app must pass repeated application-level health checks. Only then does the runner clear cookies, sign in with benchmark accounts, open the task's start page, and begin agent execution.", + "也不是固定动作回放。": "This was not a fixed action replay either.", + "预注册文件只含任务 ID、层级和入选理由,不含点击序列。CowAgent 收到自然语言目标后,在最多 30 步内循环读取页面观察、请求模型决策并生成下一次 BrowserTool 调用。Chromium 与 Moli 的试次分别连接到独立 CDP 浏览器容器;每个试次保存模型最终 JSON、Agent 事件流、网络 HAR 和 Playwright trace。": "The preregistration contains task IDs, tiers, and selection rationales, but no click sequences. Given the natural-language objective, CowAgent repeatedly reads the page observation, requests a model decision, and generates the next BrowserTool call for up to 30 steps. Chromium and Moli attempts connect to separate CDP browser containers. Every attempt retains the final model JSON, agent event stream, network HAR, and Playwright trace.", + "站点来源": "Site source", + "环节": "Stage", + "本轮实际做法": "Implementation in this run", + "可核验证据": "Verifiable evidence", + "四个官方自托管镜像:Shopping、Shopping Admin、Reddit、GitLab": "Four official self-hosted images: Shopping, Shopping Admin, Reddit, and GitLab", + "运行清单记录镜像 ID、digest、架构与 WebArena‑Verified 1.2.3 checksum": "The run manifest records image IDs, digests, architectures, and the WebArena-Verified 1.2.3 checksum", + "逐试次准备": "Per-attempt preparation", + "重建题目所需容器 → env-ctrl ready → 连续健康检查 → 清状态并登录": "Rebuild required containers → env-ctrl ready → repeated health checks → clear state and sign in", + "运行器实现与 manifest 的 environment_reset;站点准备失败不会产生可计分试次": "Runner implementation and manifest environment_reset; failed site preparation cannot produce a scored attempt", + "CowAgent + 模型依据当前页面逐步调用 BrowserTool;无动作脚本": "CowAgent and the model call BrowserTool step by step from the current page; no action script", + "每个试次页公开按时间排序的 BrowserTool 轨迹和脱敏事件下载": "Every attempt page publishes the ordered BrowserTool trajectory and a sanitized event download", + "结果验收": "Outcome evaluation", + "最终 JSON 交给 AgentResponseEvaluator;需要副作用的任务同时检查 HAR": "AgentResponseEvaluator checks the final JSON; tasks requiring side effects also inspect the HAR", + "每个试次页公开规则级 expected / actual;原始 HAR 与 trace 仅公开 SHA-256 和字节数以避免泄露登录态": "Every attempt page publishes rule-level expected/actual values; raw HAR and trace files expose only SHA-256 and byte counts to protect authenticated state", + "证据边界:": "Evidence boundary:", + "事件流能证明 CowAgent 在运行时产生了不同的 BrowserTool 调用序列,站点镜像和运行配置能证明本地环境身份;它们不能公开模型隐藏思维,也不把成功轨迹当成唯一标准路径。": "Event streams establish that CowAgent produced BrowserTool call sequences at runtime, while site images and run configuration establish the local environment identity. They do not expose hidden model reasoning or make a successful trajectory the only valid path.", + "查看 Chromium 自主轨迹示例": "View a Chromium autonomous trajectory", + "查看 Moli 自主轨迹示例": "View a Moli autonomous trajectory", + "核对镜像与运行身份": "Verify image and runtime identities", + "自托管真实交互": "Self-hosted interactive websites", + "最终 JSON + 必要网络副作用": "Final JSON plus required network side effects", + "程序化;无 LLM judge": "Programmatic; no LLM judge", + "部署重,最适合内核 A/B": "Heavy deployment; strongest fit for browser A/B testing", + "离线网页快照": "Offline webpage snapshots", + "每步目标元素与操作": "Target element and action at each step", + "程序化动作指标": "Programmatic action metrics", + "评过程模仿": "Evaluates process imitation", + "136 个真实网站": "136 live websites", + "动作历史与最终完成": "Action history and final completion", + "WebJudge + 人工标签": "WebJudge plus human labels", + "漂移、CAPTCHA、judge 偏差": "Drift, CAPTCHA, and judge bias", + "实时网站视觉交互": "Visual interaction with live websites", + "最终回答与执行证据": "Final answer and execution evidence", + "多模态 LLM + 人工核验": "Multimodal LLM plus human verification", + "精确复现较弱": "Weaker exact reproducibility", + "长链多站搜索": "Long-horizon multi-site search", + "带引用研究答案": "Research answer with citations", + "rubric Agent-as-a-Judge": "Rubric Agent-as-a-Judge", + "非 UI 内核首选": "Not the first choice for UI browser cores", + "100 个混合任务": "100 mixed tasks", + "最终结果与轨迹": "Final outcomes and trajectories", + "Google judge LLM": "Google judge LLM", + "云浏览器条件影响可比性": "Cloud-browser conditions affect comparability", + "200 个长链多站任务": "200 long-horizon multi-site tasks", + "rubric 完成度": "Rubric completion", + "模型评审": "Model grading", + "成本高": "High cost", + "官方数据 812 题全部配置 AgentResponseEvaluator,其中 663 题还配置 NetworkEventEvaluator。所有已配置 evaluator 都返回 1.0 才通过。它检查最终结果和必要副作用,不要求复刻参考动作序列。": "All 812 official tasks configure AgentResponseEvaluator, and 663 also configure NetworkEventEvaluator. A task passes only when every configured evaluator returns 1.0. The evaluators inspect final outcomes and required side effects, not reproduction of a reference action sequence.", + "Agent / 模型": "Agent / model", + "CowAgent;gpt-5.6-sol;reasoning=low": "CowAgent; gpt-5.6-sol; reasoning=low", + "同题、同数据、同模型、同提示和资源上限,只替换浏览器": "Same task, data, model, prompt, and resource limits; only the browser changes", + "每题×浏览器第一次正式执行;二元 pass@1;逐题配对": "First formal run for each task-browser pair; binary pass@1; task-level pairing", + "首轮失败后最多一次,只做恢复诊断;模型传输失败同请求最多重发一次": "At most one retry after primary failure, used only for recovery diagnosis; at most one resend for a model transport failure", + "不纳入无法稳定验收的实时任务;首轮排除需大型数据卷的 Wikipedia / Map": "Exclude live tasks without stable acceptance; the pilot also excludes Wikipedia and Map because of their large data volumes", + "官方程序化 evaluator;无 LLM judge;人工不得改分": "Official programmatic evaluators; no LLM judge; no manual score changes", + "Agent 最终 JSON": "Agent final JSON", + "先按 JSON schema 和数据类型归一化,再比较 task_type、status 与 retrieved_data;按题可声明列表有序或无序。": "Normalize by JSON schema and data type, then compare task_type, status, and retrieved_data; each task may declare ordered or unordered lists.", + "最终回答符合该题公开答案合同。": "The final answer satisfies the task's public answer contract.", + "没有检查回答是通过哪条 UI 路径得到的,也不评操作效率。": "It does not inspect which UI path produced the answer or evaluate action efficiency.", + "本次浏览器 HAR": "HAR from this browser attempt", + "在网络事件中匹配 URL、方法、状态码以及按需声明的 query、POST、响应字段或 Cookie 条件。": "Match URL, method, status code, and, when declared, query, POST, response-field, or cookie conditions in network events.", + "导航或页面变更产生了指定的可观察副作用。": "Navigation or state changes produced the specified observable side effect.", + "没有要求复刻固定点击序列;未声明的页面状态不在验收范围内。": "It does not require a fixed click sequence; undeclared page state is outside the acceptance scope.", + "一题配置的每个 evaluator 都必须得分 1.0,整题才通过。这里没有 LLM-as-a-judge,也没有人工改分。": "Every evaluator configured for a task must score 1.0 for the task to pass. There is no LLM-as-a-judge and no manual score override.", "本轮没有观察到浏览器成功率差异;这证明对照流程可比,不证明两个内核等价。": "This run found no browser success-rate difference. It establishes a comparable controlled run, not browser equivalence.", "归因边界:": "Attribution boundary:", "任务族覆盖": "Task-family coverage", @@ -338,26 +409,42 @@ def translate_text(value: str) -> str: translated = TEXT.get(core) if translated is None: patterns = ( + ( + r"在同一 CowAgent、同一 (.+?)(reasoning=low)和同一 WebArena‑Verified 30 题样本下,对比两种浏览器完成真实网页任务的能力。", + lambda match: ( + f"Compare how Chromium and Moli complete real browser tasks with the same CowAgent, {match.group(1)} (reasoning=low), and the same 30-task WebArena-Verified sample." + ), + ), ( r"CowAgent \+ (.+?)(reasoning=low)在预注册的 (\d+) 题 WebArena‑Verified 样本上对比 Chromium 与 Moli。", - lambda match: f"CowAgent + {match.group(1)} (reasoning=low) compares Chromium and Moli on a preregistered {match.group(2)}-task WebArena-Verified sample.", + lambda match: ( + f"CowAgent + {match.group(1)} (reasoning=low) compares Chromium and Moli on a preregistered {match.group(2)}-task WebArena-Verified sample." + ), ), - (r"(\d+) 个完整配对", lambda match: f'{match.group(1)} complete pairs'), + (r"(\d+) 个完整配对", lambda match: f"{match.group(1)} complete pairs"), ( r"(\d+) 个共同失败任务没有任何成功轨迹。报告只能确认结果未满足官方合同,尚不能证明任务在当前快照可完成,也不能归因给 Agent 或浏览器。", - lambda match: f"The {match.group(1)} shared-failure tasks have no successful trajectory in this run. The report establishes only that their official contracts were not met; it does not prove current-snapshot solvability or attribute failure to the agent or browser.", + lambda match: ( + f"The {match.group(1)} shared-failure tasks have no successful trajectory in this run. The report establishes only that their official contracts were not met; it does not prove current-snapshot solvability or attribute failure to the agent or browser." + ), ), ( r"(\d+) 个试次均保留回答、官方 evaluator 结果、Agent 事件和证据哈希;每题每浏览器首轮一次,只有失败任务再诊断重试一次。", - lambda match: f"All {match.group(1)} attempts retain the answer, official evaluator result, agent events, and evidence hashes. Each task-browser pair has one primary run; only failures receive one diagnostic retry.", + lambda match: ( + f"All {match.group(1)} attempts retain the answer, official evaluator result, agent events, and evidence hashes. Each task-browser pair has one primary run; only failures receive one diagnostic retry." + ), ), ( r"(\d+)/(\d+) 个试次完成确定性一致性检查,(\d+) 个未决。它不重新判题,也不是独立语义复核。", - lambda match: f"{match.group(1)}/{match.group(2)} attempts completed deterministic consistency checks, with {match.group(3)} unresolved. This does not rescore tasks and is not an independent semantic review.", + lambda match: ( + f"{match.group(1)}/{match.group(2)} attempts completed deterministic consistency checks, with {match.group(3)} unresolved. This does not rescore tasks and is not an independent semantic review." + ), ), ( r"(\d+)/(\d+) 题至少有一条自主成功轨迹;其余 (\d+) 题仅有官方验收合同。", - lambda match: f"{match.group(1)}/{match.group(2)} tasks have at least one autonomous successful trajectory; the remaining {match.group(3)} have only an official acceptance contract.", + lambda match: ( + f"{match.group(1)}/{match.group(2)} tasks have at least one autonomous successful trajectory; the remaining {match.group(3)} have only an official acceptance contract." + ), ), ) for pattern, replacement in patterns: @@ -368,7 +455,9 @@ def translate_text(value: str) -> str: if translated is None: translated = translate_common_text(core) if translated == core: - for source, target in sorted(PHRASES.items(), key=lambda item: len(item[0]), reverse=True): + for source, target in sorted( + PHRASES.items(), key=lambda item: len(item[0]), reverse=True + ): translated = translated.replace(source, target) if translated == core: translated = None @@ -433,10 +522,12 @@ def _rewrite_url(value: str, source: PurePosixPath, english: PurePosixPath) -> s def _rewrite_links(page: str, source: PurePosixPath, english: PurePosixPath) -> str: pattern = re.compile(r'(?P\b(?:href|src)=)(?P["\'])(?P.*?)(?P=quote)') return pattern.sub( - lambda match: match.group("prefix") - + match.group("quote") - + _rewrite_url(match.group("url"), source, english) - + match.group("quote"), + lambda match: ( + match.group("prefix") + + match.group("quote") + + _rewrite_url(match.group("url"), source, english) + + match.group("quote") + ), page, ) @@ -458,20 +549,28 @@ def _header(source: PurePosixPath, english: PurePosixPath) -> str: def localize_html(page: str, source: PurePosixPath) -> str: english = PurePosixPath("en") / source page = page.replace('', '', 1) - page = page.replace('placeholder="任务、意图、答案、错误"', 'placeholder="Task, intent, answer, or error"') + page = page.replace( + 'placeholder="任务、意图、答案、错误"', 'placeholder="Task, intent, answer, or error"' + ) page = re.sub( - r'
    .*?
    ', '
    ', page, count=1, flags=re.S + r'
    .*?
    ', + "
    ", + page, + count=1, + flags=re.S, ) had_base = bool(re.search(r'', page, flags=re.I)) - page = re.sub(r'', '', page, count=1, flags=re.I) + page = re.sub( + r'', "", page, count=1, flags=re.I + ) parser = Localizer() parser.feed(page) parser.close() localized = _rewrite_links("".join(parser.parts), source, english) - localized = localized.replace('
    ', _header(source, english), 1) + localized = localized.replace("
    ", _header(source, english), 1) if had_base: base = _relative(english, PurePosixPath(".")) + "/" - localized = localized.replace('', f'', 1) + localized = localized.replace("", f'', 1) return localized @@ -480,7 +579,9 @@ def write_english_report(out: Path) -> int: english_root = out / "en" if english_root.exists(): shutil.rmtree(english_root) - sources = sorted(path for path in out.rglob("*.html") if "en" not in path.relative_to(out).parts) + sources = sorted( + path for path in out.rglob("*.html") if "en" not in path.relative_to(out).parts + ) for source_path in sources: source = PurePosixPath(source_path.relative_to(out).as_posix()) target = out / "en" / Path(str(source)) diff --git a/research/report-changes.json b/research/report-changes.json index 478d57f75..eab0f0ac3 100644 --- a/research/report-changes.json +++ b/research/report-changes.json @@ -2,6 +2,28 @@ "schema_version": 1, "history_scope": "本页从有完整审计依据的修订开始记录。更早的站点版本仍保留在 Artifact Hub,但没有逐项变更说明,故不补编历史。", "browser_agent_releases": [ + { + "id": "2026-09-10-deployment-and-autonomy-evidence", + "date": "2026-09-10", + "title": "补全本地部署与自主执行证据", + "summary": "首页标题改为 Chromium 与 Moli 的 Browser Use 能力实测;评测方法页新增 WebArena-Verified 本地站点、逐试次重置、CowAgent 闭环操作、官方验收和证据留存的完整流程图及实现证据。测量结果、分母和原始证据未改变。", + "attempts_sha256": "517b613adcc40fc0ebe41af2ab2115d76f49faaba5378b426cc196b5b633e9c7", + "selection_sha256": "ba3c344df003dd4eba3a5d1ef97162ee98df2ed728f7c29c4188bc4247982d38", + "changes": [ + [ + "报告标题", + "原标题把控制变量写成了评测对象,读者无法第一眼识别报告在比较哪两个浏览器。", + "主标题改为 Chromium 与 Moli 的 Browser Use 能力实测;CowAgent、模型、reasoning 和题集降为实验条件。", + "只修正叙事层级,不改变评测设计或结果。" + ], + [ + "部署与自主性证据", + "原报告只写“逐题重置”和“自主规划”,没有解释站点由谁部署、Agent 如何进入闭环,也不足以排除固定动作回放的误读。", + "新增双语流程图和逐环节证据表,明确运行器按试次启动官方站点镜像、健康检查、登录,再由 CowAgent 动态产生 BrowserTool 调用。", + "70 次既有调用不重跑;新增说明直接绑定现有 manifest、事件、HAR 与 trace 哈希。" + ] + ] + }, { "id": "2026-09-10-report-evidence-restructure", "date": "2026-09-10", diff --git a/scripts/publish_browser_agent_artifact_hub.py b/scripts/publish_browser_agent_artifact_hub.py index a354892a7..86edc0cbb 100644 --- a/scripts/publish_browser_agent_artifact_hub.py +++ b/scripts/publish_browser_agent_artifact_hub.py @@ -54,7 +54,7 @@ def multipart_archive() -> tuple[bytes, str]: parts = [] for key, value in ( ("mode", "zip"), - ("title", "Browser Use 公开评测与 WebArena-Verified 对比"), + ("title", "Chromium 与 Moli:Browser Use 能力实测"), ): parts.append( ( @@ -139,7 +139,7 @@ def main(*, create: bool = False) -> None: ) with urllib.request.urlopen(preview, timeout=60) as response: body = response.read().decode("utf-8", errors="replace") - if response.status != 200 or "同一自主 Agent、同一模型,只替换浏览器" not in body: + if response.status != 200 or "Chromium 与 Moli:Browser Use 能力实测" not in body: raise RuntimeError("public preview is unavailable or missing the report heading") print(json.dumps({"slug": result["slug"], "url": result["share_url"]}, ensure_ascii=False)) diff --git a/tests/browser_agent/test_report.py b/tests/browser_agent/test_report.py index e463cead7..53d2cbf67 100644 --- a/tests/browser_agent/test_report.py +++ b/tests/browser_agent/test_report.py @@ -151,6 +151,13 @@ def test_report_renders_measured_runtime_identities(tmp_path: Path) -> None: assert expected in text assert report.is_file() + assert (output / "assets/webarena-autonomous-evaluation-flow.png").is_file() + home_text = report.read_text(encoding="utf-8") + assert "Chromium 与 Moli:Browser Use 能力实测" in home_text + methodology_text = (output / "methodology/index.html").read_text(encoding="utf-8") + assert "webarena-autonomous-evaluation-flow.png" in methodology_text + assert "ContainerManager.start()" in methodology_text + assert "不是固定动作回放" in methodology_text for name in ( "guide/index.html", "guide/article.html", From 13b7d72739171066a36d524b3e2a98d53dacb53c Mon Sep 17 00:00:00 2001 From: kevin Date: Thu, 10 Sep 2026 14:46:25 +0800 Subject: [PATCH 032/127] Refine browser agent report publication --- .agents/skills/benchmark-report/SKILL.md | 2 + .../references/report-and-release.md | 8 + AGENTS.md | 3 + evaluation/browser-agent/current-report.json | 5 +- .../webarena-autonomous-evaluation-flow.png | Bin 1304866 -> 1429754 bytes harness/browser_agent/report.py | 507 +++++++++++------- harness/browser_agent/runner.py | 4 +- research/report-changes.json | 54 +- scripts/check_browser_agent_report.py | 76 ++- scripts/publish_browser_agent_artifact_hub.py | 15 +- tests/browser_agent/test_report.py | 36 +- 11 files changed, 468 insertions(+), 242 deletions(-) diff --git a/.agents/skills/benchmark-report/SKILL.md b/.agents/skills/benchmark-report/SKILL.md index 941db216c..6e76fa456 100644 --- a/.agents/skills/benchmark-report/SKILL.md +++ b/.agents/skills/benchmark-report/SKILL.md @@ -20,6 +20,8 @@ Read the repository's `AGENTS.md` first. Project rules override this general ski 7. Rerun only calls whose inputs, provider route/version, runtime contract, or source validity changed. Recompute downstream scores and reports without provider calls when only scoring, annotations, or presentation changed. Never retry merely to obtain a better result. 8. Build the public report from the frozen evidence. Every page should answer a reader question. Show task and run identifiers, the exact expected-versus-actual evidence behind a verdict, denominators and unavailable metrics, method differences, limits, concise reproduction instructions, and a compact review history. 9. Release only after code tests, evidence integrity, report links, responsive/visual checks, secret scans, target-site checks, and deployed-output verification pass. Preserve earlier sites unless replacement was explicitly requested. +10. Present one authoritative logical result per method and task. If the frozen protocol permits one additional execution after failure, keep physical calls in the evidence chain, use the final execution without best-of selection, and aggregate all latency and cost. Do not make execution ordinal labels part of the public comparison unless retry behavior is the declared subject. +11. Treat visual QA as evidence QA. Inspect every core route class at desktop and narrow widths in light and dark themes. Reject one-character vertical table wrapping, unreadable code/evidence contrast, clipped controls, text-heavy raster diagrams, and mobile layouts that merely shrink dense content below readable size. ## Stop conditions diff --git a/.agents/skills/benchmark-report/references/report-and-release.md b/.agents/skills/benchmark-report/references/report-and-release.md index 57e3da41f..69b4ff920 100644 --- a/.agents/skills/benchmark-report/references/report-and-release.md +++ b/.agents/skills/benchmark-report/references/report-and-release.md @@ -20,6 +20,10 @@ For a multilingual report, generate every language from one semantic report mode Use a compact review page resembling release history: current reviewed coverage and unresolved count, then one card or row per review round with scope, findings, corrections, and remaining work. Do not publish agent work logs, giant execution manifests, or downloads merely because the evidence exists. +Expose one logical result per method and task. A declared second execution after failure is an orchestration detail: preserve its immutable evidence, let the final execution determine the result, and add every execution's latency and cost. Do not split the public result table into first-versus-retry rows unless that distinction is the preregistered research question. + +Never publish user prompts, assistant reasoning, plans, internal notes, conversational back-and-forth, or tool-work narration. A formal report explains the benchmark and evidence; it is not the authoring notebook. + ## Claims and rankings Pair every ranking with its task family, denominator, acceptance meaning, a typical input-to-output route, and limits. Keep task acceptance separate from continuous quality, latency, cost, and title/body diagnostics. Do not call conditional averages global quality or explain a correlation as a verified mechanism. @@ -39,6 +43,10 @@ Before publication: 7. fetch the unauthenticated share URL and compare the deployed file tree or archive with the submitted package; 8. record a concise release entry with score/denominator effects and remaining limits. +The responsive/visual gate must cover every core route class, both languages, desktop and narrow widths, and light and dark themes. Check that short headers do not wrap one character per line, long cells receive deliberate width or scrolling, filters remain operable, code and evidence blocks have readable contrast, and text-heavy diagrams use scalable/vector rendering or retain a readable minimum width. A screenshot of only the home page is insufficient. + +Render release history like a report, not a heading plus a raw table: show the current version summary first, keep evidence/handling/impact details expandable, retain older versions in a compact history, and provide the structured record download. Each entry states what changed, why, the formal handling, and the score, denominator, latency, evidence, and limitation effects. + Credentials belong in the approved secret store, never HTML or Git. If a service requires an account, list the account and official acquisition location in the report. Do not include live keys. Publication authorization does not imply permission to overwrite another report. Bind the release target in the current profile and reject receipts for a different service or site. Return the public share link, stable site ID, PR/change link, verification result, and unresolved count. diff --git a/AGENTS.md b/AGENTS.md index aedf1d140..624cb3f86 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,3 +13,6 @@ - Popular-site stable text must be independently observed and task-relevant. Do not freeze volatile ranks, prices, member counts or current list membership. Title diagnostics join returned entities to independent names by detail URL and never substitute for body quality, recall, freshness or task acceptance. - Keep pass, fail and all result filters, defaulting to fail, with failure classification. Every ranking must state its task family, denominator, acceptance meaning, typical input-to-output route and local limits. Preserve every registered provider's interface examples and adoption statistics. - Maintain `research/report-changes.json` for substantive releases and `research/provider-pricing.json` for public pricing. Release notes explain reader-visible or result-visible effects without narrating assistant workflow; prices retain verification dates, commitments, billing units and unknowns. +- Public benchmark pages expose one authoritative logical result per method and task. When the declared protocol allows one additional execution after failure, retain all physical evidence privately, use the final execution as the result, and add all execution latency and cost. Do not publish separate first/retry comparison rows unless retry behavior is itself the preregistered research question. +- Formal reports must never contain user prompts, assistant reasoning, planning notes, internal work logs, or conversational justification. Release notes record reader-visible changes, causes, formal handling, score/denominator/latency effects, and remaining limits. +- Before release, visually inspect every core route class in Chinese and English at desktop and narrow widths in both themes. Short table labels must not collapse into one-character vertical columns; text-heavy diagrams must remain readable at normal size (prefer SVG); code and evidence blocks must maintain readable foreground/background contrast. diff --git a/evaluation/browser-agent/current-report.json b/evaluation/browser-agent/current-report.json index c85384288..cd0c3dda9 100644 --- a/evaluation/browser-agent/current-report.json +++ b/evaluation/browser-agent/current-report.json @@ -11,8 +11,8 @@ ], "model": "gpt-5.6-sol", "reasoning_effort": "low", - "primary_attempts_per_engine_task": 1, - "failure_retries": 1, + "max_task_executions": 2, + "logical_result": "final execution; aggregate all execution latency and cost", "model_transport_retries": 1, "scoring_authority": "unmodified official programmatic evaluator", "review_kind": "deterministic evidence integrity audit", @@ -28,6 +28,7 @@ "base_url": "https://artifact-hub.dev.lexmount.net", "site_slug": "28OH4YZkUYF1", "share_url": "https://artifact-hub.dev.lexmount.net/v/5Vhnhdc_f8FEM2RzctQm-ChRlXUf174Y", + "expected_version": "ver_9427c10f5d424c0abf9d5725433702fe", "state": "verified" } } diff --git a/harness/browser_agent/assets/webarena-autonomous-evaluation-flow.png b/harness/browser_agent/assets/webarena-autonomous-evaluation-flow.png index 1fb41ad08fd25c592501a02eb63886864b16cfc8..97ae775eb331d928f8e22d9d73186a8258ad12e7 100644 GIT binary patch literal 1429754 zcmeFa2{_f;+ds^<&GVSKZI)r%v%Q7PGa-bK%{GtOre^bySu#Z-L?LB}NX8;$Dp4qu zD48k^8s4>~b2`8O?|Gj0eXsZV5AW5v&c43Z+H2kGzCZVUf7bn7YhULinw#mfG6^zK zQBkoP8t7P3QPCw(QPEU0KtPY$i$EtTDr+lOqLwY?=^f(d0=h-PljMpDBvNrwaYRs2 zU7^CCKSHzJ0Y<@hM(O_?<@e7~Awm8sl#wb#3<>3eb0aCbxRD5o7-c+8(S_uKRCK{9 zyW(81Bs_tD18cDy+!43ur#K?{e^?YwB$M5Og1rL#$zTHG$j*ellnEOBEU1<<28EZI z<;LHi*7oA2tPS^a4e$p;srX${WY0G7I>@p?4*uX3-^x3(NLJ9l%f+4KLgnR0^l(!m zhkD3|`}tBNhq-u>TvR~^EX<1(?5QTMjKqnclr{#3ge&9mFcbm@$KjDM3Jn0%AyD; z7zzWF#O*wg+CRG~x>81gMYd;w#VJxKYf^*)owgStX#ZSPc}Is|I)V1KjzBsviGafF z*nrXgX%p_JiC6%^w#jJNPY8ZNvI7wTsIv_jb_YTrZyQoH>?g!OA;%z)+uti=wb5t- z9D_l@umD+QU?f@@*o?=)uo%!rD#I`c3>=FB;?O8KLYbhAK?6_WlwnvD8q5R83aN0kY1uofEgR(3|Er8Cp zn!t+R0tbUY+3W<6y(vZ+0bCE@z+gZm;j}S$9B2dc!MY%@Fa)43C1CIihg)F76HWn)dm4ciHV=%O(=R$P(>qw z8Yml-vNAvor~RuNrHq9mK`@|^csLFVv{FXH5eR@b3aFtBVo4bZ$D@@^(7;(Z42U>1 z8V&*lsDS}bpf;F?#sU;E;E4g_QNT+S)PdCmuJ18khNj;E~XkZD*6@U|nb$}w4G6{hK-Ue*|9})$G zZF>c6f(HEvKnoP~320?)6eZ6=!frbk2l@#VXH(h$E(&~qwUyz@1iTFzC<$OQL4lw| z-~b9(5CwpCP&gd$HX5{%a4g`5Uq+!ZU_3?H9V5{o;}J+@o1exacb;f~!nQROzf!Ej zXrmAWIPe8f00Y7dD1^em@xVYzJP;^;1ks72_!}rhAP{WOSddKsyq#_e4wNK9VE|vD zKo;Q8AlZN(6n#K2qXEcR0@4JH1`Gteivi1mK7bD;Izc#qut6XJxPT)t6j}t{MuI0L z$_cn%ZQxx565vSqIUZ#KI1XqBh5@ZXKmaQMyf`onFqkr6T_gcSFPcJ|fB=D*f3X_C zu$^?+pW&#D0%WXASse?+0ve?RD-ux3wsy+E%K#Jtr44cnAPlqzIY$6(K%h8^@@UWv zi~%7GK0sLz48X~MwLyxZ0g3+{z7yGCI3OxK7H5M2frZ2nw14dZ7(irT`vArPAEHno zg@EsOj3p2NhF~wiqX{NJB={a+0vG~|Kmj$7AX9)%U>+LGL*XdT9VVfq@V3oB9wh}0 zb`StOgr)EhMJyH=33e*b#^8W)fZowySqvaF$}(7>E&_-HyB}~hU}p;LQg$k^3fQ@@ zAmNceAY;%J z;`yshAvA)`Pjj(5Pn5msPlJHM+x8)~v6PUcz>7kH6a?M_1dK;gLJ%xW!3!`TaM3ny zfDu7vfK;at@lHPl2+HP*0ht3>a64rQNJ<-|3MIsWLI?_>ftW>8c4x3IAS)9LC3+}@ z1@JluE(}Oeut9_Nj&}h)DFZRvZLl9u@KmO3`IPZktO*7Wwhsg_1E>wid)o{=g}A{s zhzH}bfWWa7KLKU|d&rI-|1#VDSCQo6OLX-SR{}K-iT1pQtC z{w(N-uG{4wJQVd;S@%Ps=v$jdUetvT=%9GT^E^P1p(~hN;;}P0y43yWU%uNlp=*#E zG1!en3iEXHZx03pi&I1p6p;uk1VUx|Uwc1tfO~KlG008R!_7aqgyc{31EpVJu;O;1 z=n?7`L;z8ZsB@61)pmu+$%8Hb-Uv8cMBuC29Vsy zN;~7p+v9`5c%|SVqL;rLNz>OOAjm7&)6dJ*)Q#jtEZNZ+ECcFTP{x8)|8uQ>%?Jm@ zE|?Mqj12N42Kl&=;357l0U`eXnaM#!Uti6i*c$q~`i79)Nbdo{T{|Ap_>Vo}NhEv1 zNyK2{Uk))wwqD||4{n=EU)lu^T5<$m9H%&>%-by-h^Amijx2E_2NQ#W`NiCQ-TXa* zJq4_tw{iBTtmqE&Bl>%}gCVX&Uk`V(ClQ6kkti$K!s>P(*n=Fn>6Z4P`!a)~zYERm zq1Lsh@I##;lFl5vN19y&i6jGxE4amBc~p)e>H zG$D9f)?pAgZb8K0fS|)({_X*#+m?m%?+)eWX;Zb+iMrj)pb z1o?(blHGjWCAKk8+jisMuRKcIZOYIcBpy}ut?K1tZqTjw5t2+qE13;UI=sVgPrxng zd;_mZdxe3Q4NBw$gCHh`1bf0M0uBd}-9Z8aiuEA}y1Cl?jAb{#wQz(N*~`P97#tGh z7H;~R9@`5m{Xcr2`>E?+;(~T^?(pB%ME`$p?@x(xziIw&5&WA%c5uA-yiUTIIY@y| z`NETOV>@K@dP?V-;PWqHDe|ihaRNy{>11YM*4BjSc!h%C3h+k?BKYYT!E$jQS*aPR`Iuda z8o`t~aH1Dd9QYI+(iDyY#FojKYc+0Fa+_4o?B@NbM0dv$d2fywk9-oP!PM32stVCoF=JLwNf zr=B+y9m(c&MVrak5L3KPoWYkHf0GNi6h&>1h@!SZ>;~(LNI>}!d=Wfdb&}bbv&D9= zE%|*e?0Q~#YrNJ3Awn6z2IYhDFoiiL%~)|iIQ@=Y`G|dA{8>BX1cHU~H8(Z&4=4md zgrV~zkFG#%qF^`KB`7D)t>0EiWVR>ZoJloge6I6$}VZaBccE=cemu1o?)90-BGC(%HE z)1HRu_qe9cyP5)?i&5c z<8s;d7WpL!t7SuAN_7Oz$74}PhyGmEAa%^`w}<+tMeLHo<7pnSxykcx1z&9G5xFwF zeyVA?J;6AX7jH-N7$n^o_}K0w9b&O~tt?T?XS6xWKtE4liccBR|GRRC$clf)>i^WF zQ$WZ8_k*O9_$}!y0|J8o$~6U;uC_MJ9K417$~qJRi69_BK@PGGyxxO^+-d$F#2=vu zj=$5IRz4C+B~If)1^rSuzwkx6@SqF+Kq>>^~0Fh6v^y)g{mCfA=i&w!u< z$qu3Zm&~W8+on?yDJVFi{5!3G&-`E6NdrX~|KtE2gw}6a?*y_Q_19Di2S_5se@%i= z^YQ&k>0b%2E~{-9AOmUhy`1?Ey0TjTiReM2}ac*+`uN-=2rhi>`@oRciFL z6vVGy;J$i6G4DNwHp``jIdbtWYg6+3`b!>Qx3wY%ql&$P$-!0uK5n3lEp`nh`r7d% zA)#AMyU{=))U;HH$ap&DUt1stHMA%k8j7Hd z{Dt%#1V9@3FSDSq1|{(?OZP^MEzE*zu7$Uh`aF9#)P2>?q1=O`=85g?2nZupg&rR% zWtaIFKOg0_1|%f{fdeE74jXWIJWkmjK@&ku4Pl_GW0l#zr}=166o)$^3c3l%%4vtJ zh)?xoQD>4T4GS33m*0}Di36Um3*q0k9E3+n-* zKTIzO)X-kw)d%^X?7u9447Upf8mj}ENU#h2pWFY=z7Gx`!C@c{ zg`+T#GC~=c`m>1$`R`PYvWISyCHrs6nYa-Fiqs{)!vx$+6Gfdy1^UlGqNpROUf=Jj z&ic0YP3Gdg@XJK#{U2<5h;m!S34@0#N^%O>5|xrNdUv z7So?9=%Au2m+__SWi`GH&l3mCkr}THkqv4eRs2QV%od%0qmCJc(((VgS0^-!FgbxOR5_!r(>Y} z!#*FzNL|ZC!Gmw@^&Fo#h89R2r6!>rJ9VEt92B9}RPlRkfD{6;5lIEx zlH9M^-rkyf1H}d7NP*=UV7c~p>)D*TtiG?YOyaYgf%?_nhIV zDqM&=+WSJ;qLgM@EC==57yKPXDkqi%A#LbH zSH1S-&@m^3>q^D08mw$E>8d?;{*aQ2x)~F39AT<`{)ZX-u2{3WiULgFAR@#&m1JBp z%s9gLxX43V)6e4*O<-t~l(fy~3~8AzV?#s~?J2-~?mNr}O@ZFO+r!-RM4+~H-Hh{G z+3Qw;e{mZk{J)c2gg`O7O)gx&k;_gchJbCq?)*)A{Ol+sV0dtN_lx-cm$LGIMSqE{ z51Z$Di<#SqEiI0 z!wssm4>B%gZRmgY)jh-OeDhHi+^2>U#F;SYV6MV(6l` znY&@p+pnMa+g9^;-K+?!TD^%MXnrV%?ha)i*We86|9;PLm!ihlOpYdUN)5WHJ$RTO99xC(K8F8#w4WbFlq^|^bBH-2jarF!8An=bdqWpnne@8eLUbjxtJDREBg>9sC-|HJYELJM=X zt0@jTgS*?wC(}xnZu>>GO1Z@_AiUo4{-nR-h@=0v>PGHu68sMt@ZS>L?**%Er$&*g zeyDuKf`3Iuc4~I-&B+)4l1emDpkU2hR(6>2eM5RuQ_RVT2&$P1V)(GPmHn97_}y8h z&1m$=#oY%r_{~i6uJ}C6o8(D)>N9Tg>He@CdaSqz?;w$&Be@knyLpyQrnUI3$zc!2 zAK0tN(dFZu4>h!v_jkq!72G2Ave%Y~oXW?2njMa+GuOWH-1>GzRJDKXC{mmIkNKQukE06C}D*gimt~rCd8a&)?gmargH8=W*jB!z9O-q)y{@r?3&VdxtLY4juoNmGz3+n8D)7 z7U}&}&NXb-`exfHdA8{r`D*6V=3UvORKKe%&Id21W_g_cD59_^&ZmcBk zZhah8^C=>rbKZ*=vEX+?MJH39E?4o|6sDuCOwQm{eD4gNr2)!kA`~?_XZ2zqlzDz9({RKF~Idnj@}M#W*KgZ}CZ|cW7TFSHGAryKbA? z^`ba#>3Qwle)W%Io_}3?)Jsff)lTL1F6>$tQ;l(?`@VO4LV}>nd|2cN>lf6w zqKI=k4g0BHb_IW%Gm8(3d%tEu9WTmo`(cAT)|ZYWvvjYIcj2vX?R)79LOWR=@e@uP zKAF!FRySwxerz(ANGx1^Gt|?i;U9Pm4;Xj>Fz|yN2Ie;3>zkOoCP{-65s8+Ab`AG&0>J)$Ipg zWWVMES>%EH{Mru;p#CSvNqlvCI9}AlRc>0@2iY8aSK)n9bajEad)lp+&4AG`4;8$| zR5CDqmg+#2#O6u-H}V3rX2L4EZ{rb1pAoOzRODz~wa zkw)d)dqKu~X@~8XP8qN=$~TCfi57xx9O6|~;GnsdQBtUR)6#Q>{m!>n z_FQ=zpK`=OHaR8Xk`#}t?gZ`EC^4CtxLYBUpZW#aOjq%}3mc3bz^W!#)?H=f_;j6l8 zPtydGM%pd+7GFOu>od#dmGcS0&my~Pugs5{QjQBB-TI5(ziz&r^+0DX_d1h-=ZbZu zSebL$_xs}~M~=tA3fSINgb&|+q5#9rF%1EtFcVZb7MQb0-H;F!!c91K{EZ{vaK9c{~kTKb zWc|mP4MOSf@1RI=D9lm6C%(`o8tD{*$^~}I-(k!*EdCbxp*^q=_mGWq9KVlU3KF7h#~hpmQ)tr!wZt${-U{JqK+(+$!vajBi~v-yLiq$+7W(O#x1`md$5g!NbRFlb=r@za3R?q zaoM{pt@er=`l>w5$@*a;-P;)_F%AphLx0ZMgMDdWHIb_H<3rTbI6=R*tC9_6V_~1J zr3G5_`}bX(rI+b9X{Qa`qg4~Sujj`;Ef_;m{pCc-<6^!dw|EOOEq;8fJa0X8fC;~E zzka|4QH3l&`@_DWzKS7J<+x6jO#ejTgXdz;;gh+YqX)_bs|NFOU{_X#Ugx}O>R+b$ zmVIR6$n|r+ZK9c%(QOg_3vVw!_7T6tELf!cDOV%A(xTON{#wzU45wFnqL2A$92tCF zr2+(Vo%Cd z4CSv#JE$Ny<>O!TxA*Gn zh^8rv(4r{F7f>mIQ&7kT_=N3LqtZ>YsY(9K2ldnB4mf9cei-39q598joBzY^O7RE+ z0{oB@iKX0?;*dyiYWlN@h`jL6bB+J07f_VjiS1(#f!{8m_G=$7hgrIj1Hq3S+=Bj- zJJ|ZWbr1Om4#803`@OryHqY~~Ean#Vw{DKx=6HK*fu)ZC7s!-D#ecj{GlGkC z%5~><64F8;;8^|d*rxe)t8sr{dxUoTcWCz%!wnP5r-CPolwpoXr5^6DRw<1qlAq+# zh)CKb$heT7_NRJrD2;<#jSSP#`{iLGpWO`H-?b2*#roxDPuSbOstMEN{uC0dzjk9G zp?+nb1#IYKzWAE(hBM{oGImnJzkV+B=lKVLKM4Fm;12?S5cq?@9|Zm&@CSiE2>e0d z4+4J>_=CV71pXlK2Z28b{6XLk0)G(rgTNmI{vhxNfjdhB}&7;RRK;SkW_ zVPeQK_Z=}%Yan@UcAnE!&t;_Bj0k?yCV=x zsDmmFXy%3=ZX4W3kE_shR;kT94rm46%82IOwL91RX8Dy*RKls+PX{%NIgr*F&7;S9 zxUWv>CJ-jeE7F3eFF7*cCl&)U7BYL8~wyjOm|N{O4EoAa%^c;<&Q zBat5$pZLkI8zqw(wo~ZvEZoD`=FJ5@2XCpw{5I5;WnKzE=&ok z#R%-x%_4_$H(j!@jAa_@CYkX|G<(Ifbo3I?yj7`MJT>Avm~(dr7MsMBcQvHfkKnx= zoXuB`Rf?bUbg8?pDcPG8+{%^58G;`>c8Li(aE@urisgmYF}O@0K0mp$=Ly|X>}U*( zNip^jNwuwM^-G^-0o?M_8Kb!-imRP(a19Ncy@a6f%7#HCmd5#raG&HBM|zB z(+4IP@D(yg&Lo~6cHDx;(7q}vmy{WGy>^+}MdPJv<&3@0JK13&{o`*G9F=(deLTIY z_`8Tg;xmkbHBHdYgr)=hT6yp8Y1-UP=}7*n7k4L#byL|@!a6D^Imt11(YYo`qA&b* zS(7G5v+=}kvdR3zUSaCp!+SMOluA5MJnFa+RdfEux9F0NulN#;pz;+m#A(u4BmA>y z-{UJX!VaBZs>j$yKQ5J>ji#bb4N4%(3g6U3#%s14M%6vlAT!p_yYLy&zZZEqEd^;H z+oUk?li3DGpPd^;*B(d_3@2zsK$)&HnHQaLT!*|PP?ey9cL~jl8AM1@dkqK7F~6SwDh@eWg9Y3oqk*rb?u$DYI2pvb${XzMREtv9|Y+r2#)i zv}IWxqrs%u`Fl*9rD>h17T>Ho{KdE~y4>AICp)?@G$1bg4r`>vPgGXWrE8=6-mPzW zB!m6g)=Xv}>yh>=nc7EgX)<^1Sm-qt)b;O3UJV0ryJ6=y#hV0+i~x2!a~rmPhP z8MTP~5FRz|+|`PqIPKmIl$>jkz?x}Hrc+#aFz=oQC7N9ya%7gDgvG zgwFMmWi=|XncR>CpSH<8oKSJ~pr#sky&Adx{dT?b(!%wEhJLLH(wdiPar*jduCPke zOBg>L4FaD%^|TnVGmo@OoY4znl_PwK!_sqoaow2#x>RcVW4?fecwI<#Z{Q0n_tv>0 zs9I=JqH`J1K6R8)r`?L0Tr1Hcb3`Sky5nlq8}qM=wxLR%wJoDv0-MPa8Rv5i%ek)x z`ir=p-S5~g0UK@ikoJ_eqe&E8xw}g^_np3ub&*+iuIK5PqdF=y_nHFiO1I1;SP^{1 z-gJdN8{?lVl*3wdl3k##H~O91#TdQTsBBuYwj`fu9 zCQB90>(_@K=Lt=Xe%Zvk$WCy*R4*{l9h-NH9Q3d_#f~)K#hpLRa%W#^0r@6d{^m>w>3d>wLYlUZH<{qDZ z<6#e%@m%z*lFFHn!qQ6;MUOqN;k;Zj7&eQX&r+LtM5Fl1CpfTzY)?NY?^b_&wyl{w zp|tsh**! zW_jb59Fq;X4oQVUxjjE+rpPGFb|rUYa`f2lie|FUGsD@8^keH! z^wgGa(N2F-ISLzXIuz^jZPb)dRK0HkFL~A^cd23*`$3BdhcyQ@CfdfACep%w8pVfF zXhAgw(}wZ)gl)wi2;#raF3QDnh=GpIFy>jQPf5b7*K9L_(<^s5uu+bF)yswky63$1 zwoKeICv&z;On%a6=zr=6502Bl1Jz8V6>d&}iwk>3F}JWK5Y0t#2m^~=4?_h1Lmf+r z74-=Xs<-2bJuy}VeDa_6w%Xe8&$!bPD$YkUg=Dr1BNm<(^i&b%Y&aG?S=5K?*qm2# zpgkjvy|ZTdR^(cm507>QO*`Q+oMQK)Bbe>0`r-aDnL=Z}>zthuxej+Ru0=|@FVZe7 zvuP%YTWg7Y(DQL*b(+qz-sNBYM%@Ybm}YOPjzWjl_|=HhaUtSmrh+TW0;3pIY}rzY zrBz3ZVzB4&J`K3NF%M}b$EksJ~v|4S6UG(>|JJ|w7$ZtXe&co?U9D* z!Nf6>gFlePSI!$-C2H5zUP^9J^e(@oZd>Z;`K<$gVzt%lrG=Cr!pAEE~ z7BtL=jd5*6+Ju+BI*4CxqBTgX^SVGx$g(7sHgG3gdfZOZ z%U_w!5pi~;QQIrQL;cIfiqd=O0Xtvw z#K~b;&Enw0^PQp|BS(xb_@?DJBQCAzx!z?VyqJ75`ffSU2x6a)U(Ysg;?mZd?h^My zM9N$m{bq5{WNv?8M@+xor>}JS@`>treI^=bG?`(o!?`6wYni1xmh)2F7*qQRr$_5g z3BfQf#~Xd}Wo}!D@$YVReDCoQv1JwWxB>MpKkbI$d1`5Y6RT2>`oO(5;YU|bX@;Ll zAVpUDRzBbFH=~znl;4p?yylp&Wb*VT4M%jI622wx);;B@Oo2>ZXn(KE{P{7y=d|k8 zHMj@-Px=;~-!HE@{7O%20P3G#l_GXYv`$J^1Rhu>HTUA^$^ms7-@yIj?ZxP(!!#S; z=kg3=t>gqE$uPqLPrjjU$Gx}V(r)w;g-4)mq0iqOQV?hzGiA=CmLGtt+HtntlUiln zr0Gy}p*6a?xOxA&oZIP^*&HrYI@ps#ur}mCwsrF2oaDV5q#3VC2fQT1djIs$G-@?D zZOp9NG4gX!Vw%L1NBzxxz8wzx4J>1*&0##cwydX|H++=OOkR0Ly=*+SYP6^=tUXNH zO%>89Pe<8$+SNGtH|TZBk~Lx=l5#| zrBPGU#87GO5n+Nz<-IYF(pt19?AK{KSrqL`b&;nqp`^Wvm&rk8L<8PE5c}aqx3sgv zo`&_W)K>IIT&GP(r$`-Z-A2oymn9C;r>uC~&P>_AvYF3laoobPPH0?TQ=8jX&QxH+ z{AQG$lD#Hc-aV7+WM+F+TVez&mE?ilw+R@22D~_ibF6drZDNcc-%FvkUb^y6{!vS7 zMut+sMuE>x87$_F^-aXSnL`c7yf!|_rPXlEp{_kn=7dJ>w_7t%GwF2RK-PXW>rwwZ zBUnhR9oYq)-tKznbS-_0gA7`O;mDrp`%)5E#@eY91M%Gqyn$C+AGp~MQ@e)oW>`~Ar$7Y zHN9boxOwJy^}duo8~YR&sGLdIm)H787b+9initmM@6j?3P1Ss-+@@a=L(dw|)VL;# zSKQAaD8}4ShcPBIw0!Cu8zI=A@^cAe|Bxs#aD(f+bO6UkHFj=jvDMS2XB{?X($&W} zWt|QfT?;58o{2gTe*9C77@xN^X-q-Aho62RoICbeyxY0*^>9t6rujgAkCjgK$m_?_ zbtBSQpdGO^1rM!uV; zihD)>*fsmsFB!9!X&6Ge{Lc%^6f~pVvI_Pw`K^8wmma9S6!h}rA!}ynKn7>1XyImB z>Za~*CxYlBEXLkvp@-2_<<{reB1WSJK(=xOE{x|bzs`u)fh zyTHuwacn|VCQuf-R#SUs;f<7aVA(`KK&M zS85zbJYELhD|vfhg`DBM>un_KqGtN3XfwM~7lyU`8un*`iI3z)dr$Jf93woY>P-wG4IqNLH1f2X3j#lqS!Yp4%#v;>>ZagiMWhi#tFWh+$Z;1o{T=DW5W{J#n9UKYadwRvof*&`U1 zg@#{!qROkP6xJWzWO+wx^CjtLBTXNIr0(;;_TDvNdlAHm^7S??;Tt4oq=!Jn zi^rTHk^{s=;-x?9rakmS+%T_qjVvMBr}DXP_GiURve$(jN>*LQr_#*3w&kh=90QkDxM|I$)6`(P#RG= zUmW^yNzfr~N@BJEgX|EEFn`&W{^shU`s2PSS0v$3s)HB4Y18ShWvu zIejvF-iF3`?(u=(O+(`TLoe>nanc%KFFMHeMxKWBceWeUISpYIRV57&1*khkl6kGDE>W%vZYiI_6Ho09|MNi_47Nwy12e?7Jap!LWDrRnMp0vRmv_r9>p; zdxN2s3Rrx5YczX5kEwX_+e^I+Od(fxHS&Z!*U(vhbB||#SaZ;!c$NmGzWkttE<|vd zsi{CO1Jw%!`i)~j*@mNM_IDyakF-2WKf7OAvQeX6IJ6`jWx)7sz$ojbz|rLALV24E zYNl~(S7r!$b$1UbUwJQge^BU(2BiDc%+uq65BXeKyH4sVj6V62axT*JwfC6q4`=R_ zKBzg~ws-dGQGpee_bj~j^@BRM`R*+tAdWIx$hq)!6iF{VV=NqdOgl|tnE9ZKe0)=N z+DgvZDtB|dBel2c>sOqn#e){N1p~4q`1DRT>`h;?k~2{-<}>+a*SiOz@?u=CznoKEn(;uA3^>>LVXT?f)u~d?KOWk9N>kp zAa<|WL)$6$cCtVgy~Wg9Sxbk;j6;1-4j^SL8N8&ph4U^9dI`Xb%eqv>LYTIwqK+ow zpY&gy7Qk+1PvO5x2}c(^J%&qEDf1TQ7w5VqNkUYmR32s-(C{ADT4DaY7}i?Ox4bp! z5K@>PADw7~Dpl`^@_-$PdcabW_&hy(Eq^+?Xq1WZ36s4h&k@TNVrViyuMWhG_^y`^ zuN2XWyAjdft|%A|{!@iPXW=!a&bd8>FUCWjMYA+AU2iOH)qXK?gf5X)HeL6Cdb@Ys zJ(&H&^WC-^abF&FX`eL@jbI68G>b`KBH)8l$b%8Af^*H1rPt5GX)|8mALx$NaXET8 z^DupG^N`)mZvOQCqbTo8FvnhVRzSLPIwXGgxP^iXd>a&_lI*#p^v^XrxSz zTV>YhR*!fz!zsRB*+Y-M4fC8lN0{e>ZnJ zN@htp!t1aGBNw9feTL*)v*g(is47RV73szgQW8xbyT9na$&_QH@l2L}TUz+&-q@&I z&+}XAJ$xLIP1oDaE%%^s8&cmj= z+IK}p2FHARDVG)KM8CmV6gkL%xkB~iXn@ztSl8=1U$Y{UFHw=t+?P`g{h(8d>Vich zT4EL%vnvE5ItyFW^Y%cmXP>);&$T>JKkOA+8K>Q}2SawYq`BS_B0Q)i-$5Hvtk{+t!}iQhH5j_iEqrEg0ehPs7`ylwfs8)t(}Ib* z7&$Dru-5k?-S6A34>{|T2b(71qpP|0oV}UUUOy`2&Q~bEScW(|TjLgA)ftgeUTCpJ37f$C^C^$%KFz(#!e4Ap<0H{6c8YC0@$423GBB($!ja~> z%APSp;udSd2dQ3;%Ck4th=0i!do_*$~{F^Y<&(*UxsMH0> z-C4xO8XBJ|7e4t1$IYhe9#2~N(neW4sp8WyZnG$BybrnDOC6~$5!Rc@8Lc5nh4#A~ zRamabwzhBV40wB=840|(;j3k!9@Ze`d2=`wa*^39 z88K)2{%%|Dh+HWx!_`nov0z2dxN-l=-lYBYn|19D$-6Vk(A3LRqJHb?SFn}s_+eu{ zqa=_r)|p#p`&8M6SXon$ywio5R_pxTHZZ6?(tkY23+F zd()vCLbU?-pPLD|ot$`}zJ87Nda8`Re5?}k<2%oJxLdQwt6S1o@uAy=Mx19Z?kZ-= z)^yg3;@ti8#dUKIuY0lcr}@rNlbE1dq>)?aUCP@$3zQ~5Cp>RX&vPFQRJq70{*j~M zB$=&OreiXj?dqd@SxH~npng5j?!D{8kls_PMs-|Ija{}B2PAu4Q=i*)r`78`WD3Hr zJ(tsGl2B`D>~5NfUG%qE>>W;1y+yZs$!R4Ms*kbdNW@DxF|P-vtz5ifdQ_<7VDGn9 znYbhSiefHoY`TQLIo4p_H`{6`ZM*qqDoWKVj?}r}z}YIhJW;`YER37`>*DPeRUsjP z@}d||g(O$w4NWW%iW{j@o z2V?>|8fF~B_qAi5H9mc#OAfMJe|oeak)`vw2~7`zmG{;s6An8*DynLppoJp+g~+k> zi61UE>x&0&I_Q{AePd;=Rk2Yp#7~`hTz`9!1Lh6!u1E;)%%93t{=8en-qrRor(6BQ z!VgbMq&|s9oA6unM$+K$OIQQzV@qQE8sBvD&=*=IM2f+Pt3v#n>T(G0hQ+?;W5NUR z&0MHSGVEl4aWq{$?5txQ=bG2`QTnj=@w5?$^4mA{8(jPNxHt}(8@tl29ptsRl~ksw=S2Q zR;hKZ$wxDl_T+Zw-(k&rcVX%}6ndAVaF1rr)JBR!h<~W6iuxg+H!z3}-Ui_>@zPJD zJ(ancz4A*QCy&FS-V;^5Ar^Xmvc?Y|iBEg`2r-26NY>pD?NIEjjA7Zx6F;hf)451Z z7si@B$kEEC8NTNsvkJYVzzV`5gbTH#78J&@B*nrm`b9CbW!a|?>9)2^f5T$K@x?}e zB5yDR_taG-zK`@E|7%wkU1q+_jgLBFig)hp=d~=Bm~5W*P-vf#7?w)h_g<4xFZt44 zCoM5E^&tyCG5L0B3qxniU6**C`8hH6QgQWXbzd>{e$^&&W~p?F`S#VT&}C2igc;gM zaWBPt{$Zv^9PMb%M=}Ze?>^mBd?`Ku#AODbM@IVoMH)1vchsB}c}`n;}KRC6BmH>am4)NS^#K{*I4^?&^@tA;#e~xBM4F{x$q&h51GB za)Piu_Z#(wOi7ak=s}|kqz8rqlHzmXO*Z5FXG{05-WBN1T^cU-oG(H@P=L;5Cw%O& zdDgoRLo+WXNhMK;>!8WhJVwno@8Yogu$V9n|KYCLg%Qqka~%;Pl>uL)(Vv}NA`j5p z)WXXMPB4r=&#S3hMAYH!m_+kB+F7|fT+tgGpHT6($dl8CXR4pcre|GVxj z?^9E61-jPr=9MBxaQC|7=}R=zb51?$y@KYy)1YXj#lfk6IJHL(=f2f^+r|G(r8Lz$ zSXb+KjKnDhS)ot&x_0$V!#UHA7m}ntD-VfueRZyWfOsOoudz}TR>Vu+*rcd-jQcBb zQL6i4yfjSoW-}x=-HnM)Od&!iA;p(5`_V3jv?lNGe1q9DFVgAG6m;P%lk#(3cG^U+ z9Jyj+InK#)z$#<@5w|~Ce5*uzbys`EwE^AuLiJ&ug0Y#$=w<1tm&4+v{ZuTn!?%YQ zzcu&KE;9PQVjzlptXtNDb&?8bx>8jE7tS~ z^T?(*eZ_-?BP+-%zGykmA_4artA!k0F-~8#%Sd$1Sz2$!8~MX6Qg2|x^y?cJODC4L zlH1_|K}uwXB<|x><=hJz=yuZx$)aH28LNx6gYReVa3DYVm<9^my&M^-n75i7BfbY^ zUFJqRY-a0r@5)7<7-pY|Dz)Pr&vWw(2U3ny%9tGNiV(nl+Jc?#9&*q4^rG4*Epn0t z-*LMacFyY2N=!zWU06{U#>rJ$fRla=@tqgv2L8_wp2&jt^cyTP81I*p3QySXDduMC zMHoRs4#&SgMQy}Q4aw*j6A~4Kgy>ZcPwX`p;8!6zKL};X(@WH~WpkMb=w@#}qbsee z(eZ{(hIby_bS1>KQNr9Z4EnKaF+M8qBXj6El~=*XgC0w&?{bQ2cNVjr64(oI;T)Jc zoFUwzFEJj~^Tgsv!T9mzH-RxGq0V~zb<&KTJ(>J1{XatR8jl@fLd?UXf=ebenQPI^C(!n8kFam4Tp>YZ@dDj4@XnWLS84YO&T0FwYZM4oA zy|d~zHb`36(au~KR?k05a3A%wFMti1Ln#&l8@{VF8I@z0Q*QI z6jD48?bFxtdFBO;Xj8}?2vS9h~Bdf+xqi3bL2- zPH7I4?&GAej_f3+VM!|PU4UC4EDvduN>K7R|FPlv$%{?Ld^@cD;v%bOLEcxzO{{fY zd-^aXi(8^a)QKF=LyHK9+Q=Fw|vjE4ue^dMBT_Qdv$qn^4)ojEPUjS zZu40arz~|wsm_&vMO`>zV8|T%4Krz9n>Q!RU@PPLFbjC=b;!iDcbyw1Dtpm*6ky8J z7I03&GL(e$y%BB)w=~+h9J^q{W&7bedxc+jFvuM?M+f(?J0~68*M%eB zvxo>1d;x?I7)`?VVZ*o1Pn=khE^dN`Mwul-W8FofYTV?OJ7t@tbnh=-sOQEl>6t3; zhS(6e^Pgmd9XD3LxAkzFep4NwAdwSJ;{wgzuyib-n}nov6f5>&rx3iOd-L9`BL`it z7#X8LST-5XS8@S)SYPPVVQ*W#!zI3>(gQ&|N89Mns)a^?SmJJ;WIc7ZVP)3k$ur{T z(nc_pgJXcoT2fGPVTqNIkcANzP)tLN!qSV6DYkEy5`J{udS8;MZ$k9kyGKjzfz3-tvJFDV=rNO?TE9=^6q|CzfG*Xa@luZjd?&-o+7O%wWQ1rP~Cu$fYzM&xXNsP%5R9BsT}LZKKU5)KSVev1JOXY4*W zOo(HY?)^ugi)GQY23lI~=5z@oQ*7wu>~}5WR~WD(W7k8%a-^4o`w%6ACrQ?m$z@>J zrxCW7!$`x)6Lq1i2Q-tAm3i`RZ;XW8-J9MRz)7@=V>NCyV^`o_gu7u2mQGKYcyJ;F z^H8;uf{$Ffw}UkSa4>*D*9t`M^|wCSG*R^o`AT&^7Ze!X#}IeqP9PFpd9vBgnc|)2 z+%aq*385iGVbhLP!-3v()Z2O7aI|1;2B@wdi!0L2l*Z*Y$%y6n(m7Qqg5BwJbwPR@ z!{3x>z-?Win!;Q=$4=qQfyMoubD*>ia{j11w|^>)M-yZCl-B`J1W<(72ZvD|9IhCw z1frRABxy34h-5CZwnt*nO_Ze&6nUI(`hBlvPg&P%o1!b&F?GY^e6wE>M%PRW4-(f59faUH zIlVjO(*jje+?Sn4>kr3SHd{6(u>KzL2dV?x8p5o}N7uxL^ElbHyx>hCqvxw0UB}96C#6=;@LE<@r(S-ekUx!s- zVdU3G0jp$cYtzZ~96{k7iQT$_{Szv45(iid!htlnf=P9cp>9*L6zp^(-e`Qb0%ow> zQ3Ik#X2M7~Cjq-86YE0{(6paavhy&q?^1DC^?#z&7+(=7dMhSh7R$cKS*R*1f$u#m zb-N*!Ifh>xj?3cEgFBB0RPd~8)&7C`E6nwQ%cM4lXc&aj@`K&XZGGMtOZ<3}o%aY~ ztTC5lP)fT4vQf2tt(BWLAzTS|?qfMesB|YVyr)-2C1Tqqg{_R^ZhW*plh-hZ6kmMx zNBVmZ2160Tjp|sS-TGqSg>;@oEUjKcx%N%@A@G* zriLI`#dI2NamF~ulekH~KRGa0n!tN|% zi^Ndp?~&Z8LabPtn)vG|)O^rKd|7?mE$Ih`;-f|?WZ_T~zGT*(3&#>!YGZhZXz8*G z{g}uY1Q~pHg66&U(BiXo+7=$xy@{y9w;xMOvyagi@wr5S+O`nivE~FT)}}^ZD17MlWrbkIfAyMImQZPXXrWPoVB3f z$+@jrlNwUCP;w<;GB^Z(l!-)ya=>$UMQ`UNl3gm}K6Jk}gnc5i*IN_2K8mFSaVr#W zQ^&5pRDoca(RJ;~H-Qwt3U&GXfdFpynUaeGWXq@;hd^{Bt9)2k46l{0Ym_XU??aL6 z$Ds*u-`aSAF(bU$I){w9r#NH6>$h~jz3d|*7h!)eOoR&1t$kMQ0F);t!vnI8s7sl^ z*RbSfK*24W9pkc#CfRp_poFEkJ|{u8+dDhe6f^ebM!2Oax=yeOBTLuj>cHlb(2+f{ zNAOB!o4f_`@N~vG%wCU+(gE)a3r5LK*krLlKMPU8TDU6^o=Oa)&nz^OIL?z{dXLe~ zjf%;FJ?S%Ml1#?P=z8z^1tkbh6x<$re(i&py&;}t@hmMg3Uut63L>rPVuu0E$eIcR zmxaR>d;rNR71N1WCxSYN?!^He5hK?clhX;D_ij7^11RyfFbhUHZB;ldym@1E0oxet zmYhA|x}AHIyRBCpvao|ZB_^+drXkq>5v0R~N)jEUEo;r}^}pEZ#XVEFykT$>rrV}o zQ3}W+WE4G)W1mH1j)>3-jKky$U2x`I zJ8=U|BS>R7SN;|C)2fR88^Xy&jzxG6q!_YVAxPXg30v@^A%5Pjg3??V)m7V8%uLle z;p&0Rud5f^-YY1%=ff>J#U2onnX6C8ocQEd(Uv=*VouwSy&&!&0#GhEGrkhDy*x~G zVJr&+Eur#A+k#ay{6{`YZ@E}(&0T0L?Mp`kX{PYDJHrqpm3_5YcRzrh6Qs9~WgP1j z?p&g#0~vp=$}uUa-2d&x=cUU;oqk|~Y#~Alq2*b_xW2bE!~+OsrgCf?0kR7fjrfHg z8Fz`M1z+;8!R86#49VCN4n?^IfCX5L5XKdCkOAjAyWZcGwwGdFTA-yfzWk2Pn9j2H^N4bWCy<&sT>cnBlMY_n#6gc@% zUR!qyeu}sI1rvy*<`!+>*S~#`l+&ZshWNv7t1$r?-sOfCn~=(j191S%g`g200BRWR zmWcc*pd~0pm(52kN;qbQ=fXNn@5Puy70)thAhJe7FEf@%es0HYd2ZoV5)=*dM zni@RFEgdErLyxpP`lvS`Hl093>hzHX(j#R44m9A{*ht_%3=KVGr(UreUGb&ZdK^!H zEU>DRke-5ptK^(L*}3@`#^Elbh$z3&b+|!LJiXbs5DN1-H2yV zk;q9&*D1oxKoU5zW#3LNTUd;hi)DiS?m;e=1W13AZmO{nnf(W9qmCBtO{j>_Htx9b zV?mPv|8PzTXlNECLhNvipG-9TtXrE;8lVcC^tcpMj99385U>eBu~CXQdI#I9pX?qF z`#Au2u{lKQBn_AxRH}q>EtW|l2M}bu2%>f{`gO*Af#Z(-~$htd1IK z(&&%F7dU|kx#T#YPzh`4Ybr5+VrUTHjiX^KFwYQAapSEip=@jlrYWPUT1$jZTkteW zY=e*h7adi0qY>0Ik;MYBCFJNRvMhGJya-gmtlgi(TJAN%$4Zw7m=LwaY)zM`)P9)M z=s7XCBm!yl04`QcLIN|jCvmS`v~d(8llLuVLyj>O0?pD7JHiC1VJp7+*0IxGItD6< zkSH8=WA8K|1PL?4i4g;R5Y}C6Tz#RL^{o~;ya+h7-jy|i*oWfl&|jr4Le(V?h`;C} zrUclo$9NKg2op)h*P*ZRq$?SK%CCVFdrD`z6jO@m&mxDI^C&5v1%@HU_V;LtX@(@E z5)0rKBs$oD1pqAn;<6J428%gbzSVVeHzo!h$B-~`QW!!Rd^H6N5wZA-?b+SI8>c8g zY6|eW+q8qj`mP~hWZ?)fvhNF{yGg^kh~FsEUYVNuGkZ5(h#RI|5Ua3uRtFD+hQQjS zLr|27wtlkcEX3mnAX^)&w}a;UzCwzlb(Pd~G$nL)3njp61tcBMp+so7&zLe9B#vD~ zX`~Q^qY$a%b0uL*GbFe4GfOPFe+kxGQP{9YEqpJwm!zZ!cAW&taFuX~aAA~;G@BU+ zHrXeC2fA5=3P+~`kFtsdz|sqf@k%=SU8zKbKL$tr3$w^<+a&JgI9ydFa_KD8 zNCC#Erb=YG3mHlHAlQ-l1@||CW?6EPAK_$il~riEQUtWSAd!Q&74BU(pxxbYQ$~^61Om0xFECqPD~*j6a`v= zH1e~{gpYF!*-l1j8;lp8k#TUdGfol{aeMe{zt@0Z0@z`)f@Nu02x$7T+7!@P0GS7V zVaaSSv|uMLK^H5hKfCBh{p_~`Zy5Z56 zN`VD?+)%Y|`fhpS}RldtuN&@8k{0V#=l!GJ(Au`j}b z3=G&KijGpOx|uvv1kdE%O*SzDf`IST1rZVLgeG5{<$ND7h^21paftFsUx5-kArYC1 zi5VPlaa{(Z3X+p>1L-0np`8$s6qr7^Jx6EG!GgAZS+R!=JZ2&inhM|`P&uxaHWEsN zhv?cXasiwqL{wYV$!yqQiK9F3KZg>bcvz+DIuU9gD-pA%G2mojG<7`d1EHBky&q_? zZf0sVc>&?1)KzTR*8783?DwWC(1V$fkS}GYeT5zzdoG$H0$!rT69>aCJ~oc3$)s@G zKoU8~U{KtBwLDommNLu1CJRkPr%#=K11sq=xGBiD3rs*030IL)JfeV`WXJUE7$MWA z`r}Z^dn5cXoLKg+r4EenF+%G^vu*xRmuY8Ex1CSgT7lbn=qlrCHXpky1)u!c1iu`f z(FY~9?-_M$Sx8l|eOQ4wAA^^V)b6{?gHkeqsy1K3=&p5ny|M`OFNDXvhr_zv<284n zEexv#^hi*bx$KU8l*pb&U9ggc*6F4&FzJOYS=PKLoaI;yqo)n13j{B7vbYZ=?Ux@W ziX9vneDBReopQjeMFNpdp%8K6%i|_ct0M$YvTT2&nJ1}g?_U24rz7Jpt4Z1CbbXKd z*pbdxy&*dFv{{EXHCPPFfHW>M6j@(+B5#*Zx1y&xE!xspyJ8BwggDXJ_&8>O6SLwl zx-%e2=BJT`xUm#qIH8z@(WD%sSGXhGoWV2#F;yEephPux?yCmk@;Fq)SVN2HF3gm#rgXkDmj zzb_jtg>0P#l~l^>d+yFiV@pSBm6YTKex8~>GC zT~f=FnV3vz2#8Jgx}s0812DCR{Tiq=igd@XDIEA_X(nRxe+ZfWy_L<`C>N0&WGTd8 zQexJHeYW3zY*KWT!y}2qs!?t8LMrTQ>i|(r3rtUnFIKMrugMY2Dyjz8Hi+k{em8;- z5!wC`B@8@Gh7O`?CljV5=$-Y5{2_NnSTWk1v-}Ch}mD~ z{$r+M=dFuy)*jnLh{VF5aux{x!owitLgz^`ROxxp|2hd9rICG0y5PNH=i7 z1*)8MnN3+vnLNc~Z)6qv*EEzia}#!&Xz~b|Y~3?OA1|_Skh-oI0JacgRn*!i^bKGY zXyO8LD#;)$t@CwT?X2_WIcD#m2%oW;19ySP-r!?P&hgCHFky?$NwD)VMOD@t?W|Z~ z4AMl~U8U|4zA5^aRB_oD5@n+r%>bMfVueeM{ft{B8Gc!iVC>PJFY&1ZBm*WMP+>M( zBC<_+%+4bBKX{&k9~c4>61+>?l;;b!;nY110(H`JXSif&6q53bs6hl?BHbjT%jO{m z$9QjQw$zyrf=taw8{L0v#eNc8v$hMle54ycD1CcQg^juugI(dOB9pI82Vp3jU z57anpImzbddx|a@rNqFWt^3&Qu7<|3PYYON%gTtLW(fTo^cuM!8;u^l0=P2*35Rcp zV-Etb5bm-iThlXe6>#U?T_P9rF(a;o99yDpgL)59lVvB1@bzintQaN4!52+UUu?~= z%{z-r`WWZpk4(&liSWck+%mwHpX-8}W+wD=0pPTlp%E^eC~or&3H~sQ`=><5Zr+H9?7|eP7H)Hi80T~Z zD{=9$N!d06h=h_1#9O}>#N1s$yYY}F4v~>Ph)UbP&EX-=k%%w<+d12H#>z$CS4wV9bg+DNgV6JlX5HUlzeOjC!f;g+V9k~XUjZr3Q-ksTh{c6F#>Hbl$KTeVwlw_VlN#}l z7X%ZY=5)h^mOPy!ihRvP(56XOHgdS)RS7t;>^Ce*x)Jk4I{YZn5@x_eSGH0LAFL80 zjz-Y2w#2dDY|vuSVOwdtlhuk3%JLn!j#3;=h{*Sld7tPqKy4w~2(`leDpT*V7}@u% zNMbt91O>nfo$EhBL{x>gGaarq!^Pdc(ca-8EvUuLdUlB?}#e zuXd@k7cQLJK*|xKCSsy0wAs<&N+(W3HF814%XuNBGR%0p^+@0 zd27S8n~9j#rEdgorI*0ADfqB`<**ds?&z}N-7FJRDXIS;B68?7{b$%zhg|eVA;d(L z+9p=wm?yYL8+HmZ6e?#kQSU#ZK*lK%YP*-GZnKs7BHSb;?Ufs33zWFc`1x9;5>1x* z1!&pyCP5T;o@Wx1=+7mAiPf|*NtaH=E(Hiox&#?u2J6rj@MXUs&Ia}i>yRfS$4 zJ2pKBBa0Zd7#D`ZFHyW*w1uR--6lDpoBLI~H8KH&JxS)pIcImG7fDYnxOJoT>s3_~ zxV?P^U&>pwN1wjZ&AN0l^ubR(E0MZe-IG`yTV`C5b8h&uiX^ zbDx8DV3*`0ozQq-$tCX-oGum&Z99>P#Nv^oFKpk);|W+m*Ju8;4;GkH1o6mfSZ2ny zsBTFicU4kLDBxwb&f3Q5UPQdKo!1@${~pjw-aP)TWtLB$!=_IexKv5i$`T zjViQfmMXN_$y9h%D0ZDf_kne`+UF>$mTToMhZG-B!JlXr-p+Ke!Znh#A|Pf0Y@MUB zoTi#sMmmLO&S-KNK2V91L& zht|IRmmrF%3T<~X6<$xo{)8sb1d&0NqDy*tm9yu_Xl|0;wh7?mWeT@aC>1ezU9K7l zOT`})L42x0+Z_?n;X0YrGLAcaIcm_j9P2Hu)6GmGbiDQ@b{%SoHqe1Bd{J)Zz>Fg` zHC3TnrGX4%PaRtePN?SO@u(v&_K8Ne>~kqVpVYp01+WXSH_l|~N$1^6CW=E@B3U7&oJc4Wk8s#axxh1e!W6)*Z z=C_tm0(lbT-KNx9a0EESg&!<{Y{eyt(a}v8BQ$!878*EI>Krn(Xk6v#x*+>vu<3!p z#hO`2S4qNmQg1$J@0sNJfe)|*Xso=pt)JPQX#p@feuq84E#6CLJ@?%D1&jq8_65NvC0{N_A(*jwn%GuoVxqFJYAU)3+jHzVrC&TM!d21V<4g1zaOl5hzEQJrWC;<0QJE6HE}~ zF))KK-Xtd5W#e3CSk>Fp59U#Bi!?wtDpj}LLM)if<2qLFTuqFlFvd+40kVla%u4$@ zX;=FPqGf+Ku~_4)9IaiLL%2r58mGepZUQ2r&Srr#eaCf7=ea4Fv13z1iNurH2(?q9 z2xHlW(%#mlAP_f5NJ%s3i9vkfImu8m3&^CCo^K}Ij;VLJQGrb0jV4Jr$UUa2P}dfU zoy6Kjh>a9*BS0G`#jg7Lhw%{OFLE0c*|sL@$T9@WabX&Vi}JG6S)fKTx_mIEhjzD# z)%MW2?YC$o%*-`2;%SGn0i2@qVJ;HIw3ejWg~&Yj2qVyRfZia7f0T zXUPi80XoDZCZb|%#)Ih+w9U+GS!9NG(3V&>@&^>!RVNVkng zamh%eeE1z66}Y|WDz;F9^a5icmZfw99;(w-i~a>AhM&%kcNNZ>Nrd8t&7p%+EE7a1 za;woWCUM!Z)b5HRT|}8=1~Izcm_n2O%~Qxls4yBMREc>1Ah%9n1Ri8Uh}^sj@Hm?Y zBp)n=^H&(A1t3~Q15kaNW%RoU*dnrN+;{&oCsA~a>ExUNAHbSi5g~64b{|`TFD4?N>@pETq9T^A_4wH+u|7a$<)!kYw*rS`T zM;3!}2+RJ70jVin60BoIxJQg+S4w`r@Pa!#~*x(iitd8e-MvP0mR0$2!<)%;`_5Z z5onyX;KT6L13q`-W}uJ!6KWLX(V(;+mP3=hsIOA6K|p_<>Ej556a$JzydL-Eic4+Hth=WiAW(RT} zMpb-uy!I$9@UM8)l2I;g9H!>yLoji+B1{Olf5#5cuhtDsrE3~t8t$To%A=D_5`^fn@30?(T`3v)$94()z?X!EoBaj=C`omX7L3L3bh;XGKd$V~sdJ=>napcd1xYwvQb`7)>~sfWz8i zt<58WZ3(J-W88x$0h}Ms7sJTq;ddeGS81$ zSC&)P)-gP7j#z?Ult{iD=9V608wjI)l=z^WRM`WUdw>nuEjXkHI1D5(%;sHYse7v@ zH323zF=4SAmKHiylCd}xi-*xeyspB=Rqv39>1jvDC?mLJ7NVQKI_LK44HM)Zo*X9J zbIana2Qh9l*hEGhqDiQe2qEKhfgNyIGTXJk-nALCNp1wP{6ku?7-;5{X8R!rjaR`wbvmv22xUUV3%L@f79jAx2UO59 zVTc<}Ne>c4(NFi-u%Kg6A|~3{8@l^pb1>_^AfpDMxFGIT45eLX$`V1dQ#1^NAH(#i zchRdKvF!?q54c|IQNb?{j7o?uoTVo*t!GDCVpzRUlEeBbY>|tL_mE-DFh$0HPls^Iq$>>f)j*7%Hgu=lzy6OegfD+b6Geou|7jKo9en7vJ(;KW^#XA=h#b|IH z*&eps!1gpn=k8rF4BzesS-BVZUFe7Lv+vTLnemlFkJM>B7DVI`50>ZJKQa5qcie(B znA7k1kQ1+FCdP%YlHt%H4?#=zxkxvmQG-oupnZaxoLq>AR?XFW5G5emB@v0e1%%;w zFb@^sZ)xZS&0{Pu_du$py9XkeiekZvMt=GRJ>FcUEkh&Kgkll)Cr#ltbcZ_-hYS^y z^FTJ;%Jw*CW1phBizfQ_J5zs>Q@j)sq5?6*AxU5qt7%%z6|j!9)^p@sc2;T%3ps%j)So)*1#M-!IXEUUeEf4^PG8O=Qo8Dc%i&+)Yn zDhtF9K&a4_B7X0L_@yxpfQ1P!Mj|I@(W|Ukj%tbicu;B~%ky5`-b2BJh~D3Y*#-z) z;0g;)c|Zp-a=8n`ZDOb{0^b^3Ca86EOup~xD*MafhnH* zCKM_P)HKC4wMVwkmbztHJKhF4Fy)Y$tjq~xX-h)DZRf|_s|>32AQ}Wi7fO#m7AcTc zgw$?(K#ptPZrrP!LRsd(7UcRULxAvC#pNYn%k;qc_D3U=y46De+~1sC0Xxrg7z--t zW?wY#!yDZ1FY^FN4R2ydxAmfF??umc=2(+LPY1XIqpb|>rapQ(aNExaQJPCQm~7Ad zw5xX5y4H9g&mIFfCwrZhqD=dcqeQ5KMHdk0mk)%1#3GLY+iA{I&WxYNQZo5{STYhO zl<3F39zI|LG|R_mkValY0%3%YI9STqE@g$ChJprwyR^UuO-*4U0t;Uq=8dUh2d)wn zNxn13IiNW~8xs-yPDD2J>^V6Lvd>*As3Z9u;91fU(znJ-i_88%jeyFR?3Wimn=G=~ z+tRYL=cz-;)eS@C4G;YzRhHX#ZsjF4HI5vHj7C{VP}vdziyT9=V$v5MXq<*cPkVeg znE??)9NLSGfE>-CNXR#BL_3L3sXz-J#_83OiGX&NR-w?3Qg&s1S_hV3aoU$oq<=BxnbV={~g zY}${E#RkHVNdngeRyefN^9t^iJ<`I~lAFSY6jw_7;!^}V7zVKq0dDX73CayJ>cd9Xm93?<#D(yKy$cahm3UVnL?$K{7Oy;Y2_9n~*~2+q zMsX|m%}TuXWq!~ixj4N}2YM>9)qP~tmR{5%8hMcsf{GK@Vv$)`5RheMB8c@3!5z9# zVMvpRz9Y+&LrRFqU62Y8Z}-@18sV1MYBEkgeI^X@O7$Qns>Fo~Q-#vLNvPwY{SI4V zDqu%7=6&1Wrjn{3*bqR7M6#)54%p!lg%vw^Q12ryAR-1q7Dn7%@qJ-QqR63$A+Q>O zSq!DSJvjI$j=y~vB(Sr^v|cd-qmq~)+z7>Q9`4V9loPSTA@H9Gjg1W7XYBLCcsRgK zg)`p`l~m(CaI*U@Tms2%PKFg>knM~l#9L&a$hLIvVcTV%^Y7FXcOP0|3>KTh2D*jA z^np;GmMgJ6`suN~b&C~9+(;SsT6Z|38$l-1?s(@VhQZq0t^cPLvQRVR>ai?C^+JD$h=2r6 zU}wAG4-CC@i!=)UVE87V0Rb}rjGsGtOj<)UGXUva1v(WX-=+a5njBzZ^o0{WRp^gB zqEk|H7^NGYxnW-=vxXkwva2@V40Fl5d)W5(EFd;SCoKO+%K8}NWP*sWbSB9^b}wD6 zkC@?yk_W9|#8IRqNn8I7V2q+xbJC@q=+D>ctdKC~Dk7=&2WA?+tUEpYQHGd=j^5pI zfynB8HL3P&8;S_J0~1TY4dv+(Eb^C&vIH_6ax^rE$eZQE&>0=K^T_Id>KU-dlig?k9CA=LqJ+`-&V@%Oey=LO36s>YG17r@`c4gg$voN440|%4|f|x z-A+o(andb`{P89GsQpPyty0Yxh+~eM*`bVs+5Q;;w;{k&Q-h*7@`S(}u%dOc4j7n> zfZmv5D5zKm2#HAs{1)ur?xpxJI_IFQRE6Z+I#;8tl8j!{$mxL0$#PV698b>PK}tF| zr~{8SSu-hU<+fe1NZj4oTQv;9`Rq{O9u;H`x365__k>Oa&ML&A0cWDRkk)z%H`W4# zr{6${fmNoN0UuaMXvIK7%U_V7<{((1ZuTCCJ%kANmEag63&%ZJ_O=oaZh&I(kR|3Q zDIg#-nE=JnGsL2CJdr-N{UaedRc2W8z5QA;El5 z!eIer(7U9gXKAZ}Rq|WbX@CfDVs$P*pLSQY+{P!fp7~{uHY!8WQQ+&am_VXIVmgc^ z0nQN%7n?78*&J$@!iFfYpy@SJjQCZO|d2P$!XB7d=| zQg6%dzXOd{Enov*$6{_`CQIq`|Mtg9+x}C?!lsh+s|!O^TLvtAOaom+yBkTzoh?E{ z)qXNH+iX)MTc4q){kXYwpp^k1i+qfYGNaC2pWRgUXShJz?g*)p_CYzP$PChnUn<{^h0qV)QJIU zhJ%RZi(xKSA+Vz)n7oSINs~Cx0t)(^+Khrg88!E}Oorn=?-bYr01Xi{k-a5J!gR@` zk#)qeAlJ|7qSPPc*~b9L26hXI$CoAe5$t0=W`Z+x>jxErcWDmW<3n9x{>w8+U!iv( zT~I{|c@UADDRn8Z62=)-Vtbu7+@oNmcBXHT3-caou@V{^$JjJ4^5rcK)(FFL9 zTL@iH2VYzWS{NL`xQ9TWGD%Pv-V}^Gpt-~lJlYxC6P<*zY=lBEe=|0~l3dW_aT?GG zp+`bDDd;m$pYgvZXu(gEgI=$lW5iv7!Q)#aI!_Rvnu?o6tOkXfEDf2$ssWI&MAnDT zZd&Hh7-SpXY{=>#Ot?E~UkK?~86!$^n%J*ueR}ZhD-y;uxe25(V*igM0YonmUBIz) zV37nwc1cL*NMfCJXwy9w7{2HU!lD407C7G{LAfd-iW!WzBkleFUjJIU3Cn-*d9iUfMOVKR^XDdocCG01jG;g`TU*_s2r4Nug*j2|`VE0OmqJAC z#~%h#bJ!z$5z_Cjue9V;? zaXZR(+!Mzx6dyq%+`$T`^ojwg#OsLkHymlv9~1Av4|gXqixqFSuxQxzh8OJy)l2eg z=+qpYbkg3FqJZl7Ncs)A7rT~$%k0~u0h(*ugq1N5{mVWt_MT54OR zGKDHtM*YS3m+K~j)AY@IHITfGr{%{48Egc_&>J`KJXf(ZWeP)R09 zAjYGm6RIZ8O7JHDNr*ed@0ScaVu1< z4c(4YBZ+OD%kVXzd%6&IIfa97o zf&A(8BdyW&VQa*lj2IHzpt|W1eole}H|+7W2mY81OTHSc#hGA&%$O`J+1*W?R)6S1 zNp=ugI>O`pMOylmh;$db>y)E9YGDs3Oa+L{b%rZA$9;1&HK!1&PD+5Aq)_jCnX+!W z+&{kHAC`g*P;X!y1%+O=fSa*=u9q3~nUn7x2a??d~?mlZ5Sy zW&su5XgQ7q_xdb5@{>s6@(jrl95`v?apGKWP63D+fS_B#OT4^i5pQ}%uT>W`n{JS zgO~<(VKIq0f~#g3Vws`zcMu75nlCXDx#ti{tNFI%-zBE1U8i6O;IUBQYf^5fgruND zW7WJ9N7zNcBur~kAaW^QVS?RU*YHTDpIkzs=Ag*?X#-^*b9jJ>tI-P>z7`!5tNNNbq9ox5n5V#%_ z)#~i193WhJk&-x;{n`Rt*jaQPbebk(EOsMm6*mt-g{hh!SPoAfBgo$J#(j+G%)P75 z7&kdFc43er0~@iP44B01;ak}JsGcN7ANd|%jS!y3aEupU5y?c~o6tT7(h%5@y12kb zTE|E;X7YT^4-+LOaiPSJU)R4sRbnF)E5CsU`lM8vAnY76nVcPv`*dZ(sh?j&SW&onS@gRAD#-ZC$y2`Ex z0cDRpHW--_9Rw=`80qi%aPf-}kR0ImY92B#mM>8ZL%Jpm$h^YnykerIM`7(K;4Fje~&iA3E)gHlW%(IlTy7Mp5+2=Vevzy2iYJB*9S_)2g_EtJtZP; z{+SC$BzM6e!yjt$2%VuM5N3(~OdIKvxC#uK`3j(_#UdZLkw*;dU0g1eh-$1ZFkTi) zR3rAXr)tryuPkJfv!&%Qe5Z}lwh|_*r%H9G=--AW`EJ5@vw-@|dYLRPfH_!4F-a8i zG$Sp^v2Pvldqx8_bBhEeY6#TC+sr$DcH1l&;#4{;4(Z3xTV|iFNf${0Z7S&x#+PbU>2VuqP@jMJxuxc*{91_fn6-? zHNE8x=piSwI78dGgDjG=@L;U4iw(9HMM7-sw4cCY_As3^6!B3+s{3?LB}*4IjrL5q zPXi@R3Ft!oX~jhSLr$;GSm{Sh!YX~|m3=~!ILN>*Fjx2o$>;Rm) zb*SoBm1v)wB28=Ojz!9xCwOf6(xBY=Z!(HMQ8~I znSJse7aWo#_T;wSwZzs707h7v12`pt5_mPm)+P>!3aD>iqvdoJ09=F$`e_X2ScDA4 zI$cE9L~;&{d5zy1y7s{HoA6qmV+hQ_e{AH+13_q4qAgIt1%(q!g_OA%8&EFE24`^; z2OB}&w>lmNl&3{tOEVk;JdF-kFv5h)7;QnhVrMJl30iZ(wD~el@+H}!bl?W6468y8iM8MA?9!&TPd(P_Hi?$R4v`o2!D@C?ir#Gn{Pe#lL&{$e2u!QfuGs-E#<9 z;tc7plMycbcOEa`ZDvpl6bas$h2pyGuQ;8sAt1Bc@t;Fo4`pbF@k@k6oHyo3!13~6 zrkE(p;njA=k7 zh|ePrN@ru*y-YvA8plGsOmw=?Iu`Xa_k#{)lgvuEn3F8hH%&CDWx1n>Uzf1hpwO|G z2hIeLE1mbHWnG?$s2M8&)+sCjh>VcW z;0p+DqaeJ)4m^{UwQdlPI8noKM|xfq7|Xy$J||r|7?LZoF8%!*=tdEYEg_B;f~;G& zY>kb4_6-AM=?)Y14%CY(cx;zh*3=JKZd*a`| zu;|H9=RV*B;xs4u_!+SV-5WKpek@fQdOH*{vnFb@CQi+aY#40i$U%m|4yK|u+~fUF z7-%Ostu%9t1X03Wqwl1LKgKT>LztMVn3LH6W2_I{8m25sdqJ!z4i}MzkIp^2aA3GB z5%)Q^bqiBZv)Y(e)FzEkLw|S`+HUs=9~v1uWyBtJ^pep59HC*fUQ9XS^T2jK<$j&o zk>Hf1*#{4SOb8c31lc(I_+!wH2fzUS2hbdjCLtZa#;RZUxUqkT9HmyVp%Nki+W4(7 zzwjc4`I2wjVWwKmbPN-1>ORKG{%EV5h{7C;NA30+vShbPSTw|gVF!kIb13$sP#s){ z-Md8H$vvh^B2{h4z=`59xh4|_iW(D9ldOi6(gACX2VYGHxe}71iCu}LA6(vf?(CWf zb3fQ{5jqaJhf5k{!Gw|3262Ri5MZIP1fnwud8XeVppIBTjB1J=%4A=UP2%xNgDK`AW!5z72wXq3sEu- z;i4eiDGVZq$uSM@tmN*@&QWnjxXxQsJP00eK^zxR%uQXm7~d?WQk$UiV&6T5%2kxP zrR^c<9u`63$l67ya$}8)kc3y*E^6owhI_;xna8%Lk8(CL5x8^~jz5f1xF2e<*?LD;3q$k>*ItQM|8Mi5k! z2vu0gU&O*B47Cs^4Ejw(VP#}K5f^ZP#C=|mrWC}0#~8(($++;XE#!6iY-#JF>Q*x2DIos&^Yc2RVxpOoja%MHEDpprw; zo@;Xh zu+HDn>w!bbowiYJSlo{yLJ&d?tJq}?9kv!aj0cF(I88pFd+F5%R-NpH4A`_6tAnYW z)b?_s>Se5c#%w+pP~-_WIH}W{JjDL9^UV%(p*gTWAJU&>0_^qeN-+>T?qFNSc$f$z zTl2z*$E#|C+N%kOsTi2qQ!X~gG$p1=MeABj9?Gom8cm88jm}DE+Tuw1D4XLpI7i2m zbR`_A6f@7E#^OY|dI9^?bhAAMqyQZn7QyEeMJ@ohLQBv)uuCPeRhTV;)B>Cel108j zFpp_SIbvETlM%peQ6m_(Q6ZdynLy-SJQ%H^AD%&|5Mg!p+tUhnaYwv<2k@u1VD8cv zvY+a4@rBz^JYOVL$?zdL8ns(RA99(8-~B0tcIT3KjI;5Y0CleZVJJ`Z zMG;GzG9>1vVxN*Apjo)WaWPC=^ovp_6o2$dOy5J-Hd|GH)6GCD+w`yZWKvXF5K%^= zSp6~5u3~k-S`u=^aGS9$5Zi)%e}(O*I9sRU)kUz6&deeZd+Y)BoAcNUp13!E_DF3y zb1XF}gW2lN3p0p@2X6T}@yrC1tGgpTsx7+78!|GBEyM&FfC7%hYh-)?#hIv`Yil#B zMA;ZX-4K|f*FFsF0iy1l%L?}%%Lu_CuP!KJ0ndkrxUr0e*Z@zBg+I=Wut~Ym8N`U- z+;V}}2wN_-u#2mr_SKJpl`h;idPmK0Cojm>&|wF_=ix)x6*Q7&_$HyweZuSKrU?7@!tO1M**nx;3P&$UygiN}iE)X{oZ^`FuK>s+%#UHk1cr8SP zA(2c=jK>sUpWOi--ZT{KQ+&|8W3x_{1zScdXB==QtPA%NDe+D?Ug*n>`AF@tb0SI! ziKKtvyqNd&y=rWO;=f%IbMWX*4H?~iFw=DV5(kNhJ>1IDbuale0AyRj-5>adq1dXb z26<2e0!sjnd&CE~w~!OOwppoo%_8g-lw@885nW)*T-wImm;jsMw8f)47_B0V5w7Pb zf8R-aqV}K<`BXI=z?5u z-TXRTh6Uqha~uj-0)7{T#EL>c9!VOE{SuT!rE_nCQiDJlZ;afdr%FP+Nxh3gK}tPl z1u5tz`+q>os7rtSE=m|=uA}^>MdM*&mgP3UT<Pu3s$~#cDUtoD;?Qjavtz2<5ZxB9H7@AJJLAE+Q`d%dbUUtU^E8MJ z_#@JHi9i#^>A6Q@@Mh?i3DwCO`;uU~5VYm{I~&y+A}ObNWLmlsqqB(>9bhyMgDOprH`SU>w%M zHctjs5IRg6CRotBFLRApX%YjKvfqxe$A(5(LN^d}y6b7x(M}DKOrUM>7M$V3;7)4b z>IlEttYP@{FcIm@Rz0bUOTD7gVzL`!tzOl2-)ozU%(N<%LQoJilr*0lB-+T=nuC+Y z49Z6E!gZ@)V)zZ-p?X1n8%7y_-bWODtK*n^kKfq~*hwf1SV-O<5n-_O{WF}bxFL`S z8|gxTPp#EUV+{x3ceQFnx`&NV^VcYC8@U5mDOf)^uwZ`XC=9b+oc= zqg91e^0F*NxW%fZZ*yr&qc!^XgWkz{x@uI)+e(7Svd~;+nw!;`(T5K|+|f z5eGbm&l-gU6jqC<=3nS?ct>1_a1@qHk_|YMNhD$~wPm;yY}fh*@)(5bbn5~@)2lCS zq)mmjO6n|;3}388N+-fx*Q%lB+$_VPHAW!43Uel=vFC$Mm)z|t5PxLu@U`4EU8 zIu%|{-RxxDdnJDtw=vx1nr{vbD})8~wR#VaGKHn9FoHyj>aL29&Dc!CBcalLx?!Cp z2=4f`uS}H;7c%>?9jPNP_kh?0%^2~A9d(Gv-)I&xNLHN43mX$+p}MfgBz3cVXmIse zXnKGU0VrWeI3{qI%}QDvc2J9AYPqW68of~2T3}$@5ob{rp(+C?lP-BO^kA?V;mOcR z#Ds&Ibl9j4XlVqC95y_pOBQYjG9M=IUK*r9{Ur9!S>v?*&e=spcXA|~RrqO~<#$qq zFWnR7K5EXv8=7|Z_DOH@_EktGoz zP#wyO{ewN;@d7#N@atg1{jA#QCzR(xU$)(=8=(8t{`L2$vTp0%D_ymdtNnCn_C1^$ z3KgOm;tDnerFOX0Kj^kllfSAYuz^o0ah@+4up~>xMw6H&_Cw>M$pF8!*GJvW2^aE) zyVcPI?zy@F+2uuXAar%B|{|H z;0%V@>lW#9XV*qgyW)HFqpcDGbA8OA^CCK@Y(}gHB8puAnnwuKzQUM{J~yKHWTQYR z1|#j+LLHF+v?j;WX?j<`#er190-xj7@t$^tdvxm*WoO00zCEwANJs9QyzUU4yNB7f zG}=qt$YTQtl%VaAR89L-%r>A|qdwZHmrnm91mhE>c6Gb`Jn54k76zMI75pXpR+(mwIu7wv29z*Kv2O z>UW)vF?%UeMVQpjh$$`*AS1424Lh1gd14SkBB={<`Ey@JPd?eqe_;;egzB!KEETi6 ztK}s9B^SNcwfFl4RALxJou&lCe0(wZd~h)MbHt7=HQRSrrwRd5;U5nRNOVfC)0X4W zAyIexx6n5ymr(EiR(V>0WVKKI9+$-pBh5_X&Bt9yK5RH3EX7fT8wX(aj$!{r=j=$$a^U;{e9<&&O?d%d_1fsb2&tx}j_>Z^%)FpC<(IJwq%z`yBH+?-7`hj4 z25lBTQ?G>N@BZxUrS`_JQwLxm>?xOoE@<9%Y4+{g*j z`hei1AuaP~@h~rmEFQ9MV|YO^at-ho$kL-c+r7PLST4njPA22-8f0}p?ooCT8)M09 zK=geN1c7T|;l$U|hC7#lx^vlOZ-A~1Q~mttQA7mOtFF_PD5mZm$rtv;lo>^G46#>9 z;@vjQM1ctDo96omU0hY01NpH)JLUqz`hE9K%jFH`K~3OdPE5gZF(dKhEU&ZVn54cJ z*@#CWi9Imp?!Fxtpf@=VP%Eh~|1-ieW)~PES-sU01qq1(Qz_~4Rd?x?+ZB$o)P|{* zidC&NcC>Lkh0t}Okcm@E+bUz*QTiPvvGU=9NQ<-NrYl{fgCll-?U}g7A_<)9{tbSj zxD3nDdkzTG%ORZ1q?bp(`Vl^Vb$Vl*un(s6t`ehB@=c&A&;r`0t%NUvaQK^V=@cLZ z%Jdq>mth$k#G;9rV5XPjeA^*qS@V)SE&AyOJ|cqWcE*ThObGTD5^0M`5$sIT!trE* zKX?tP1tqS-v%+i0W4sgP@r-@PjlV_$D&l*Lf^*EbKQT9h+JD>-6^Z4-pK=JC#7kC8 z9((Sxi>i>N80)Cvu{Bt8qVbs@#@%TOEG8@_g=yQ|Vj#>RR5xB*NFGNfR0K@#9v#Bf{ z5ki`Do#e&mM<8@`z?s05tM|A#9w3jOg#fO7!86bO)L|hxgMYvP1=H}}Jcj_#$Jc1x zm4>ssFI22hF~$vhy-Am1lLVS-?MKd0dO=}2(-o4tmA>PY#t@NRR)dC+K9D5+G}C0U+%!wIVTvlc1U&n) z8x%qMP)ZgOP^V%rr(eCQ-A$qbj#W4!`2u;h9>JJ$AK>VM3QA{sUjrV7Eg^wIlH>$} z!L^(aqrUC{;#m@L`Mx~_z1SlgcBN&PZ52NU%fV42*oqtLvUpMiWZSBN3A7mXV$oF3 zUJ=fUf_=Y`&v8w!SI;)sO%w264(Jxr?8h@-v2OappY@C@MW`DW#t3WK=u0nBtB%&dsDISt*r^OaD{Kb_~e#{#ZOaM$?pUHhEiM7=a&Y_wBArVVbAMkmJ zd=KSTSA?>5+!Z>8Z$l^kxJ{g77Y~?5<7L?)IDqA9AsC%577hGj^+4HdtaIDIP-#vj z&NEts4&Y;jSE7k=d(;9sZ7&=Ni|#IK7qA$?AUcj#iE3CSYaB?&IQY9wCD5rBY(pzu zIEW4<{;OQEsuudNL0riFu9pq;(RibI&4MRuNon;!&XCd$CF1-sQ}`l#Q4z8gT|R>SV+0kN&1m z%QlfWL#|GMKl&SwSE0y=oC~p+?2DMVOp%b#=1}8is_a;}Ue<)&pq+u)X6a#B-`He} zMtb1mari3JKA`nmyo5)P1y%zJtS^aMv`h3_A7-U7LI`Q-C6$@N!}C31!A&3u5uw{0 z4#ODRWuOck5h(D9(^WKuhnjq7Bi3dNV4@(_!}HR?>W9qs{*B=-vsGNSY{YAp$nmtV zdXQFe@+19eSaLQR(mTXPx&UqW@-Rrw_BF>A7VTr)V$ju@SkGB4-(3o_Me3{pJHXr- zDPYH+#gWmJBNzoDMx*#oK=FNaKvP|QE0YX&wUKX zptS6SU2&Jq*5cL}1F57qQUWpl2%?P}J;6>J^deI)>c4mV6eTHwf^i2M9Nc9Llf%P| zMZ0ydZ4VgeyMXJ#ye9E*yBlc;Ht8givLx9UMlAf})vTpx@5}Bv&F;g6BT<%`NlAFj zEG^5iFx3PWlSUzmUwU!6Qk77Ie6i4WGFisrsY#=a%{`Uy$P!z#-mJ}B8P!70rIG~3 ztQuiM6Xey)S(x^@cw#b{v?57X%0==cU(A9=f`NpF+GOlw3GGlAuLrag4^-wcSB|=c zlN#blf5MFV!b93d{|(=N zXLVR zXE=<`nEWliEF`6Ym1((H#OPhv^<^Ug@FokfYM~@?683_J&enWk8H^QPDXu8xg`DWI zGkDx5Z-I;fibJ7>OUd)Y;KjyGn}E@R0ot!}>@S4)SZeNCrs=|J`>>}IGsbRu{)l3*SMh^Dv z#5D8?0L%Mjx*olhyI{pGNUt*cU;s|l7>r(m9%yHqc=XYsekXA#*Y5!p0R&U02~Xsc z-E!8jEU$+kxsfnKlf}jUKxl#lp>G-15QVxt?UVR#kKLv?o`bv$JLWh%S|C74+yFpN zWFYDb#HdPidyD{3P))Xn0Adaj^7Ca!@t2et4%h?{!g8ju5?Rt^0S#bOfGrz0GMCVS zz)qHIbeNn#s2h)L#VmMwP(IoIPhDVWH`hFbm|r!O!NQ8O=Df8~j}3rQ4;dEj+CPJP zvd&KhUX|TY4u9kmV?AmlN6j8KLjv6zcnAs4GFQb3ib=IWi%-=e#FM3$C~spsL6dK8 z7&A4b(LZXfSBzLlC&W8}Se{_q6-nKRpc90k)~epAXWN{yvz{cqi1-0?2o=6zn=~P8 zinCkN4|0d}TKsnSG^VOmFLBqIzjRzpKYK}hYq}ElZRuWutyy9Y*Mi+}c4!VMdR7T7 zLdZg38sz$DUImL}z=X2MNCGoV!h7d&v)4I(Lg|o35D!ie74vpa+Hq#x#*iMfI0i5; z@VQ;XZ%eWx<$w6B_E&ahn@Q`&h@U4)`!7+E1 z);Kxd&R_sqjmStPKo7OY2mBQF($aIsHQ&Zjs9-U{5&$z35o*u*dU2hcCL)P7NWx92 z3oIw$v3F=!rD>6fsKS+~KoIlLP1}{%p0!0YN`3^|Mg5?hVA23#1DgllwYf+9mNBH; zVhFUnB1RN(r4S2?OFk8$4Nlu)67FNvaAh|b5U0f*MB3_)Jv!Ikv% z#JW~dkbC`XdQ$ikd(=ap1mI#)S7Y|4^SbxUG)iic8;G&AA4;;MRx0%ex2>|3L7u1{ z4r>tsUKW2ujsis8I*Zk>#DZ-KN1+q!FV7B|;8atlMg#apnZs{sX$27#D15^M%4y!{ zp#Vl)SR1lZynV5;lu@RlW(t`=@nsx)1*<}Y%17PUEH!2+q7OY&_#>3O*as@sxNUdh zmSPnt#Y=B3z;n|9tS~|N#EO}pY1ZF}!{3E)$kZv9O%U?x95JUAzY&q@mzaa=$-tZiqR}W3h?D9idZNft*v`EV?&1q1Yk3z2*F?r+&2sM zCw#!ukRfUt>6H~A94tg%>H>Q%ITjTZs3fI=1j-JFWcH5LCL4|qb7+O49>i0c(k|&n zd=?U}Wj1>lns_D4W*O1dSg{d zu|5N-6^t0&h0{!?01hE4f+ix)20@)U!}KUJ0KY>xw5AzW>tRJUYi#FnjQ%i!1lYih zCml&n{~|O&|I+=d6bCC-flf5RV4M~mlMOe)`GOf{DQ?jU-jc>24rh2Qb(7gZKvMi_ z2AH3vD2`f@+CTxFb7h%{!W+yetPH@`p)H^x-k3cN1@FYv(rF0j&LB7!#4y$XWZi}cdd{fXb(CRtq(7Zt^32A09bA{kUKT`VunGXo?h5Fi9wtIjfgco^kd2XK z2O3Ez1*?7Guj(6kG0RI*$UtB1j97?lP97(^5G38cvRLVe17Mbxjzue6(n1(k%E-qN z_Z*`&0z@zg>pnT#Ct~yRN>&=_U1{XS{_y@*($UQMgqYOBv0>nag&^B27DyjCouFCx z06<*d9n{bV7l<*4ye^34Iw9gw%8`XE4tKuEgl*0av5~*eh(cd`^TKt$4d_Dj%gJa2l`kJpcEY-x_5*pSZLRl?Ga>Y z{SR$$#4%yg;&KBkiH$PQ4|h)o81zW$gsdJwb*^)*wF)y&Q<-dPZOzy@MUyKev($Mf zLQ+U3s7%arZI2Z5R0?xpo~zVaMVN~)mx&p7x}tcmdMK)ghY{s)4c^q})GF+3Cf+ND zxXsOnT{j&aV29orXEjh$61r+b`yiD~g`Q9Nz2TJ(G_e_g!B7y{gseK}?rISyzQ$fV zHUi+~FHSgF>gocwCAv;!?gO#i%$1Pl`yK^@SQ*9GsB1n{*{3Fm<*6v{7D}mE?_RKR zQL4Uy5$YdoFJ&|{UYNFa^s6wz{T58cH!pUOz--JjQOaDBkpl%_5GiWJ&le3X&Boe- zgyg)T4nZVABNqSMAw=hzMIxogogWY%I_eu_5yMNPMqk{A0S@UpTzlbU6%pdtX|vK^D{_Lus_BSr1*r0ddSJAC zg2&cZv!Q(k-5dURk`m|s5oYItkS9d6QXIq}(d@{ou!Wu6WSTmHZPx;z5gue<4RTpa z@3-tV)^s{!>W#HH6YM`R6ORfxmr6ElfbpoT0LK<*w0I*+E>H%>)xX7NgPiaSKv{edd-t1i|6ae#AQID%W7xCfLr=jUjZ z4{37tWYn>dh5U(Lu~^sMxRB)@^-YX&w#31-FsR+C&C+|MYL!mXb_W&!coELrszXxH zL4+oD)+oJD4Hif!C}_slnLsi`O0Or<5i-n;I&b1hiME?C_%lTPG+x}{?-xsHVppO= zS|EZnvPyWFT@im7AZfNfVz%PE>dO~qV27=13_;JRWou#A+Yr0mOm!|^;EKpZWtyg` z*lH)5p?DWU6tBV3jRB~$K$j)mfbt!{njT@7VIn=jQZI8}I-TFh(gYkK)82H)+};2I z_~Mxrnzi|jO`@wgF&PX3nLaSVM;^!~B6~wd+%2F^mn7`zqqxJf65Z4+7J2%hYO9oL z!dO7d#6WX$@4||eeeIAII8{YC3RHc4jBW){MA3Q5m?G#HxNg*>047SLGZKhxL}*nO z$VjZxht{(Y6zl>=%nU53WU;XmY_p^S`(hw)^w0&Qi7h9=)i&mCHy4)kM{8!@32jcd zo83H36BgwQ>CQUmU#QOWuGV_y=wRY*-%K34T|L=sYOM$B)tRG%c5<`bZRdG4O~fQp z3|N|TC>tsMHd?i65~!Zn<6%dn1v(BA8XC!0{JcnlySnro#>E9O>Yfo+@wHyfQ4Ge39_oMRS8TP))%nsNy# zwY5=O)&*3rB1BTy-|9+4#KlW+-1cYru>EFoTLKh&+#zUia@mkhSfnzy*OKW1ZRMid zXbpgwIOZ3HB{qdpX`qlJkr|n8fN2*h7`DHxDen?*o#^OQV~#Kh3stGRTBoTLF0&4T z(Ev2XRER^?hIqsQ)fCTHFeFLZ+KjA`ZF-5Yz^q^_r5Q}EyIqw^G;t|Y^DBIQxmr6e5wi%sUZ%7n%^4X`1J$RttUR$Smz0Fm*+Bt~3*^ zr^#k-n*XcS6!LjH5tTyBElm;C<~;9e6{3UHba*`3+vsRpb^FY$&u)7HJ^0KPGMa2)i9LM8Oi8q4d`f<^%fz(c#x2hhCmt~WN5vw{hG;FH?u z)}+@ERHbITdu>n@CYhjn0bS`DT<~n15tU-b>=L%WOrF8?Vhx?Pd1Ta)H3#C@i{?+b z32qU{``^MgIcXhrU|(=APNJCNYJbI-6G&wFQC6XapRF|MQ3PHtdu=#slZ0OMucS)JONW7>FEJA~ZM_FQLOU>qjB%ayUNZ~UtMAKU(y zZs`R7L8L|0ZP9aU3U6;<(y1Xh8%+#n5$U@IkbA9^Phg zS|)ZpF;OBTK5qQ3P%0!)YOBe0W~b{Z)9Z~KAyAVa#E6|y3F$N3XjPYM&#nut_w{ml zGZVz5K^wFn&Xi9KJhlH6hs9!+-Dg#KfE1F9ynU!w_n?ftK4MSDLJnZvnh7|f$G_SJ z5`%W4S7yXvmviENKwNh7!^0LSg67VT6B;D%%i=KuUc}}_K?n{L7Q?+PtjLE{;3ap2 z5L*1&M2}Tbw^!|pbPXtW^%=g$!1k+I1lcz*u_wX}M;M(@CUH36on^g=#e2b;5XSIs zBvBxi7cg@*nJ^h9=L{DBmExr?hxN>Q82(DV6sq=jH zXidZqT{yY(o%g)&z6U?>=#x)fx>C3EdcD5!hV#$5?fU0D>$c~A=rhipIclP~^X`W) zpPbxy-MME!?fQc?ot|!}OoeKVvRrQs1UeKjE#o6i6H=8vi?Elw`=;U+f`YcGwbt3D zwt#A7nu_fbN2iChDn~+ANaCzn(sHIh(j0EaIW>|dU?H-pRjXu+S%du{vQ-@6s9vG7NF}Lt zVwwtVw>ua9G!-VOczJKWpd8W9w;ixUtR%LGh^u{tS5$N%lgPG$OiWZ|H}8nJ6fVqz zxtaW!Sg4BZb~6|S6)vmQWG3K1Qmii2?^>u3#u4CE4fEW}cBQZ{pfE8{N;Nv9v~9C< zE0nO1RH0IMnkHY#@wj)D^dd`%s?@WGD-gA*WxNuNA(L5_p{w3QG=ZF=r(~0T zyh9=q>6y)(0$CTrl28yPV7?jQ^@bGR0kj1Zn1@@(_WPrV)SiS*23ALaxX4o0V z@f#QIYu>JDY}tP!)UpOGsZ6QF>^q%;Zr`6pIg)`)$kukmoAhdoX<4nd*j=%p2W$B_ z5d%dwq%Lf~+7Q*mgmoBlkVK2+#MFNHfmzxkVa^_@Sg49rVYR7FxoGQ>mJr(6!rqF* zoWV0($))I)#bj^x;-%Kq7GFV_d+oD5y|MtQQ2R0qtRkR!7FkVn&wY6jW0sO~j?;)BqOwS^W(L5)y%^MLKkd;8rr8wL30)bn z#Zz!2u)yvU+eD0V2PyfTmee|<0Up^82m7P)x&7~9}rYdvgI-fm0dhDs~FTCt^ zzx}(fd;6Q-ao+7ryX$pZe(^|Fm1Lf90#+_VVAo z<8)WAzxMFOFaC%x{o>F3s1LjKbi30?t?;b##|0M*#SQom&_G~mqj-81B$*PzL18*t zb?c^aoa|~1UHfM2-cu^osLIXrL{ckH(^MuZF#B)upvpY&>UvdH>lK1t)#dNnS!@-q z7EP$qYAQ$TB4kSoLYrMB(fxjy|7%A^$R?PCWunsFAl9}%p6=$I z)Y`Dz#85q!r3DUv={7~Lva4EhZ0(A@%FY2fv?ElQr&Vd&$7+?`Jdk<6?4B3)6ZkhpT??>Rf3DvuO>E?Jbc$@k;n@rp)S% z%v#AiKGa&wcC$I%R-tAxQya39U|K4bjt{35O(8m>ndtIKd!U_@lY#?99h^lAe@5SK zySwJh!MpB$_|eBNz4&84WXJV%w_6p>)LT)#2ocqJx2tol)V6C1Ss?Db6hn>ZBOUBN z7TJL|i*R9@ra~*43NbSYGZig9Q6bt^dexiXBT_&6MbAATx^m@Yv!!X8xJ*J)tMI&= zLu^`$z1#jirVVh(2{eVZqjiSKyV126rxS05Z6Y`1e2;yS(f6F%ZEfAWVv zd3noIge4{_B86#MP2cw)e(UBN&VSKg`-B_MAD*0UPBxp>#M5e38X;{#rU;8A!(t;c z5tTCWlTV$zm%sOW{@zD? z=xy6w<-)50!S)@}ejW`VLjAN-rS<|Znus<#-2-T(v(p*oksga6R~uLmQ%S#)VchPm z%@8H;SYiO;SbEQ#F>}>}ah{m=NA;cH6$Kuk%&T~9Yk>r`#`XfSv+arGC=OwO@Wnak zc&NHLOv1(#n`aE0Nrdnb7HeKPfxlhv7=x>@O}_|8#%E2Ps3XmdHXY`)i$%p`b1;jT zn!xzNy=8U2H8R1b?@PDJfxnwihYm+Hegw#H(qr3%qtR-@i(0*%1o(K=ilSLYdkhP zx~QX5;0USe9p7IB8D`-l6DgG*W0B!ZYezY}naR#9w}XEcRAf`*P^N}(7X(bleP29C zOo+Vd)th%vE=2tj{lJ%1{p_bSi&8#+XYS`nG;8&I<+uT1k!9xtzl%q!MJ5zKB4w1!jln_Di#!8n6m(pG} z?Ro+GLAgj`BleM?cSjhAH~1x3U1SteA2!ryrg^Tl!aAK%v7KcWqJ!1MEaVT9i;ZhF zBOxKGySdgNt62OhMAJ&QMC71K%teF>OKI1)60%l=BMea!ld z1NW2BPzHl$*~66(vvyNQxZEkC!)9AdCxok?ER}%G3($em1la<$3FM=r{5$soJ**HV=%#EXIdty z;*as3oNh=&^Z_Z|5K{`T%DQbPe(Fkn|9ubMci&?l^6Z;G^jSCgQ^G=3YMGhDHUpKY z9`Nav-GdiSh{U!pt*$wDaCk6jos7A;`gRs(SD(dFX_ehhh^gAU56H@e zFtp0L1w5g;Dl|<~v)n?&OoeG;vImy&FvS{%tV&r86)2OEv=O9|*op@;?<#F}GwIKk zP+Q4mY1PfTT9wny{La!SUo> zOK+Dmuqe^9{}H0Lsf*o5$$-fwRKn|3`Qe}X{YM{t{L^0ag6BW)8Mobdt`&-_hzQlT zJ(9WD zE4%BiJ$w7pZz}7xmcW>}D9hqg#^kg_RGXb|x4Uc49zXE#Q~%=s{I75MlRLikpZqUh z^>;qKuxvK-YBiB9;4slt_~_tp(LDZtIj)@Uxb3_akt#D0lQ45xum0dQZ~xlA{{sid z*M8;S`Lw_OS6}q8&%gbc=&8%6bCqeey5@M|{ulrLyvKH~ZLlgW%2cV$Wjb1|-h9V< z|HVK0zUhW@^KL^Yl}_pFzU}Y*wNL+u$9KCaf(bKGVLpGn`mvw?!|(XE?>m3v(_i@q zZ~gKw{fy7}l;>Y}NT-|aJkM>Fu06k5s6B_R*^yMLwQ{RiP1C#X{=nb;d*5~MLzka? z;u4*lQaP9oj^2C!Bky|m1E2U8pY3HD(IxffXN?ueKsi1ViPSnb!zNize6%h{>w{7X z5nb8JyWacAyY9a4ZFju;&bQrp=R5xN9q+pD+4ftS~83t4LE-ts>I?BdN7gdvehW{FN#RahX=!=owo9Ag zLB;VH!W6me_Z^FMIhP4Q%Gh;x*WP54Bg1NL7PBhPJSp?Jk=Izf$Yke+2h40l)_&}{*;53M5sL8KaLs!s$=!;oyb;hLYAe)ky3jn9pb*qrK!lYy&hr8?3M|&aVJW9YF*~ML~WKq}ey2hK>R> zkKGR}xC2EKW?TBx%RXZ;(lf20w0@gj>+jF!=wQ~7E(An1YQ$EA^R^$GhXIf6U8p$I z1(|{2hYX0s7WbM(g3$EGN>~JE?ePRu6C#K3jHLbBk&KLxPSTC}Ls*H%OD9@~mD;HgOv z7}ymG+ElW4Mx+-NkECEYwd;5q^C?cLnx4p;l2SlUF$MlWF`#!TB- z2y3rGa(FOZW!81Ft59qj$=hSj7Ef>wco!MkN}7ZThG@NR&(zBFEF?71Jo9?BzO<3= z|Dj)e`LF!W`KRA@biR~&kz}>GGQZ)KfAH^q@Yauh{y)9u?7`(-T^BA=OKr0pIXrv( z^4GufTmIn>zU9?#n65uQzU7?APGnc-xyszCki0JIGe@h{wJg)=QYI!L`@SyDgD$sdc6UrXTy^Z+q4=Z{F_aqAyWuzi8;?B(@Da z!W#VR(c${{UVZ0V-}0Wn{kJ~tnYW$aY_{87ZS@^4T!>fea`CClFMIhL@3`as`|o@7 zJ@3B%-S52X^Z(9Yf5|`l(&NK*n?RC~*5-&xtwm~kll*R$GslOodi6Vh>_7e7m6Lfo zTr*4E&R_G@U-VZ$>xCqNRh8=a2sAOD|m5tXC_d z`sCw}e9hPVtuOzQ&u%{ivYY3FGX3nYy!xO2qaQAZ2Os@{XFvbB&%EX4YyQ%wecb1K z&c~lUKG^IkO{E&ljs`CNJD#;pg*Kbr>FJiGl&P@&RvxpyF~{OvtMt{~PF7R7_Uu9P z|IKFi@P*U109C0<+d3%i4ftwCrb`m3n_WG7_UPJc&djZ9*Ph>%TB;Ngsq^8{(fjUu z^t=A`Pu%l{*Z9)#AV*q%U5tDHG|bkBp2f7kc? zm%HC`2VHa9cYN#HUix#-`}{BbjL-YLPyP!Ze*3P8Mg{ZF1t#7@Sh~_Jo z`Q#!^6VEeEbzTcorCL~cBH}Ygt2f+n&p-bs-_I-;FI@QXANhq>zTyp^{{^4^d7uAD zpY@{W5z)m8A6Qui4BbvOam} z!C!pYAHV1?Jd22{u1XWNU9@>7W2u-%|F%(Jbx_W%Ca<_Zc_Qz<=ka^*d+?p_x$llY zz30w1-}z^E-u1xy@1yM#yyi3KuetW=x12t9^#A_kZ+_wDecE%MeKQf=@$UP-=OzDb zd*KpYJJHET)m=pqzLkz!l1-JUwuji=1DbIV5v?ZLU82?5PkzydfBf@rt8<+un^LsJ z1#t4b#Zs8Ky`F(mx?&f_e)&X*aG#`$CfYA#FT*LEeM=-#-H{hN#1B0W+MGT z+OA>b$(jFX;9Gh~lE~Roi>Qz=%*h2rJf!?FMowT*Q5>^WBC^-@dpSbL?@>L4p+| zbFt_Tdmj2^@*gFFfQ^zf!^*j|7xUSCE{&x@qP+nW24>eWr3Yst=MdMQn~K$G%n04F z6UYJ2ZVH&{;|mS_nmY|sy2WW0u^EWC=`b-B=Jr!zjMY@^R#hlO`mAPDNI{A|ch@&z zFjS4I5czgF>y2F-o=0XhHg{}e>Q-b2XcZ{lA|X@CKg2<%Czm~^!C<3We{@) z1bML1kfFN-GA6)@x*b`p7Jc#CfYPBsQ(b1OIO9aq#8T`1_dfRIlb6;<2Xn1eWG40% zx~Z&gyy4oj=a09$+J56)2dNEynWauFQtSO6xOm~oEBZ2EYHRkDNDkHqPrL2<<1>f) zwjYb4?JXVE$P<=_9=-JVlUKBa+)S>ygw(>*O}E@|bhMu1p`c(^EF7V3qed?69oev# zhe73D$QL4_d6rt~@Nm68d+Yj^>xt)yYvC$Xcy)Z4>+$((h}sNGmy-O)I;wWV0phT* z4^9R_kSI{x^rPGJT8mkZ4p;C0z~hfUdAThmleBj#3lmK&b+b7>I)2*iH?I!X^G-7S zCF_Wz^Ths-yNmg}{TQfYvtYMFamiv8#+Bry9s?jmqjMQ_WZ5aYcl-60*k!xT_I3!M zZ@-Es&_0pND2*hez6X$n_?8qL)}mF=ijK=IqA(HZuWe9m&*fARF6CYCzW?&2&9t6~ z+Y5Z!Pc2oda{DbeoV)f+ZF>Y-U&c}b1A_snvRaiVp1ktdTRA*h z-F(aS)0$`9qREyod0a`kX#a-6yf=IqA5jV(?wH6*MeSK6W+vt;^C7!}dBa=Z@aA{^jnDYlOQ$DPruG=cyxW~WcjhhcdGMS6 z$q&Bu)vrAN%x7(>&f6=s&h5?dQz@nR6AChKt_ZKnxig2W^=_`aOB)su+D&KAmFsSO z)9c^(hPU4J8K3fkE1SArAqzZkl)vjDO854^E)#LU2XiNiwHDhTCx-qO6KmxnQZHOS zxpLvc=5R&34fBp^H}9rdcv>AwwNyjwF$^pv2~pMgUL`p`Jb2UF-uw5z;U(|9^U;^T z;?4i-Klqz}_0vCwNUof0MWk(rKEvxLpSt{w-}(a&-t`!*x9fE&+tXk9?N@!l^Q)`Q2~5^KX5^i$Ch@(dzW{bTySKB8BzmZ5l{a z5~B4~CYJVCCo;~{g*S_N?f9iMUZ9O=iWZqHLnpqfZ5vC#{WtuKsx_J97*Z_{^*W--t_kCpY_ovS5EGI-$VD__2>&;@R3J_>1glXt66Bf z-JLl+*w^4HhY(%f)W&GrK;TQR!aPm)GvWNhss*X9-oZM;FU(mV?#m_g?#__rL3|M;^cObMJZg&%JcI z?#64M{`9B)*uVddPkiCi=Uo+hd|0GbqPf;Wbk}?Df7@%{M8ve3h}X1z-}N`&KJVrd z(@j=~^apxHn5dAjJsiM9Gx6EugZDqS{m$?HssH+8zqG#QnrokS!^8LA_wC>QLytUp z@!P-oORhOQxOnLb3-9Ln?9uwl%>VZf{n{`7_uoCd`RwlWvdmS8q!x>>LPclltTmRZ zbA-8hu8;`TnOLY)l1fDib3H9|`Q$79x3B!HPx^=}S58)|6){nnYOQAuR~Js^m;B%_ zzWO)*@S0~oW1e@ip5OR~fB42feEl!~>c@TAm;a5g`jXGO@!GRL|EsV5!SDW2y5Tyy z@)XTX%v75N*c_X!7Clwswq~}bi@O(o_9uMN=YHa9ny$Mh8QZqf>88%}JXhMzv(RQ+ z);r<$78egug^CCrAFdvM^5k3p*?)Q9eGeQy<3p%knGVmr_ucRQf$zENrN8;AulU=a z`DI`7*&p?xx6ZrWJXe~x^Q^I6H@Qk}KPN+#Hrw4Zo_6CGe&rYc#1H?%<;`4oo4Tt) z%&X}qUi#br@azBPHD?ZY+fAG23iI4nlNlou73PVG{TX`yTl`EGnPG_&b^G)w~>979L z|MvBtd$=m+&Y!#O*7FY>uMW;l^XZl~tTwYQHa#?Jm6>F=tX~?1qP1A2%je}_eRQxg z7sZ-aGo$W3CHmMD%x;WXPXQyX!#fGuMtt{lnz40Sht1>|l%&B0(#lAT*(RS*?$8NC z8xOLwJ{yD@%hC~9qIYNnLfYr|E~kH%=JgFh4rIEFR1jW+_pXa1dw!LJLEL-B)a#_f-op<=gZ3ac9EeW<)IT_e` z(L{HVgE)s=9Co-fRq>T{m@|NI1-&}xaZ~VpNeq|c$uI$BP|Sp)o7%>+pVd;~!*e%c zR^WEDK#z%^+nD(}rjM*U=B!w3)ht)KX!7W$9Mh+9*KAiC9DF8M(&2XGB30-byTjA# zX|`pSHV&Houdin=6*#+6l%IuSzq*X$q*w{2t0ZNlOOrfRZNkm*FoPg{rHEBNG=7K5 zjI#l%6t(>oqa0G*H(dUdU@G}8ev+{$oTu-0GEYxJ|M4;za=f+*LIAdm_Fwjn= z07)G!20aoV2L#@;=Y@&*$B@w)wvQ8%Y3q49el}Ge!<`W^3QtcWXmh}DX%+8 zt@GLA)dwDW@+IH@v+sEQI}Xkq*X>Dr$bhE0Ii1g+JO2$||3zQ)xi3CFoe$R&5HCcP zg{0Eq;p*a3mtXSle&KC@@{Tf|KpOJYdQ(?g;bcO@% z$_>H4f_0}wns&Yt6V)rHo9*^gE=WJ-xV{w^e9Y+1m^CkNE3s+`~yO z&?h4i599`ELPS)k63e`kgNZI)KK&Q}>c8Ic_PbXHYvNs%>N~ZkN$BMA(ZTU|z2uue z^+nIUI6L;H#K2@Cz98pDWEEoH94bU4?YF}c__73bT{;wj&||Y(HUt1nH@e7#2T%-gqgg}0}y9j;htzI1W3-7uH3 zUat<1SL@@to_4dyl~XO|GO^5Z<6E zy1HOseea|H<_CWIfd`&AIyzXbO5L13=Q&URN8j|t zPrLPcA}VXT=7yUO&To#dDbs5G!0GAX*(3FwT36F__k$Pjz578aM8~Uh$G1&$-8{AX z{4e;_=RWg#BGMUbK15BCPIh&7dO8sa@ot{ih3Vk`L*@uaWc zVS5wT!69uPzv0%~Kl8Ibr4{Dh@veJ+^}qiTP3LGe)6QNMg(_^8&P3CiXidAvcI)-J zaAlc@R{qo9%$0b3u)6QzC;#J*zieGrr=`*%Pv_6we$&lg@V7tb!ACDX{^Z5uqr<8b ziCU{Z5?8C-bp5&W=g!>m&U@c{=Y0pOB2?QCs)!P<&s%Uy8j3pkv_3do9e(78-TI8% zZ{E#wD?%}eR3YXnv|g2a?tAP%{pioH&!3;me0cqh)B22Dc=TgG?jt|xFFt3Q%0%=# zuYJc|?|bqqKJSwbR%M>sdLJFGr`5Szr!%XGFL7C&K1MfPdrVY!+ud$AuU6BU!^2&r zSH0oRU;5=gdgnXd`2#=j4?px7H*Z9WZY-(H#avJ3+8EI1{eSH@UvXtVz47+rsg&Jj zdg_tK-g9x&-c~WsRhX)9wH>c@CL+4-+~N9$TV+M711YP6(|dQOsFNGw!l;>vXvs#Juz zmNKt+p31Hg?aG@BWt`dFh#_J#8mjzVZmK>eCYIE{rshdi4=BDep(!F=y{gVIv&R4wh_22l7 zf9DJT$4^{x<%Q#GzwGmW;y?e^l}o3EXKK^)^}z?;{?51FdH?5p@^g<5k3VP++nMG% z7nVBH(Igj6Wh0UWE==6kWM~8fHe6YIZTrKCMiqzfd=~jejbx_Jw zcUziIzxV(A_s@IYv%cW3d~6{)owwW5OLVZVyIH+&y(+wlxjVB+k@kbOEwidZg=*XI z-IhS=#Fgfg-DYP2KcyiUeWM;=2}#7MRCOVraYUEhA)Y4bKzk3i%nQCkM&9?Ut(Zaz zc3OgV=-!$oVgx5wV*rhBnEykjobpqZ79@0CDKD+f;zTT_8%+L&EgQ+T(1Hv-Y<_^gWSG)t;Ya5GMmWu_JqcKKD z`$ORpFZq6kR!mfg=3PBnmlK&^``SODgX@U5Bqu~$5~4bhRI1zCp7$aD;440hs8W?u znvHX_S|+X{hwF0xBTxRfU-^R&G}Qza%UYuXg5s}Fm@hkw!MzF6#+wpA1D zQ3s(~sW3h8*rosSGcSAc{qLKOsBTVeQ(7Tf(G_~(XMWPnHym$wrz}c)VHPgjKuD3i zTLI)S4h)Dytq6dgE|sM1*=U=dA}9(`C7Ma3?JjBWdz))*uWuI3o2a1VIpE&ct56RT zF}Ag1BC(iO4MAzUtJj=4_~k!%)6e|$ub({r1f4tJc_YnADicjqnAV4M@u3%e_Gi54 zFFfaPRW>{E=NZL4hXNP;Ipub~x=oITBIZ~w(MBll-Ujj`TH_jBw{@!*M)))+N-b2w zD1dKELJ&@5=t_AF&#YeMYnKp47-{Y>mEqTU%;=Rzi0)u2jc{g&DL`GQ+_!FqtUABd zC@gcOiRk8AZ~3Fw{Rz#5rVVk`7b@|Z=FKbbxaaG>?u(!QtQ%^TDwVjDDVVq4($R&_ zDpXibcJ$Lf_p(2I#UIk~0ntw1lvpO(t!O)c{kMG0nWMwg%|=A1NF}z-`psMbM1o*( zP3Y3Lzi(_vIoeJeyOcy{@kDn~ppmWC2dmYZiHRru3D8=3by#R~?)c#Rx${I+xJY5D zWhe9bGe^Ju$M5*%U--S%;hC8Woo;v{nMumJZtMKi<#PV~Ew@}VSGs)V^vabJdg20c zp)+UK=g!N#D>54$g?HP+eCFtwh~`T30`MjlV;QBBz?iRuOGV^JX|d-Hj$f^q0yfMd zr>C2`-Rw38Ja2{PLUpEH*;JWly-7`7cUm$L+piANRNCWN4?cG3TfhB3zV;1&cIKvM z(!9I&nKwQ9{`;KiCz5VY0^&39#V?W}y?W)Z4EW!ut>97BdPkrkj{mG?E^Qv5- ziR9wruXy!aKIs#l&xLloT@hjC^}%#>bky<$5vqmgsSOj&JKmhsQ>pXJGtay2X|0u* z>s)KC^E^|f$1Ys?^Ttw7q!U?azLC zAu6R901LI>T`e?=y!v%_zW(=LcXZ=zn=7YMceU=GI^F)nPyE_X{kQp%M=u;69@>&{ zt@A81)y)Z&?N@x|7yZv)_!LNx8FR3U$suS`;k6c*xA zrn6@cS17#qh2lqM^?d*yC=}+3%!Q~hRpx2pE2q2d$>r(jShz6l zN?GkrFR!mXx4ts{`+xJ34?cA9d;i&2KJQsKJo(hgw4T;dSBFzwm6_{YL@LchJJtuC zZSqCJ#TUj}wB=g4EjQR z+;ZmNgx6EuT$$(7xvURvxT!vM@}GY2$A9T%uRb_FzdgEDPKD@*d77rxv^vz)ZhhHE z5#pJ+%5F1nm}r)1W}><+MDP2+V_)~p|MtogPgarBD_2%){>E?kpI`D_UvqM@rS{r1 zC2iI6-eTCuOzVU4z#}Jbc-wpFx@#^!{P@wiiKZjkZRX8Zh>mW!u3WeK!{2_*Yu|9! zw|w)Le8yk;n0p_(#C1E1|01+VZIxOPsk@yNq6?R2I#|n77wW`BwTKXxvaA2^Pygm) zPrP_@y4mbz-+|OJ1ukVO<#1Kj(`vhuzw+rXy#AVl&FOC5)w+{w&YU^r-2->sb$H!1 z*WP+8jx|?^qdb-<5om&>!GW>es(!8xir#C(O*|*<%Ww(=g z+f+PXx=h=d%8F>E6T0d<8mcB*yDc3amGuz`5zQ=ISmt?Vl>h`8%b3WXJn?5-^*S@h zZ=1wKt=k$@H#n)J5B-^Yr$-VinKgtTzG1o4nxxx^}63 zXQwL2Yft+)I}B0Pc%k^PcoacJfRXtmz^{9zMIuz%?^JRU*`T~(SmP0=IqcW+peu@p zFQX%4vQ5?oewJK^`zF3gwM9DaYzSun;W$Hz%dtIaa1gJQ_N=4w(hbI3;x?4hb~Ib1 z1%K7v0=C~A??kLx=s{aIrxr5YVU&}ExDxy^DVj=Z!%V(>0FKJQoo{Bq8oN`j#X9dY zuNLa9`slE@`+CJmL?@+c76A+m^9F3-jDElhnHku2KI=yeF%rBvWdu)z>J&sJsd6De zdg=#rLR#@(-QLZ>cZcGQi=Tn2kYu-qwLQKDuak5D9TC18bAJTz{3hKN6-%*S*x|++ z({`4lHNEIXA92Hnee|U#o;bc?;@w5&xr)q{xEx-7y zoA2hRRa%9WP|gB`$cSJW48~+I;bTnBHrV70#u$u=A_|Z|fB*#$3J8>QT&3NWcC|U& zbi&MZSAGBJN$0NY=lA^1Bkk_JXF_*%b;Y-L`}iCs(IZV1$)KAkM5qEnO?Yh zInW_wB!R;@$s#!Of)}3MiZMMak!5IJg_~%ALrUdc{5&AT0%Mn6FClO4s|7B#E(k?~ zZ6|qD4qjCO4=)_tXlp~t?_87gMhLv6JPv?>jm6iy}Z#> zhEtF#i?D|g1scQIw)m2(?|jWGo^jZ!@yvNH%&EXoHNMVSQuk|ETZzEB_Naefjk(xv zl}%AM0P?FUTc_ap`69U7OoN2Ug&+;ARykU&sFTZ_nubP>bDHUp;c?mYt2Z zC&~;7aX`(D@7ccdw!1g1T{T&!F98A4l2Z&I34jqDYDG6cykld|GBf}9Pqrl`* zYj)SnlTLc_nWrB?1TV@25e>LCMHOkAPp~q#{lt{qUt-o(;J`|0)mW^M*GS3k!5Lp|wI&<3&eew~<9kteb388wgYOHZef4Tnd%?~~rEtv3W!hm`u ziZoSq*(uLD{oI#4Z|Q;sB7Syy_OZuz+hgg=)%K?wkpf(+rRTOPjfyh|{$fXM)T&C=$?L@U$p{O6Bvzjw>K-}b`i zKkcYy!-$A;?vxV_Wh=eZaijuIHopGWhi2!xtyY|-8L1k}iF4oj*)@Cjbt9uhnsl>c zk2&;tXB}fA>lLYw*)!M<7*MLB@B$N=u?35#`pKdNh-1v{G|QJy#!=pClZjQ;gPa%w zLs`743PMW;q8o0!d-v{rw$&s}RT9(SEbV;i6W=gInfIX5h*74zF@@o{-=y;twk9u&qzD(ck}HB5UHtU@3amn4Z7gg{T;KOrOtG`42jRQD_2xj0gzvC&>f zLENzW=6V;M|2s$q6F~%|fPjk9s96Tc>8GEfpdCB+Vc)JEH#Fo8}$xr|H-=6)Xl}pB)2nb%hs2A^)M1T-Z9UEzPlk|qWH~#M88-9A> zW!u+3Xj^7t@hC@&EKl_;{47D4*UQbm)>d6$FNJT{f=N-uB=SGQ;mmeW63ep4x6Z)i=YAyW4l-I|! zmJ%nc7Y=>t^Y0s-T=Bhc{9G+JTTPdFP5atx4J~Z^>bt+$xpU9gKKtflk5~zSoF^7I z?VGywl%TW>E&C#e1v4IDvAY#spt#nZk7oF5MEWhRT z&mS8fy5v_^Z+_?@W0J;jtYna7q9V=MFpTKP$Rykr?;8q3Y+T1_cP|WsxG0Ck!E)l( zP!u;5UR5;9ym$cw((7gYJ+o*x&`Ct*I{m&SgzXb7UsVkN@G!;zPGlnI{KC=Zw?6&) zpZ@-+-(Ps;!}o8pvBa&&tH3+Wx*lk+SnXzJzwpU#J^T+Z_|^G;K&u7GAX#3BEga7e zwv`ex2c#GtfidvGv=CLVL-7Sa`|Sn4xKxr1&Q}SC%GQOshv-MZtNuPqpFrl}Y4 zJ*CCN`sUZa(0z%`O+e1ig|{|fKtl! zl9q9k`ls&bmM&SL)C+yC^8!^hm4(8=3%{zo#>&*0XHJ$nZvOKsHK(P0UF# z&nh%0J&~JN%=LdUJO`gw5UiYNErVOkxVnFUWb=Yx`LA<@lMk-QIoGby;#Vb}sB*>Y zK)O6XJb+~y7(8kQY`A%l8EQ5e)hASZU8=qP=u;2))d~g8=e5dO4__2(JU{15cpbkjY~D^Rhs?KgOVWhNRrHzX z8kI6UpUyzaAE<4#$|1n~?~46cJHC#JD+(Ia3wb@kM2TvQT(O6iRMraaq}5jgpOTxX zHir`S1=I?JRf3_~%;m`E53Y{KC}W`ma;2J&)<;_&*~uk<6gVQ4(C2jEpk~1uQu?YP zEkpSTa!hGhq+;bu<9ptyQCJFfiidN>#e^!!L?B4vo)<9FlGHi$37Yrvsj>k%6fQp= zRLSRRxfCE7%tmOL{x3KV<&#i7xwfXoaOyQbbJ-;bGlCK`sz>B>8icZyG2FLVG5 zygVO4K|HNwy%rm9{zn0yAD#&wngC|m(gg>uUUch*P28T;#1V*#}*B$0O|2wA zNSSCMM+V~~10)T;hnG;qMT~kai_)k<#Y^6APEn(AB^m<&qu8L7tJq>gP|2cN(-E4h zo+?g9ASZsfX*X=|+n-5zxZbDkBNc)z| zUjVtdU=9>!T0x;YkCaw`7AqZ|!=+k6CDbZeTB_o5&Zy3Vh62I^yQkV*1*b9!VTeKo zpsw&LF`V`@5j`nZFfAx2+h=|t=3;8i6s*xVJAmSzA6Nz!s3Sn6AVcAPnbAeY;wR(a=R7E^;=nqOM zQsHAufl|IcPaT#?Cjs%=m6NZ3|En&(;HuqQw~S1Vd1^}EiHU|+9d_##*L`Ge=A37J zB`(TFops`2iw{2Vk-P8m27=5qns={%IEv%Rv35DW9lQ2@_*37Xdw4rSYt?M;o8JAF z=bm*m8KYo@RWQirNJmzc()LmYIVY}{^*YE>>UWd5ZtAP!Au8&iSyAK5i#SoJdTV*} zzRoQ--K%Lo8f$o$DhaF+=Vy1%uuY)*BdVR^U$KDSvqHoCpKgdS%YPZi52jn%udA8YelnTca!t3*nyk`t383+01tOCyR~LvguPnGt$55z}-x z{lLHfkIwdGa>%$8%+l*)r1^<|c~#O+@4A1(hTCszEIry=6>aQ!Xv4?8dj5Am`&O4_ zhDpS$c<}`Gp}j>vN&j@q1D9O#*Ps6C`fYbSgjhzFEhCe8mpGS9PL6-zUH?8g+<3*` zo*6}!RH>@GPs}VTk>SMauYY~zY_HoIhDgRXO7`tqz3zx(pR^{)T&J6`3Ydru84ldf z>csnAKUH`jU`m5@^C|)nh=Yi#4>=ai{R;}kI>;Kz%T?tr(6z_u-cr^uPVJ z3;Rx@;g+}zNmm+nV)cT*UUtnJXZL;P-`;T6nNL!Yes@;;)1AHQlMaGCmK)~fVG}~! zphg4g3(K6E7;K2f-8xwmOp&5YbbQJLrt#4!Y#or+n{w z7hnGCKXrHQvO^7yBdBpR)63SZ{K)73_1;_Vec&&Dwvz|AG=a#-`0x=YuIqKWy>4c# zs3;k3G;AE(Z96*Kw(O-=tfC-8#sFwo8Xao(n{93zFT#mw#8C(|2cS$|)+eCS0cK{- zyNIiomgQ5{Ek9-5-#quMW4`%=ORxOhU#EBOidxpT4M_?W$+|A>zVwYRd&Zf^|Mtfh z$HNQ7_aGq2a;%kbp+OaBibR=L)li61C6I-cvOXowT1!y?j~N9KF*6%hB7sqpBz9zY zs2zKSLPUhgxXc~1c1>$^d}`ZnFst)fx0m%CTElFaDJAk`qim+XbnWVQyz5oZd&gFm6phL=3e)~H$3MlPs+S=Vhs_Sz+@mO#H(mO$)a}i`#-(n z!XI8CLeylagc8bHnDhnauO{w;Y7R)G!RTBWYA^ktkQ%CK^Ld{_om6&85Xq%tlhSxB z|EkPv%AK`=^eJ(Ebf{)IX{FD~`wAzgaISg z3dm0Nh{{jN7g#Xw6a0xPK$b5?sk7_rq{!PH6?3Pe@x=fdr|^_isiJGmsZ((NCMqrF zf{Stw2B^h2%k!*xBqvX@(7e=U{vt)KotC2Hi_&i?2pN)QIfjfX*MaIBSWT!}$${ud z-7P8pFTyW%>wl1ii|Ncaq>D{wOM|X%V^$*qsuC&%F{+7`7GS|2#Q^3-XLFJaSfJGl z>fBgi7OS7D7)ot-7mujOQw!TwzEo~O7lWaSRJz|}2ew{yWx*Up{c2QGE65Ufpwzha zAbifWCFr8S|J5dW<}jX$5Kk2OD&9?b{Dmr^TxyFvgj>mhVmxJ<2$gCqBK|sWe)C^g$csvK zRs$`@n=b(rD6_#{;U- z)@<9?z4P9UYLrZBk)?-gB;DwEkBiXmSZ%PF<^I&IL}tPLD6JMX(3hYZ8FF6#eC=J`L>ukM!$Yl8_GEomcx2+K zXC6H|)adk*ut}v7Qh`?c3k+s#m?}$<8?@%5!6i$ny4`dpB?17Jx$xPY^b- zo9c`$Uiy|dz3iw1mpr;VNX7C#R=`vH zuYS=#zxG9Qy}mFTf(+<}9Nb5tLuwK1qCyLoR|2(waT%ATdf0kE@o6shb1hXQj7h6U z7G|^{lbSPM{#jz{=B#n`4*=}mw5S4UpXM6e5 zb6@zhE3UhHG!kOYvaHpN;?Z#xWZtR(Hs1aC<3IcTwTG-(+?(rlGl=)ctUGw|g0Xbx zEZS_@xZQ5yf-!3ih>0w*Dl>BgwoJqfp^EE6coV|TD_EVEEc2laWj--c4JFH+?CKETa*}Y3wE_&%JpL6iaiMeh{M580kdmi5Wvmg9!YTF*$7zxdT0&-Dup7*2) zTkEFhmK=EC8{YZ4wW}uf?VF8lv~c0bJ@-BGr7!=?%{db{)Rl_}0L=M%8G^O;rh7Jh z@$)}uHJctvk|`*eok7+|vuPW#dIxxrsB*7AJ(JCu7yx|Nf#cbk9uZ|CDyd67oG=rK z%lciAA+kzP6(~hfY~q+mlu4Y3>4*~mUW5pS7zD#)to_FKes}Yw*Tst$rpcV4NSyX( zW?t~-H=KUlAz9|W_4&6w^YPvH-+X5@6f3c1WDM%9pYy=1l{Lv6k4FZcfK3r-ttM&dp4J_r=lgI5eU zEL;SDLNuBv^c(d`k5Ba@k{6zN)RRv*@>{2#^po>0y!+(kNuzzgvqNJ|g>n0))*Q0x)Bp0CyYGL*lg-4FW=t8IG{>NEE~Zrg zI1M#PVTc5z9P+o6ku}6Ih{DNSQ4{c~WR$EJ7B+q5mW@kY#~^Q*Qs)m|GCa9@#m4&| z>o-6|L+#`p zx0UTR5O$cqN2*xNei!9&z|gjN;NB;QvZ3&j3yZC=(F;Rs!04_Wo~mX=8lpE(WUgp+ z^rrqg6jpEPGvp3OPVhn1UBA5^Ga4jf753O4o|falDu7Mt&Q-={VC~!$qk$n%@!WhU zr58k%8DDxgmDykT>^Tor#%DF;!D-}6P(gu+swd~+*D8#*9}#L#EA2v}`JGq`yLupK z3FLrx)Z+m8qsrJ^?G^i3R{ISk2LM&0En=62e^xD1xxj;us632*<1_ps-K zXNt&q<@XiW66#yx35He)mU$xZiN{f>jDqz61eq~7ccofa*82F1twMz)nLnknivYL`NFj#&V#UTs{7|#?s?hsj`t!?SgR}ryuezXnd{wi-@}Lr z>VS|LwgOg9#`(2J9=2+-+3jb{3>4tbf>2rtFRDqUJdjquCWH!W(y->{?zh}jn2XgH zAs{E>%gedg3k(hQkV%yX4rdjA03eituY^jWG^z3n^W|0%3w44&{_*;`xeg5vmol`< zD3BOZiD1glm>WL~f@=T$Bv`DGBlCiBa`>e8UvCWYw=l`yGt z38WlYO%(61u4~$@wxV1tYwd+qEKIessfsVlb*>hm%wpvaDp*mpeh&yBHi_5gpS5oB zforxsw3(Zn$AG}9!b8KG?tft8*w0RPW_M*-(V`*r zF9M7qV_;b0rtQ1l;tBKmzdPfYgO@`kOBcy>%Hb^J7Oklm{c3h=;6ore)Nk!-gMvN zL(3K?-L87|Nq^yju@%b?*t%oip1sp-NFm}C&M#ZJYURR_ZZCCCv!qK#N5_T%<)TcK zSF4VV;l+z5jfH4KNui$LXJ)$2J1$N^g1Km$;0O$$(TqWS*$wx9`jg+VBSWcE^2CUJ zzjNTxM}F&5Z}5UnDq|C)|MISXc-<>*u`1T|l^dAqedrV4{PlUC7#VIkRj3e&Az=cV z(cGTB(_8ioEg039HFHM1cfQl@BZCiu}beD0&K zZ;rJ8>+?TO<~q$03nim|=gzHb&p7GjFMUoF+br|1dc`@XoN;2iZJhIlFgny$rEafh zt&L1%V$<>RqwoIerYo;A;}hOH6Gwh}dgT#IKm6hUxAvf=vvYISSP_Zh*u*w50a)`M z-=+0HL_{2RVWsbw7={gzHJUj;HQO6$MsI)R8RtCXxUc{4H$Oe^;_X|vGt=Qq6G%Kmje1KCDx&1oWInH743GxfjFd>Ng`pf;j{NL*@!+Qi(@j*#+9K5xL^KhB`bjb|HZZWqI^~j!M}tQ6xk0FLgl$0lxF$h@au-^;5yaq`(nAPQI-}Ns$b-Gw~*A6^tEUb4kED`75gM)qDzu z3-e2~sDUT7`%7{bL6rgOmsHe}|K@Rg$KLf5*#erqoLJQ)ToLhtM?MKF(zBQ)FZ?C_x zzS^a>%I~jLYO4=gtE5TwCM{n#xCiIGaX)(-5TN;-y!XqELSC#BVu<)io8 znsq^1zA(S4h6d~Eg7xvkq8lcVY-ubM{E zXvE!4|DlbWL1;vzBK1>Oh6%tUkL}#L;nC4{+xe8>p^VT*j6A6~MqMwxs&Jr&1%w143x~2?5LCV9W_p12 z1q`%;L?8rFWKQFVmGX+w#S{j`9)a@^Q1BKRFa!lLl=1>AqC_w`H-&8sMF;{AK}96= zM-G4sDN%5sGJYTaq8c{^<0R}-A~=zeW^~u1`|iB?E|4=;Mbmt8P>>-Nf+xxtUbAj% zc<7P4AGqnxjYl22oSA&V&>}Su?jVcgMOTa1A)$=2KytiW&c>n;kDild|Rm$Z2}3^NpLNW^fOo47OFV2M9!f6LPZ9{7x{k7t$`xsts1~IN~D2>L+N4< z5>dkx87{1d+Dq$Fsgxx}^YMJ|=e24>A?})H$sZRC*Sw`6td@i z9EuESDN%w<4Yx6nI$1PqpYfbC&fo9@P@78Vuc}57?c06R?GK)Ce zgZwON*gS=TC8l8h)`Q_(ZdaF<^4^!8^wc+|d*-;!y}3TQ%xNl76Hx?ZK!6O9;iN}OWFphBI@f=6(~i^&3i*KG z(GsA_%p^o)h%A_xz#KMlPG4qbQcx(0AW-t&hyJbrdGS8fU>Amjz#LNN+Ktu~f4%p* zt8OQdhamG{N_#0Y5nJ_QBddmo8Gb#P-iw2m&(6(dnIE?_Gc%i}8Hfc%U?L0PdVZ6AN}<5 zPks0+(d2|v)^0Ksn>W1qwQqR&Gq>&7<9wDSl4ky>Ll&l)iy9HYAO=}U+`q2R=1Q?=p_D{Tkn6%+dq>`@719Nlyp_q3!rq^ zvB%u|z^3~i+Ol=~zW&Ud^nEloroDZ{5KVMwebdM9U%#<2+Jt1zdr?*;I6wr>0Rm&F*&s3s^-g$f z;a&G`eDyzl4o;Yr*%)Fb!-kB3Wkj*RiT94j9y_c9kT zfA;+^C&pJlcV5!zw#G(gckWtxz{<~m{GVTR_OWw)M{HjF+>=oI+`n;WmZV1x3t*K6>qn^2XUte`>n3Y{>#^s;4;xvtg(Txa|6* zYK@87rkA?T-tN%{E&k4byz}eFAASCJFFNh4Uwl@UB;i#BkX3@BD=dyeu!}DgBXY}%m|1hgTuqYZ+D+9E zwPTw&5uYBswDslpzxP3`9US=r*W@>%%l@beN3IdP&xqh5@X?2pg;Y*%n7k$^>g!L)ATN zMCq-seAa7U`t$`8&7p$kktGETiD3^TfPf=wZL1NdsfM~h!D7iWC*txF1i{J>2-q-( zGRoF+iAX3MZZgI&lZfOMm_toH2vj7ee5GCs@3Hi(>kdxc_RLKo8gPS12MyHyJ_R%7 zKj;%&P;-59k6Wu@d|+vJ&5v0PUR?JOwVG~yWLiF}$e7lQ!P1)_ zmU|m0d#eIKqBTG77790ACdzU7ivsZk(5ltR7Vl!f5e_)~*|{luKVA zH~*?}lss0qa>`R>6`yMzSXoU0WtZH1v;&wLMV$p}0K<}*%8Lu8A8@~@puoI$W8TgC zeR3L=SzcR2mVy&jfb7dn%*d;OQc=DKC1XX0m zvE?jRKuyETgN;$jqO$XNNxD4fS{;}!iA+U3of1q{#O&txX;Fkz;#3&9`E^x9y11&^ z4Yh+j!6(Y`J4N^|mnUI9MOddqlVX=_c_ zR|2OF3o3+GV7?;OP-p*vAHq;-F-{u%16p2PmvQ-RgZptXc3oabfTHM}Sqo0rKLbK* zUd+7MU4@%C5R@!OHz?t$AaNOs<{X+r1ZF4>IcU}KPdeBJ8;ZKs!6Ljod&Kx1QjUhcT_A)48Tv89^!0fk6#PYfI(HI_a4J09Nf(3Yu#mb5zk zEVA$lVe6G)2!w|p-PPZ@C!QEqpHW^tjg?rvZ%vGze#$XKc}yhWc0}O;fodrGAqqt! zwvmOcA=b2j6=n`9sdHY8a;Ts)K+@bXQ3XOmoG6@UQ1Y4++Hf!^Sq+2LtN6&;W~&j^ zOkrQho7AJ9IVUh2s=^H(9xIk4#JNHlh+IaJ5+U`z88?4*`L%l<-$o5$^r0%s3ZpRO z!@?=lgT4C9+nAEMOE16qh0i#Cay0HFj+q4{MJSqUTp8K|vZ^5j9D&qWUI7tg0;dFp zccLoIm|HDtY7E}<=%cU!K@rrHJ?h}Nol zFGOfqjw1WSGYQjiK5gk6r$9j_gbt5Nh6TWtf4ys_=WN3&g<5gK#%PwB z$?<2LbCXw+Rx2#|U49tO%Jv$I|A9RLMI*2GcdLo_TnCIn+ZfK%s6V>H)j z0`HrRXmq$~m?CD&@E~HM5M@_s8U=B$m&|oK;n15k6vr``y;sAbUvi-tOR=r0BI>;% z#EpS&2IWMOB=w5OvSCx&riICha>1pvBoqR8CrXGT6B*)ynHg|dmZn+e1k4J!%oWy8 zsR*g4FB!%c7NY9;5_S;f*=nXhK@kGx`mSN|-A})M(RkyMUtIiOUwG%z2|L{(spG?~ zxxQR^^&K}~f9sM1R=(uL&zc-2Od6rKzd{^xM7D-FxF~u_cIL4Mf9d0I{huFQx^ewx$^6oli$3$ocm3V-j?F}m zSi1&Zi6aO>l4i4c>``mhEE`?Dc!aA?ikRy3;y4Cu03~_&(ILF*oMYz+u~^;tU%m+3 z@~#gRRVQ93H&no6uDSW)SHAJnvzs?Z3&ylRtHwK!fY5CJ_1c^MaK$yUZw85nrhxF+ zsIfNjk$4e;G3Lwvb^go$=B!q`RR~9cl9Gy-{;s`ngs3?&x!|Bq6c*5AFy zbY`Qd!_tqTBJ3#!EE$Gjh4pN`vV!t0OIFoK-dRIR2pvgOy+VYYQ|C>bOzqw^(oXtm zV93zFHJn8dK%7{hm$-(?C*J+?BuT&dUq6}KwR>{WlFxkX9WOfj*gdlez>&qineOhX z?v6dP+js1_=l;ij^0VKFp>KZveWxA2cE|4Nh2ulprsk&h&H%PRqIeKzPG{!&-plT( z&csNo)0Z!N{g;>g{^~QIeCqKhAF*=D`0AyTtCufmHX?>Zs)pd1g6XS5A$!q`BaNax zGifU_?|k*sUwqE#O(P-_$5Ff009ZIu@_=o|4a0Qr`bYopi(evMjU z{KHM}dBgMjNyf&kTs-lV7d+QCIdf@f#wy->?}tZ*AA02R%@01nmWjM7i!xx`TzBn> z$1PvBwA=G(KQpYvv~Xg~bMvO#9_q|YEnjoM5r-~e__!6BC}w8gOC}dA7;41;B5OlA zZYFr=hnpCRv@x!SOU=gvi5JHM^oW38Nb9xvIS2<6CYlPsOgVzyK{l(~&q?vlFKSfzfe+&BnD zu}Yvk1*f$OpvX)6p|IK&K$WyEOa;|CiF3;$T$E#a$~7oLSDOD*Rjc4ju(wNam)6!r z2LOrH5^61?P}(<~tJ$0m#Trv!DdYi}^4x|7|Efs67Z!Js53c*(^<^Rjh*<}1@yc7z z=Ul=M3N|ZaytPmMV}4}6*w9)HBp9dF8;biVzhhv{v?LHg0UI2=P&tFjvuH$NF-tgM zj$jNB6BKcnT5HRK7B!eqF%JQG1^L0fG;hCZ{f>fz>yA`K#^QIrx>|EwHM~b{!xX4r z)YiK8RY|gXI8<{#a$s0tv7ADbyaKK^BpSFD^;?S_TCGX7(TfGFU84X&ostl|mL(gB z^2ydk1ajNE1_}#;BW#=+SRoJf)HpQ+X7gZ{FT`(HKG7PpJdt!Rzp8(H#b}ZXK?M;+ zAwX7fC+0!xYF=(3OUetiCYvf|FN8>Kr>Nv^ZXOJ;ycDXBwv3R}_+!2jC|!ub2@?QO z3J@(Vg@0WL)i2!-saGoC`ctVUk_&dEfYcI?s@-{QWkLCorBVuL5iLbcD3X#u6_jND zfFDD6`;q0Z&{%rF!XU3lQ~ zla5%LIL~CH=wqTGnj~-{P#tQ-Ho{ETZ{0C7GdDXm-IZ|V@ytH%5{xuFc3Ku1vg*=wgKs&94bQ_*ak5Z8$+(uZm>1G_V%{#nc6co zH`hzk%z5F39yT{ z5|1`m6DUNDLLg342OE*GpuA&jmL)Vcbk!Bt-}At8pL*(HnfEz*i4Xv*AQ*rtK*@-U z2&9ux-#=jBlocMZmeeB^ao#nJ86J*0eR=Sa?YpL?lhi>qZnO?swPbvJ$a$|d?Lh#M zI+2)ZxM>xbp35HHJ3Teqndu}+;uP9wMB~HFMH9n|7ml?WaTDk!-V4~4O&MyNflB(Y zsB&RJ_#E@8q+o(maZVetZ8o`+=$6NJZ`-wR?@Zri-ZGDlHOEJXmMj`yHaTQ~PTz^b zR&nzJRH!v+rmCt=yd@fHMwXF!Y~MXM*Xzx7lj+%Rl4jl^j?7T2F+SRw7#mtVInuTU zKritch8hN0<|;M#p?Q>KKn2vae_c$3GvSsfbMDMzRxdi_;K%NKfEo?Ta||rrb9?l< z8}HgT+iNu=4I`H#2Nq5W5Im?6+iv1+z4`7;DYg-53I;?tY9xF19dXKuM<24706+}o z{o#ewi^RmORz$!|s#~}1o0{z;{nUvV!_7uxa$PpSeuD8p9()twob#L(LdK2!1i;-5o)7 zGbm>~4<&*3o}k0cC?f2dNgmrfH8a=g_EPWta5G-IV08JC$yU?O_WE8lbslU10!)#K z00uNmU^fy~3Ob*_N)!OXHTVQEEHueTB>n9BU-6okKkJB9i#BeV$|Mt)LOfJSylSSZ zV3VhIKm;q!!8>AutfP{7RSEfn5L66R=!5tpQ`mWV@~z1I1%(q%x0+C~lt`2!Vsnyyw)&Jtxe*J^Vp1tmmajb-%UUL9~X3;zv23O5jC@*H>rR8M3;P($7rW{x;AiCf%`NApq;Az`E%YS;x#y+E5WcQa|j zef&MIn%O<|>mUF6-S7IxSG?%V*-jELwUM>kcTIog8yEcX>g%`e*xB8_54}E`!?&*A z@{BXrHCxT75!uMXgcwaI1fcL@*ftumF&s6bIJS@N+0H(Z{mx1>xKxELc)@9P^W~;_y1(-RqrqybMq>g|<8rEuj zeAG5qw3jdL_dCe4@zIeCuttFpP1f9|EZsoLQm?;HRVVRt& z6J>(2EMBMP(v#P%ICwE7u+Nov(j<-P%=i-L#)%v7yNYLtTdpF1-GZ+cwR0+~T7r|L5BuKV%#z zpw3}nF0?6QQZ-5tRl`VA^ivfwjkY0X6US^VP$b@hGPT48#v1k}BpWQ0iJZECbj!mB z3P3ZMUDG}1J%RHuO6k0)LXoCftC`LAGk}ASp}BjOA1Dx2DU-vMNDF>W<%JWf=#Ccd z=DZ~cI#+YAnVh0BVdu-t`M2hN&3@Mv2(7`_gfEFC*mX~ozt-{r^WRc;!pa-#zI`EL z3aGf0nDKIwgQKhiR_Y@};X0MZT3)NV>Q)92R3Ly0hmJxNiE3_XWhxI)M+g3@-c|cW z>D3YeqNE6HUk@zQ&WIIWe}003at0{NXAMxShL@`a04s{Q`ao+z0s__Bi7^q z5Ed1%-71EMqMn8m@BmGUvv&g|3WXVqF&BnQ32rO(W;q(<@W?P1%0fvC%H2;bMqI8= zMe_paR)fwIi&nB@MUC9y7lndc8TqM#Up20-#W4nq|NZvGAetkrQ4X#VK$NTx1<1lk ziVk3TEtvgw64h+Naxs~4 z73sq7sd$tsyr`Gl)2hqhGSx3!?9TsVxW%KT{+q(k_5uOI8IJ#Z;waR8S*|O9x~Z6I zAq0Y2tXc#C2xwdhTP;6YRbE;?y29#|d@F?ch69Sh*I5%l-olfFL&6Njvuf8+#ROD< zUs1Adi32Sz7p9_{Z-sgY$4m0&?*z&Z&Hy!!*q1^_3-MQNyAaiYC=pZQ{8-HE)*Wa@ z$CI8zs|nu^Dn$guX503?_dm4h|cThl!6PFNz zk_tz5daiThZTG(Vg{KCkLit%p#FpmflDqHUq-?P1~ogzUhHK zUVi&6ci*>p+xC8^2k${5ef+7%9lCJ5J=<|klxrJ|1K5^I-CIDM zM2W~{+4xBFvRfa0`0kA$V@OD{p!CVBiaY4&!*_1k-kIuXOe#(RNqV+9JoV^9*Iawo z3CA9yY%-r8siWWlD?ng?RSn?1MooL)hV8$-^yaDQPP5fCqzrGEvNVf_htGNT$!k_F zh>ca%Tkm`9_m|&t$z|6)vT1X#*ClXdbK-55->a;HI6o z-M8tlf4%R{yB>OU>$Y8cr_v-PMWfMNI61NQkX0ufe^{GYIRF6w07*naRM=^!9&!8; zt5+=)44J|*3PEcD5yKd|;r2~`{KKuS;kNe<>bQ;s>%8_vAD z_Qpr9`19R={_|}QZrZSW&zw33V}?hDmoHg-%#+qW>+Iv7a?)Xku3qd!)69pKMtNmR zCCyzQB2^JjDnrf4FmK*5{ouyOZ@6v4?YBR)W!sLOyZ7$ev#;0hsn zo^iqLcZtSKxzrOCZnXDq+u!r@C;i?6aibAy!nT!8;`vdDYwT4^PvSl<3lh>68EI;M= zLtD+b9own7-W7kj>(`guc-!6Ur>AGcYZUQ6z2WcP_PTT0O}_D-&42#OgW{ZtJTolR zMh0ni)KP~#`P6mHX7l#Ff4=elN4M-)ykKjx7_u}Raf0{``r(0*}A>c?}(?-k>Mi`IpD0Po^aM# zC$2kW1tm%17#xF)!fUY2RRIoo3M~@SQ1t+`>Hy0iA(&i)kd317`=0M78NBq4ShdhO zZ=20DQ}hxck2Hm(V1;bdE#Kj}yL+#-JH9{2Xlu2n+0ELnw0*VSy zg%%9L*BC<~qy+)u$V54+w$gD%h1E0Q8_o7~H~HqbezA4qW}X;UNu#Kh?b~z8GoN<$ z@dsaV-F+YY=+|%k>upeNO^kJCrr!TgpL%4|?vH)wRSkpb>0TVMwKlRQij1{}3j-_| z0L1y>buUUL>UPsR|8ggs0~qk|eHgK=*hV2X5KItuUYwACi5nnMQN&TV+qvzJSHnqR z131_QkB%!GJSht#^Wp*c&`^blp>Z5taQRKI`p1to8Y6aMf?Rs{T@SqawI4+0p&rB- z8y8W6rB-v!(|>*WCvXDwFaR@VX4f2l@VS5cY;Xi-GKRc>EpwyQZZ(@6#f<<37KLp# zfq+?5Da}7GUcCWlqNkmB*cneh>B?VS-WZP{U}j5(l)*5~A!btV;JqfPLgB@GLex|t z2$2Oh;~)I^_wRepn=5QEF*z9?AO6aJ{mau%U$<~H2H?{E+-#@kW#QQHC;#)8x8J_; z9dCchIcFcUYUz02Bh94jwC6=Qjz0AtKb+gPy*bh_oH8jJ(-&EH(CXMW1t4Nl>xG1% z3K3R~RIP~hLE4eYOBA5kh^kJMguwyRsel(^B7q1P5HCKfXH`X15%7hD*n>8#neJp` zL-rkSdeQMG9R2Jw*MTzcT^yNSB8;e?=q-P};o%#uShi%z=t53-$@F7;AH3(0p3~Su z#fJill|87XSxErY34rZ8_wLxSXLQk$@kPVgT)(?($8}q_J>~2tzw|jLW~qxJ&YMC6 z0JUWX5W$N&=b&VbikI20GlqxT#t_GiI3TaI@RtBM?_9r=&dp{0zIx}pLvD0S8HE}2 zm#B=w2N^_$K?Y>=lu?l6nFk3dPs_L^|>R6RX z8SttT&3M<;?1w(`-M{|+vgSm~a*v^8qinW6G&%ZTU--bOM=a@fXC`BAj6@*5bN!~T ze*2Oi{orC~BW{h}@t0da{^75D<&*z7)ZnSPK7m@1iJOs$;*iwvxlux1K#^(15fmy& zvuwC+Ok^Qa6(3OFh}cFk$Cd#G8D=0Wt-io2MCwJU+5W-zfBVj7EacnH$Hg8vCb&v(L4=t0L*v3%a=&~=Rrql;K5YgQj0}|)JGxNwf2Pi4y(cx zwQ=l6#^M{*a#+FX3>jI0Lj`;(rx|Xf8hEPoGjnj-eA?_cDkTEE@-=p&D)gu4pzRg^*=ZYY8!^F?*>`aE1;*rpH#D_Mpq zN=*O%`I#>)YTqaTq>hMTOOXG5#Gv{=>(YlR@>iD*u;G9#GYre9tJLcAgqs`#G|Yc2 z?Ds07f+EeXP*2w6K-B+z7S-?yYq*M+4-VxCM4T$b!PvaH^B|0^CUxh7$7<6V#4JC- z+7jSFipbVRTZz^Avno{p%EQ0^`!d$=4(noxmcEwo z?tLh3FIGk9B%ZQ2|Ux7201Uae$Zu79F9 zPUh0v@4mn1r5UjhERzyhpb^oIZvUS3k5X#{#4rXRkbsPcA8zndPCh12zY&yOdrQfw1aNvfC8@AD)W(1XFYVU(Pc0PFh&A&fCYOGlLqUWFe znpZvVDJLGoh}HO5?W(}u78=g%(t<6kg6 z%aO5-D7FTwkhHUP>gGq+-+bBczW&WcFMsLt-}dHnPdIwbY|o1#KO-BQfE<5aJB6*G zdzymTSbNd$uiLkKni`gn|RArsAd?o*DL7;f~^ zOev>F@Jm$@5gFj>>+bo)2S1CEMabsh`_MiNBT0ZL`P>K2I_c2c@7w&tA7A#}^M5=0 z=wmQy+OahauvJI>UiaaxTOPdOrVG!z@VGNi`oP;?`od?O(uj<6!c3+Fc8fw)M4=iR zisQzPy|aJ1{=T1I@TWgqb7OzoK8S;1j;v|K##n%SFWEQS+5PC_cl^0`(N8p9yy%=~ zp7#1zJ^vYJ9NCObKMj!(QibF=$zl(OeOv}46bQ$tsDP6Kq#y=~27s3L`uER&@d(Ry8iChyzb+eo*2O z3WslVA6Qf*vTTjL^2U3=_SK*K>e9bp-!zzE4H*Naz20rQ_4Z4Ce(7OPJ>~uHe$DgG zIwo#5;0a_wzTh9lfK5<15V;5~lQhaoN~EI9v}yak8*h5xtkc&mSvX=d=Tv*CZ&;oj zX`K6_Cr`~xN3m%bj+r{nvxusywO9S&s@>bR$FU3b<#wjv-x+j2&M{tm-qUXY%BFn)ZC=)x?>)Rx?^KIo7Zy%4<$B z+el+!N=(zz=t9lUR53H)J-jMNl?=ffgyS6Edkw=0N0QVU9~tkBabz<{W){SP$a{8< zT;LpHf}z;PLvf?sjAFwiDrRP;zhJ>cmLZOtC<>DVFWGw$f%nSc>~K+&hR6c~5vAa1 z!3$t;VTYvAysi`?A zt9k{2SY5Vga`JDUd9)$gyKknSWK0yrQ5+iu-S@!ehi@$vD zFh1l33U=O2j9{Eqm`xxaB=8A1rlFBs ztrJytsuq6{`=sytY2g(KjDS*OBR|ubnH~ylAB-WxWFuQ9B+6Z=1+GVcHrVOjz6>! zM_#0tWZruYDp152YQz=*#H)B{S=q}2q0fLLaW=F@8K@LgxG5CsWdLlz3^KX4VgqJk zqX>RWXlV-(c=&K|K>%y<(Ds>M{pR}PPdxnObt{LOwx3Gkgh-7cQKLjof+{Bm*4T=`bV3sm{`;~ zpY=ya$M)>l1*XhLULsP4X>8iC`MjTA5s~)$X=EeH($mj4`m9qA*DC%*0I3QM4c~a> z4L4r?XHXo3V8M%}zo$?Sb%;l)J({~`1^hZt(imdSX&)S;kcXiR)#%XtI0|C?fiFWoQr z^h6hzLsEd5ELZ-17(l%g%lrYBaq;fzrQiqisQmk0I- z0}=a&k8*q40YDU?!rWCu%@4p9ML+5*RGZ;`{&;D=Yr#>0Vv#KZ6sS4F|2x6!Alv`# zGcZRWKR6P`U-kmcV?!Y-pd~pADhvWXi-3TZG%n8p*H4OSe!x)w*AY@_GF1wo7LTmQ zLFR4NTv>$S66*1a+R$s4hVjrC?NjDxC-_t-czSs zo#<8UtHztb>#lXU4v>KmL?RtrIU1A(#e2$e=SDnG;m!(iOSIojWrG*WCtKna1}}X= z=~m@Z^A*b%wi<4Jq(`)Xbrt)V2t`J*Jda+|xE8*y)t9Vo`0`)#hgFei|1}8z4yOvJ znBzblpPc+Tw-Xdtvug1%M;)~3*1IWWtwcaxycip0`)2RG=V2#^tm$RMhBL1XYi4_L z_uUUscLo!~K1~X<)Z`1Bzd^}HAh&bVW1BYbS-Wy0U727uybE;%OhLj;SmLa zED*A(q~eDb44;1T;W^JqC@ria5T&73v}Nb?SN`Xsum8u7FtejE(i&Pm1`>gB-gq&p zAmXUmY>ZhpmOxo&_JVKy{5O|e_vwFq?VJAL?9oP?q*+u%;=%#kqKs51Y-AM@P7DJe zji8Wt=#rZrf92~xF};3cYsrLN-GZbp6R)Zw1a7qKkQpYB>tsLu`gzw~cgN?y_?~mm zeo`+LuK|vi)4!;DoU?2|Ag0c>oALI2>GfA#>$85`j-W0S`yyj}uXD^P$DZ@lqkezc zs#~wRO}$X8q3((Lgh$73z4^}j9(we!gBC}m24X059#qb)u2AJm1HN*o-CVkCAvbt< zvYqu^BGyqeo6EQvj}0|vdg;G@?g!uh;?Ic9(27OYCI&r_WF8n^ux!nWhC$EyyvKz| zD~@bruetvIPkrIXfBwxMVf^T#5p%$V7s@<)pUX7EY=@d`hAa~jcg4kj`qPy+z4mqI z{>$6`_K@Y1y?z=hPKlyc#1lj9W<0d8wS0-lPy<~{J!PJqc1Gf*OGh8yzUQs){LJ-# z{7YkKcy!eylXEJWi$pDfB8w<)v5D04?bqG<_c!19iI2YZz5nlT!tONrmJUg7HIOJW zY|LNoc;r96_KS-yxE!5cG%-HBbUYOG6Cp3*1ObsD)3lMH$c!<;cY442>F<7Z@iia& z;A`LcmcMB?qMj2FE6QwBk)5L=ucxI}${C~H)|)g(mJm@dlT+8NS# zG6qQ0MtsFJH%`C%+=b)qOtcafl5ZPJ>O`;o^R0;kH>oI_PTXiDd$yhO%%>i8=n_#) zGEWQzTcSoBJ+gK0M?Qc4PrvIF`f;a-dBF`hbwRX z{Ab?wlINYssZUeSq(wtqC@{HjGG4fJ(S!(m;-V}XO}7lS8d0kq?bti>#&>?{+KaA; zCKlRN%Sh)e^(;8;jxJcXu+^{thMRG`Xd;dp!y}z0sVHZDWY6RAf~5@uIr|pWULecPzaF%Sv=g1CWj(+F{NyrirqKIt5=KzFoe-|Gaj9oSka0&ip-EB z)9ttBdbD(61OO9}1`HzvYCz3Ke9wj*pZoIpKmOJQa7klw+$@_Q(tsF%LW!8!FeA%0 z+_nB+Z@BxS-(UCHkH7Wo6A$V3`-X{_12`&}h2p$5Y>fT!?{0YO+de(BW!w1DaUNgn z0hfj2+Y(wOAk_!{a?=|g-2Ca!zPHsL0~6GsDw`XmOi_zMQQ0Ad8qy+`;uWj{n2&tw z$3Ons&tLZX7k%jce}BwjtD-{f4K?VzFTHV|K1=~9V#4X?Y~1tcqfuMQaN6-^bn*)y zd&}`_7eBgZjyOaFH23GVR#k+GKMS2-nN&o~WK3*rY^}n;M3G@r9(S`Qv~%UWiU;Do zQ?tpQskx*tE;ZCJ&Bn}@ZKpoxoEJXtw0`E#KJ|z%eeIL)eCMYgy5p|q!by0KcB{em zzrK84t7-q`oiA$-HNBIfj*#T>Ln=z&5NBy3Gkg2SX}=3(Nj@C~JgFn?lTUr7rZon@ z#&+-Y?8YtIhgw6ULv7nI*`D1PjnMBnV<~R3X?Rs4B~?j!$TAoUqKplCvvboO0mO?E z$b_Rih^^7c2-HZz;)v8|S(bTk8Y-cCq(wMJ3-u1&=~-jMT1z%gdTF=QcS4c1LEtEY z9`GWPIXLG5g<&Nlk^&j2gF00&@F=i2fp?7IGfzAUBO~e59O5XZsMU@ev5n(6isPu& zwCz^ZXf~SB$Z%`%lF4Pu7Jl#h7f(I95!^=FZ?yQSXPvZgau||9O>T>k_3)ieGSrUA zur)>%3XP22+fAnT>})O>jeP%i7hd|i3orllS3mmJSD#Iw%&TRytDAlFUJY)kF+?nQWkOQNW6&Mw0T#rm#Vdu5-tEROe9%<(c+0^ zizkc(^~_|zMhKL3W+oReY&DwztYv2IC9c!ULcLor;#Eb|iI74?GUvS4iILXCNIQ77 zmML}8Z1CE32brify}QRlZ4V1X_dU4j!A)BZTX|%smlDDu{JaZ5s3JtH!6A=~5ygWK zZt3ppw3dw~Nyg%nUUu|xhaGp+iZpeGIj?*tVA9O%Pk;ND_rCrkOOIUlrq`eI;uoHN z!KWz9b^--dzA+{g10e%4O2TXa z7z!uVjgYDDdQtI`=aHKc^-}-AkA3f=|2c2bkw?Au70-RixlcW0_0plCsNZv$mr%d8 z@LF>B)2qgDWQ_g%S1$a)*M8bszC2U!vqpv{IvM(NuYd3V`_LPn)$1ps!puftnSJIC zT{iM?x&nsT_Z=hEMr+0LuYBf52OYBJZU1m)-v_*L4m=Llw9EW(%YNs_7hm(Ui-0Jc za|Ilr7@9z{k%n-p^B9@D_pW>Iz5dovLJ2$s+4b+A|F@@|c6cuSs4xS`5cz2FWMnNd zmALt;G`&cPPx6^_sn^mCV^eNVCSod`wYnd@pI4KAm*=_*x0*^H34(W*Yn(sv~kg){08tO!{Ia|+CUxTg9#e`Dc1mro&> zdU&D6btpKdetvcUx2ZK#qX??hrla-W*B(yuDFPH#!Uh?!PNN;b(!Qf_@JW>LPH0`>D)%OHCdbljhh@<|ps z)^arl)>c-PIAYBy3dckWGE53Q`0zHjdvCOSh4TsLaz<4= ze75AkRYx2+-c3>yYHEmtfZuQ z^6hV3G`De6W7RU}64&XI1|vKOLzRp&QjluYY_2}=(e)3$@^v5n$qzpF;Y_rLn{cyx$HN77_Yyf;i>>r$`GLk$DKd0|H8ByPku;`4v==MTOA%eyyh7+bl- zSx%Bvy1s}L0tc-CHu*ob~YsKl!ce|9HvJilh2z$9Ga@RfK~}s$fxaj+GP!Ln~IL zvvVK+=zq3HM&AAUXY~T8EgV21W@ba&&HM$I-1eULeP!yA4ee#iWZ|TDF3B>eFC8PWSI;#>>V3+K6>wZSZ__H z;UKUYqc%yizj@wM#z%(}=K)nOUUjJ5ym!m)SO3!&Zu{faBTJW2b0|yu-6RW1{(uW{ z!p^Zmh=khB(X|I|+wjP1-uTg(uYLISFFIAcq^VQXko@JHwoL6%UA48%H)rZ@4t+tMpCO_nHad)ut}hLYpv&cdnNv z-m=diFkl1a!uj!v$f-!*dxj$zLr_gq7pmzz~$wgH60>vCek? z>w_k9n4>ceju9T{<%V`Fyg*hLW;Rxre6%a<%} zN7CsgY2v_?7jzI`FMgMU1&WrSV-QIN9ISXU5xpaEA@0zO^NZ^q5p6x;j!*yFqY?UVmA+K!hl969Qkqt1QN88=MgMpYwgmabko+K%SBvp!Q6Xg0QT`4XS^K}G0DAOsI1vuOg>h}lyp3Tmwy0}o*~ zlFJH&Xz;L&NV0xHfTw;xW@@+DQn-p#DKuyr8fvyI>YrvShIgV~ybBo4ur{IVXgJTi zY{~fFzUb-u_Dvsh@T$d&CRQw4ylCO*#MtoINPA?sHQa6uwHobK6c?>e`hEGy*S^{u zUa;hV70-XsQ{MH~zkOuW;{b@&kY@-C&(8InAhOZ;XyiRa1z>A&*;TjCZr?}jsNp6L zSU$UDThi$Sv98{Yj<^5$6X$Qd@kSaRW>To}WEDgd!LVt>Y|b9$);5y)II=K#8Y=VN z7#$mm6i|>g=HjdFyx>=V&61voCRyTK1{JTNExJSzC!M)dPCE9TZ+g*+#pB(6VvG%D z+R;ZIxMuaThwoUA_KyIzI^tCU4@!!7i)Wg;+X*c42nv!V9 zI6@_kjp3M>Jn;1&T=tnyeP?L#B-q8?dBa|~+29wyI# zD2s6FRHV@81Q2mMwxeyZ1>4v(ldX1id@ST1$QWY7hDl)969L|N?+b@G&{+v(`2>Y} zRj_KztY8fxPG(^)N|zR{dF4Zu-gpt2s$7kdsS3GVD2(8@$_0rj> zTD;cIot2zI$WfT3QCfIWgZ@}`0~G@(jHBv?0oSQM^THq2!o8yEdm1Q%R=>V}`+##b z;INf}l**ZVqEB^C5na)GaHIY%tuKaZEnYG(F-46M}QVOAg*n{f8qLg!= z=jIn9s9ECG5>z1q06O1EFTWeG;pet=rND&iXbAD&l$jE*3e)@{3e$! zP+JDn>Nk{(KvXST*#kstkErB26~zcEN>qc+K}RJMW3WqYIq(`ruMi^nde@H$AsiwK zcNR7BiwH}&2Gv@ZH=#oI)kAf~I#r2O%7;?CA|Fq=n+B*~v1$3pl7iKOV~9!bK^)(T;oGk=YIIbHJsYPn*;Z7iisC2p!Wg-I4}B-P$nZIfaE?LZR0fZqp+(I#nir!QXvDg&@xtwYM#r$aC_ZUDvmH^L+HuEu?`Qx+@X`%!| zFZInP9eU_;Gun3DIT|vM3~&&rw_t4DvhDVJA2@9F30am}W1wm*J-&1Az4vXTMq2@e z1xVFfYeyGP%uaW+Uc%OZJSm%~Ju~NSyz_yVKmQaaB{IZhEq4=t``r&9ONEU(50a3t zb}loLoqGJyP3WG)$5ur_5LIo)X3LJLe}3OruDsw!!>bPw->0Of-aCmTRD{=X)?byu z#==CQgQuj%WeK&~&DPMDKK)&4G(Y{`my%(h3B-qeSsa_q$r#qEXs)HyZ2#%POGr{4 z8_AM61Y1;%=F!A}MYDmvtg61(T)I;CY=7(9K6$`}Up(pXWxd34=pR{}Rum{SA8$BS zt6GmV-8=PrH%^)ANVeS@@h)ai@f)L(w@y$3BBneL`W1*%}LmI`SC1&eSZ4S-1mrrj6&VN_4#d>0pA%)I$Y^#yE(Jy`RJLf#(q_rzYXM5hVh``u@ zZo)7Abk`f+^qD41>!5X6GD~SMub+iT0a78&K*A6qB2a*ELP1VlGD~qBEnW8Y&-~9} zM;`j-mpyg1mxT&W1RxyY0jR(rQ!UYvZ|+bMhhu4%lwO;b>>gL`u!wTib&Nn z98_SMZtt#ZuD$!jb*n?klYk@-qA1H8x8mRb@uulLdraJ9BnlN_F;O}@+g!Tz$)`Ul zHq_08nZ1+IR(#*qsh7O&GY?#U!}!XTj@0*Nof?mrh{-vhWvT+UFbxoqdgBw1{qO$L=?wZ^z!MZOE_32j z0Bo$Wk+M;N@AcJ~XvxIvqZ?oQ_Rn7Wvo9QRz(lX_EDJ%Ihzv(@blv@1-txB3H=r`P zus_!)id6$3LMYUX3`sqOiqi0y?PXgZ*nrlsilBlUptNQRol@$JJPTcbgE_LiYr6a1 z4}Dv+IE|y`(pA%s?R?8C|Mjw0J^N!HdEJu^T|t12qFr;zfB)cjAAR4~(ck=}qYr=A zo6q&$Zr?N4o9zRJLX#Y)L&NPxWQ{e-!g;5?RpeU-cgkS@MLf1xGzP#D% zj5S)r-b=rUvC$ELR0KqO_wJpY-iLH*R-BC*qH|kz?CteBpZ(zLSFB$BxzBw6@q2E5 z=HI;J%#)AEGRH<+#_rlZ{mj#k`tNUk>`ia^r9IoZZ~Oa_l}uPx?rrC zrG>r6oZ}Fn&dU)8FaGRj-r;~|BkJ|CEOp*_5vM95O3Xxv<9PY1mEZm0Z@&JC|8%Lp z|Hhl45!{yh?>zZ?KmUgP;F}+OaO0r|F4;FV`L<=TbS&|Yln1vXGOmx+<$ydJa91h@TW$)b3a8r!E;QULw`({kLMIs;w zSiD3rKlN{(@#K?_?$7R-nVFlJ?i{>&(O6rZq~Y-}=Gj-2}G9 z;whW!X2TOFpKxdtnQkvxFy8*{Rd;;vKfdpxA!~@lDH*^hk&;DT5{C@Y5vxXWkZ34k zrnF!rGOSl#b?u&=`zVUwGt#1?ZV;RbMZ|_TJiLLDeM^@u5~0x3tMoNOWLs37q5<4? z_om;RcM&8*+<;_x;xvz{2pTPP9=9I-S3i##5{?HNN}WIKpp|P5Ui0uB>q(6I%(mJ( zy?g!Lk90C=L=h;VyuL^AA2@`MRW-JD#^=D^}ebSnzo_Y+0e%#&% z)!S@DN#=j_^FKa#+x=q)uE|u!4me=vqg&tg*3WveSWxb{P_r4|{qXjW zeCR8~?MSKL9z}C#WY>N7eC7iW{rKlkddHhy{E`=*x?;&hmSy5S0brWzrZfFC%jA-4 z?t1t8{;Sz&3n{B3Qo}~m{;^Lx>!a`ghkG7)%(D?u=hbDBWiIKbNtU&T+6Nze=#TmTL998Y(IG4J$_vmBU$o z;C`bh?jN|Oa#hRk_Q!7xKAZ-Bsa1uj3QbiTa_NZGeCOJ|g`KAJXhmzMrS z@yg0gEY)-|F%&jU4#FuTv-8|oDj$x4-xu$z#Y`&ivsl>bu0j91d`l7G;L<6lVrF%q zU;cOuK8=8~^w8k)Q29q0tk7JhRI@2wSA-VIxJmI>ku4ZFwpaQ8wc%*7o$5pyj5E{& zDJ2URQHOyoQse*f?`nF*&dU!5mKJ?&|J8Z9{JU6^@V;81Ev-%)mQccienezzgQ`z& zaLmOntQfBl0|l$q;>EZCu+JT6#NS&PA$taDz~_ zD+VZD#heOODEIQfb`SS!5#-x%T;*R>%V!sqt_D$$4ijkJuA8^H>z_(kqJ>}n)PEni_JDsn_tcrb zz>5;WD@;zvLUx57d4jw>xHnA(H^ip|FPTT0=7cs38?$Xlp-zZlSgAj47BAiN$U`6f z%#VNk^>>U9#c7g&a&tEvd=BSJp-59V-nKWd-+Iq=w}1>AhA5a-y^Z6}&fRA|{h8|y zUEb;WBUeoxania6?_TdxOEzLSHcFDVCPweS`|exse)!nKR$HUu!3OdK*+E6#!^=Uv zs?QHMIUtVv{r>m9c~Lq&tCN#i<^z%}vfi51Sn3OD6b-dv035|oEF5cIbj4kt`N-Fk zshMc`^6u;ulZMa`v(Yro`h9o?4A^@J*r64hs0D~&CEpYHp~a(j{N<((eCkKv|H7Lr zYufjbr4n1?a`_NWa|i%Yp>}h+mrmbwThwf5tkyd*khBj~m*B$)sBiBrJx(SGbMl=Drrcm-k#%Bpy!*}1m>COi? zPVJkec0;m`F+vc)h2&Y*G($siC0W%M$9YOmQHk>MnqIh{X4>tg$`B!AwSs0SV3Yvo z)Fo-K(^%j=lC11DWkTdDkFVuwO!LY@@J>8=&pI;H+&P#2`{#ae>z}R~IpnaUvk#I2 z5LlmO+UbCeH4S3QGUxjV8rC#ogmZ|L*wMz$`|o?#N56T=cR%2r^D5z_dAr@(y0`P* zkNq&)x;0+49NpO{X7TKm0VcJQ_K~Gw<03K~HI#rxY@4ymq|gEy+B8=dXHEAN9HlA} z>amkiuk?}6U%2t^+v3G5CGBbE&DhAu_~@^H_S?H2-uTtezVm6PuA7>kd+(Qj`qR&U zueIy|WaD4`)-PWCyfY76y=ZTzo6hyXEMSK5-Y;9cAdaJuf>ZB{7+irI4M($~st$mv zLm~Q%A!`j2IiWSnC%^RNfA#8_)F)Z5o3&ybeBe@M2C!<`=t*ZDoo4;G5y#DDcdmQ< z3F}5jTKzP8+dn+x;FXKM`_n7m{*Ny>c+FxjzTIvzQLEkP_L8R@d*GM9`0ls8^NX7v z-neLd^x#8RyyCU*co8J35zC%pLWvl|EiGzr?7-OAK8A_ z^*69g#ly=CM~FuJo}ok5ty@0UJaFaYDTgiwaM{&2Z@B*+>v{}s+N86u`-882XYY=k zpZoMXPdR+qJR`*^GA9c1PEI*y^_lPTW`3-TTc67`{;FgiBN9u@#;D}YhFpVS~A08jG zHgp&Y7J^AKS+QW`kaY)L@vBI@f{n0M&?EP6*tmK3Q3ovsGg(vM>nX29$_8WtNb3Ik zHa+yv*2eIdifSf+U3bEh4qLM@bI!2w-m6kL;(p;}H=XzGUyUtYsNy(FQck0Z(Z^suW5G~E;w|^z z{@%CV{l`B&=k4!!`IC=5kR$5_r1H7%{NXQuxqWtS?w(ukZZb<8t9PJC)sQGmd-u*= zXTRV*d-qK_;eMJ*=G6(Z6wV>*BhAogQ+o`Qs(7y=P|ap_WFpzL@s0od#b5vA-w#+m z(dlOv!Z{s4$Xm&hL1dYf#2IKq5f@=Kgd!OrZ@{6ucVww=bA;F8iIEddI&|OETyuCx za$1M1aA?p4Q76GBR!>FyPiR#MSqM-Dv1SfYbMc~a1z;4!QaI|x-+2_I{9(|~3i}&X zypB)=6^fI}CDN-31u05@e$XeY{Z$y!^PQTyQu^OMrEgW-Pt zLU=4+#{3|CHOBhGf?<}siIsnsZwjqFwsfWEJ)ssH7?^y`Q7&AC8YXo=BILQ-+L^Ze zt0MD?;y`9?Kta}Nc~ZH2XU)y8(XqC&730*%Q263?>eL6kpKqQ^ig|RXT^a^ci4hB$a3_kCk`tb6r$^*RBmdImmT49@th0DiL;2tVJ ze8A@)^!95c&!4MJ#r?J58UR>D_2weO8SbOP)+@kpTHQy0YEkM6wP@{NaH6sz z2c$>!e|bUuykUXND>$+WX$-`-Yf-@>C{eMD4v>|~__>xxdBYQ^oMEwArMZ$Tta6b` zt||yP537~C6~&;qaKmz&HJ1fN9JqAbihP9@+pw5l?Nh8=7-IcQZcecR3z@&Ngy#(s zvUqHLH>eiVufMDmSvkwr@2_@1E!0~(Lt9d$7NFek&Uun3A61#rz`SnXJSI8%+2=jzx?|*&UwnQ zOGixK7h6mLzHJRr z=wYHMnBo>)^Sdj*cgAsVd)aAe;=ruT8j4PP4{{2hGrJOzfvSl?&2g1Xed_ut9O5{# zk@ZSRKk-IvyDg+pB@QSdB~@nXWcc&dH|>~x?L>pSiH|J-5a;8FU;W}!uDR{ppML#k ztpy8}2%fSu@rfU8w znEI5P!#Cb=%cI+;jy`m$6!s$s&Ph8q_dT}vzB}#%DU)N(Bwz`hgE23B{!_-shMgju zuW3Z~W8b*+_A9S5BO@6A=K)I`t9R1xFI_fq?5R&$d-x%ZD4Lp{z3+j|cinVHcdlca zvA`&zq?CHyul^`AgzL6J12wKgf)!MUtE3t57z@jeerzyMu z0%MS5kS3WTFb4rSEzkKV3i{<_YBb^{3x@mbOtaNb`aNHUS@HvFD$ zK6Pn-Y&eJh7y>wdT;5Sm0fk5|BE3j#jq~!u3$OX*dB2!gv$CH|F;WG27}w9xcLyK7 zX5E=5A9Ub>5%KPQQ@7oG-~G4TlJ>Hw6&b+5fyG;U>t)yc__xn^-3yN2Gu@3Xm`Ox0 zy6oobfBC!NHA{Ve&aiq38rMJ}-6U?B1q&D2M$4!Dxw+2FRF~SywyBaak%rEfS|BWI zsaDiXqB7=4q~Szd>SXWkU1N*Kre@}%rXizneNQyH^57l!Z+`ys|LLzc{_2ve?)d3v zzZNe)GE?EWx#f$fB5n+;52g~ygSuN#rM7F^Pm0XGoSX(}!{2@5d%t_>3!eSF-{9*`@7?1(NlFDA~KK zIX3ak=bg1+a`=(Q_l2UVhaPh1KmNbheeplO=X-N(5x1L3M!)^pZ^sr*{QEm!+Uuue zj3M=+nUiG;Cl)Li7Owy$NjBY2nP6ClTa9j}Kl;_R?|9EwV(+!tgrp#(-L6M79v+s| z-}w70U-p}ePk;8A?|IwboqYV^E0!+s&?Lzqq@qw~i2$8?Y>UsEJ3uvqIAhhe3?mD@ zH9+izwRD7X5y98r^1$o=?yW!+?9Bqy;|Dwb1@nY~G_Sn9WbNbk{0K_R7LJcDn%K8% zmqZbWp`@fk%NLU2aC9$3H;Bl&EX%yW5Vzdyj?FFc(w>Mju$uJ^IVH~|AXP9T@>$|j0U06+XQhpyG!v`( zq$3V&PLB5HdS+;d)RT$VZ`l0chR2UOXmRLJ3BELBA;%okL z$1i^Tu`^FzH`h;c<4|dEK@0bcV3RRXWSf7I{b_y@vc`rVku`WS)OcZFd(IXYGdKl9(W~jkk2O=Ad zi~_PL7_v2rT1I(ESj=k!O5ouCaE1mvx*`RNqPCXR z9j5Sg<+Y;>jVk`v9sJteHNUuqc2RL9)x6>XM4`C2bf2S5UDcp^OcgU$vL}l&3JQr?wIt~Ix|4-U{mxW)PVhsVP*Rj^bO4@~#Y9q1$(Tav@QPVnbQNq8qQD$rES&2~f zN+ID2CW{}IP+z3+6gQT&tCfg>SFP|U4=T#4y^4`mAM~|pQOdlp%)ZCeE!XU-RSKkru*HE+eWB(Hp1sJ3uYITW zy2OqF4b)y2`HL25U@-$j1FIoa8xVvNgf6}7 z@Na&9*>x9Q*6rogun-2+OFY^(Gi>3%^}~ywb<(~g!)Z4c1-yg~5fXvIc^zulZ5?;n z#aHy4OUF|t2PO|gHg)Z8bM-z?Kl$L{hW2usIXUZ@M}KYYez#qFCs7Kk1P9gJ5ymGk zx%|d^9(vJf$E`({=OG+Y6d`` zt(Hw2^V^@m@X*Nc5P;MWGoSaX%dfiZT01r7ea3K1PKkw6Kby6zdCqIk{=j?Rc*5Eh zX+iP6{gIvj@r_^n@<+ev_MC0C00*)@ceXXWb?eNzKm5Z>pMA{oiROH!hFL*5kUI~T zLtOBSgX(AU1V<3>z07Azmn}Kuq~ni2?ue;nQ?u>vo$J!z(S zFKty)N*PR@`P0rk;Zq-f$I}nl4L~^a)dOGp{zYH>?6_4_SP?y#dH;$3hOvzc8dKKG=L zeCW;1MA=xw?7ho6+3J)_P2g?{_9_OY^!Mq z7;dFcY?=Pbcg{CyntPDABD0iHKs)Wd_h0?ex4itUGmf4dADW-fZoT_~pZ)ZLn=ifE zi-iSwAqrZ2X@GaZtICYLpGP(3WzjEF?-|)OAHqYk0j+n$L8E=4O-~Dz^6Dwiy zR4r)P03-z85kL?X2ZZFBUc8D(joql-ZsxsYUM5F}e*XI#zVo9$sW!$Z#`3(~%f0tb zk>^>Kck^DJIq^)K*hboFfMVEhqtQ$oDH}F%f=M`0!X)YJbZ5=pdr$9b|MmO+?bMSG ze&5?)G-BbrLe#4Ol#4bJv$L0d=0DH-(?vH9kB)lR^FDXZ`zT#Sbdn7b4G%ST?%Mg6 zdmb=DQyC1rR|e9Q*_`)_ORv50p1Jv6=A=D8zt^72tZlT?vF$rL$;gn;do(m+nxp6b z@Pa>Gbi+_9RdEcRYtKXRj~{yTd*6C?N1SCQ($sRA+GZo&G1LCqcQ5+KPyY9Kn!52( zpY;Sr@@#ad2}-8tJB^Vv-DAr7v74^D>801+_^juh`st6o<za zg{kQ|?RArtdrXPIt1eqI`pQ>4{~KTUMo&$Wz$Au?9-(arQ*yMVt|MZ5rc_2q?0p% zp}@%tTtz^kqTuL|Yw3nli0V}japB0<%y|InM9Fn~{;;)seB-BI{-5vs?zYRXou8Zb ziAkCkWK>17uApW2*h{zV{LDZ9*U6_GyL!bK!JKr$;dfqsYqDe{@6NIFg*#oc8pSSI z!b}nP(ion=ssbfAKp1M^15Ztb1RzL4f_z77#fo2i?|X+HvH$zu{OoYZaaf4G>XC=; z|JVtK8zVNcL~!0b`pA}E*N3G&QaEy#j*hH8@<0P6PpL__3~zz4PPAz`t%riwUcq}% zHKGP66iyt*gP)0e(73kzDC4@EL`c-d>v@GcRq7UAvI~m${Sy@JzIwxrCI9ikj zY?%RcWHFDX7#!T^{S=;8Y)t8@=z{yI?+YuVc3s6zq5h{2dVO^VHm;miP?9dJd098A z@RZU=?`!w{*MfFdC{@22O?)#)RuCx@tq9CkA3711CNUOZAK;xbQ zAzJ)Z>z6(VK7hW}2?KSRSH05Lh6PF@&gMc-7CR*I%Z0F0*$?IBPuV8}VU_h~P~W|^ z`t(%}#pXd@pVcUS#d#To1&=C+e>G7sz|unyUq2CgV0tXl`6aouK4B}Yw7voqx70>U zMO*G)ETWp}uY{t1pha*s4lAw`IKp$vt>~(d8g%>AV~mOgyuO$XjE#D))k>?hMUV;! zsTgRUi%~s%?OtQ6R?TkR++31EG75vsFqn2e`Q)RIZ9QVY<@4QMBf;jaGwbhvgolS1 zo(;i}I3b(tv)}6FEj{3X-7qw)89a=ts1k^$r{{0D^{&^x;8^cHf#))R{Y`hGJx|L< zIqw2ckQbXfZID0i@I$Ps6C?yxHALM!`~I)4oV)Ly#;O(MJ4B>lfl;Ksn;(DXGk^bs ze_J+Y@@}U+ug)vTG@IsSXB>3MntwRsMSs8bFWY&@79)?;d)FBM?vMZQ(RaOYteIp^ z6y&@*h4Z57NF6jM)xg;zI6z62viX<1uzn1rER3^CR`r_u3?FTU!oM_zi;VY`VUnvfbD1}1ptrpAZPyX5xA*FQv-4J)9E zL`ccC17ZnB>^1i7&%JHs@{#}e$6_JVds4t_Bm+Zx4+}%fAgl(Q$R($%etBO zUWjSm-N*jvT`xFjucu#xbcp=?|C${wNdu-$0tHx(~`FJxe zR?wPJM+}ZTt-W5hV(G})r>+H*iT509{o+S|+S1Etj7gQ@tx2-n?Xhy=%wtv~1bEl9 zr-~;w-o-GrAbo|#!>I$##FVDClgpp3y#0}Nca5x=)Lxr4Oq1M*y!x+R`~5GyXJ&4G z>&`hP6OQJa9BF*^Bd^Qy&bL4J?bdEf$@dho){oqJ`}Mcpf9kROwmXi9Zocil`)|4< zotTik!zi*{613ah=a7AW@W20Z%G$}mJp|d4_dojW=N$d+kA3$CU;m*s? zu$HKyvv*F7yfMpOgeIH_Yh0$hh#@oI%h%ns?*8kqHDfD%Hjl7LBc*r?k5~oj0OsK# zO;hzAlt4hX(Vok$|NW(afSX_fr#EC>z+rTXS`%6B2rY;QGmG2#@P?g+m=%_Mo(~T- zIz1N}NJQ4yp)}0|>()Q<&tE+6H$V8@khh+0g0@h$Uh(JA~X9640Q0|$y z=hbt~)DI^KGgBsD=Rsa_=gAZM7%33g=Hdy;?7>did$O+AoSkcX5VOgh{^JMEx$lvO zZn^Bbnc4Z=d4NEc$iO5}3swUkGQN5BBHWVsm%ZcFXFu;aRcUwJ)M)yTAAiRiU-hAx zu1gzMRUr~GniZT-DJ-|jEinw|wbe+!_v>pPTz5Y+0Y1QO090cTA}NXFWRx{WW7xEJ ze&<`~Kkb+``>vXpn{Sh00ldT`o;DJ;wjl8Zt)hWK#^eK$ z(*UZn-`>0Lz0clv-?HA=)b+YTEHks$Tyw|P*PJ;t)Kr)N%*6y9Y!w_hw%1;B`+axb zmy8U1@tQksq(`50^`|;J|hb8wnR+DVL!pPwnUc55(iu^Oawz@K^`S7Asnh+ zxJg?0A*w(KK7dmVk*os1iKsxlE*YlRKIg#apYiwK`}wv1`#(Q__}07fxs02}Hmqu{ zs=hskrvKmv-+lT?`}cZzE3td;vBGCvb`64%G0@V=j+N2eOF^Uv=V+;ll&n4R1h0W; z>?s<4qyQh{pn1=)^K|@jX zKK>B~W|=D8P>7&Kpd=C#15C)As`8$@FC86ecnRoe=^{rz3aBy;Awp}BUQoHgD3oLc z$CMwHslQNoi&*f|vW#jK?WZ+`FA5K`bQ~ADZIzQx1*Q~ysk*aOEG$$^vGP^8!WG2O z@0Zqog%(w`G#&I#D|oMPvnv5rk&XxmTHz%Q)TNYNC?k|O$O8;lQOyUWTD|*Pt^-_@ zQbkkU@he@=uGjPg_fGXZMM{lEUQa8ID#-plcB0VYZYDC zn7~)+ACZ-)Z5@>>hws9w#S1Fjp(voNN;SQUNfT60K^&m^lS`a+G086a8pJZo5=%g6 zpwU#LL}~$6l~o?BP@IZkU{Y~G)C?NU_y4ng#SBn@Xh5}5)!+pn3i>`Nr%v^zWZD(` z7GXcH9Cl$$mt!DGzQlDG(T>H;;5hXv9Z)Q`eYcbqD-r&MTQdv)tlF*xE4aVa1L7=U z-@%Fh)a94t(z?p2U-wi!HppZFsu5AHEI}Sm-RUJs5G9FlF^*pC`l6nd+rHqEdQbHs zz4g`%=z$ugw7OCQ(j-C^uX;-&08(vV6H#?T-BboKO1 z$F!P61SA5eNz_|#>)FqG+R};P%%hP6>k}_{&dGoH*(Dn`?@U@v3Uf%kXC~wSc)`^# zJ?GSYRxg{+a>K?I8HK`& zZrwS*bbR>DFMsAWSFOAL(p$!NTjIJM0jcENNOx|TzU-P?-|_O3jY`jf6R5!X;LLld zE0JvlDU6?OA9l>zAAkGb4%vHjhn(Tp+}wo(DOxr5wQWl=y*M7kEK8P+Q%-vX5GuqI%VyiOL|#8lMAV`iD#-QVXl*d z@)w+b)Tz%p>5}uVG)qTRGa?_?_%z9O%{;kb>z-4q#ghqM4wRW7(FRAM80xQb<{Bcy ztReA@ib6846HR*V=_h>a_B&J?AO;j*exGZk+41M}Pj2#+QEj<8K%r zvfW;mHq#_c1!@h6O78r-U-!I=etZ4(SKZTS4uQbP8beL@#FMw(y#9r!9c~Thd5HfG zekcQA5|FTZVN2bfRlNIe-~E}tdvzntI&(AKSyI8u5wpjUyZ6Y<_wsQdiFVrENSYud%+{E+VY~!FhX4-h zB1j}YoZCv)Hk-`{p4@)nMc0ypGsbj0n29)Vch($y(8oUTy4kt8ZckdN36yp)+U@Sd zaO(qaeeq9!b%o!u&5R7G%RmVlI=ge`PgmXg%o7hHBeR|Cf-7&M`5a3P~vJ9Emj&v?~2CuRcY+(+O2!e9O3_fOvU zkR4W{%(o_feC{8HC#Q1LQoxHrt~9n}kJT%qUkHR;`PFI@QYA8CW&nw)&4ZT#4Qwka zA_Srq6Iv7KUjNGRGhTHUtX}N9ZcR(EGyV^wZ`oDVlJ6?8N*STivLx20OPtVQl7#U6_%X?i0nk$z~ z&CGSTY~GL#&8BXNs6Z(2S0lYB%r6+gM&5untZ3St>$Dv)8>2*Im;)|g6Ap%^D0xbp zq}u7g=YT=3fI>XT^H8b@vwC3?uTCWNm{S=YZC?13Pkj5=*Ztwb8y|S!;n~fb^O<=s zDw255B0Y#_W7wKTYDQNsecfx${`+^I1D|)>9iuco+db#BgV!9r_U0>YL$m2z#$G5* z00Z&@)90Fj0JYYLXw&LkE>_f5|+4Ce*RLga~2AmO4I86I7>=M?fTH7x0vct0~c=Uir!(svCx^g#eK zh*U&GrQ7YT7;Ejjc3&9Vo}1U)k2Y!9o~uU3hv(<#myNcpA%KYtQQ)Ro@7WsL?&Y`M zaz~SPYToE&8Fo$Ye&9Z5oP4aRx}Kz|&k%D#kcttR?)1FK$p=n6?Z4jnle13x!WX`M z_s#2ixwX_IC(nNV883R);UbbG1{Cxy$0Ld(4~~HZoPv84z5*y<-^?Vw*iC@MWFYlj z?P6sM+7%uuq5T?i-KrYWJooKRW)1)Ct50~*nMZx;tH1l@dB556*kc(}(oEDR*fn?5 zvrqcuhhK+o+k!3eUMrU%HR45Gun-`^@QBcPSSTX>NDUDO%nd{=M9iEJDHGX-O&X1n zr4x^=f5>H?4LjeuzDd<_0pSdr-rOC&Vrpg@4Vk^+dOSr zty0^G&Y!&{KS_I`Nl^M-gjm0uyV z_o6$dXV>5JkZlc#&k3R+Rihs8GQQUyM;*3bJ9A{MhW*FHtbcUdrn~M-#>U_s$O5dK zh_@`A=fC2tBle%_=5DxYRWu}+0*f_BVU%$8Gmrk%npN8$*+eM<>k3MwPQCu|(wkoM z{3BhYOQ=Y}lu;o#Rv|bAJSAw)H&?Ct(kI{Pd-+_h&89KIXd!AC^m63Ref&MIzUog` zZ`(YVqzMHBQ~^;nUR1BY`R=#8>gj2-AtGFmN|Z=MN1MrCwr4k7yH3F-O$$~Ck!`y9 z`Q~1GA9M6x40sU^)UP~$#_{{FUbA|`6WhsqrH}|uCC|+0vg@wB_4d0T*?;e4fecYt zWLj>_0`MZGA_gfVY-JOtP4wm)!^vkp{r-2n^n|%iClfM6tN_$Ir%)mqO06i?-FAOx z=dQ73qaqm*LkYH-Z{M-vfc;;7_GvAauF}|00zgFpwFYToRI&da%b#`X;n!bwGdV?2 zr6R*RMZh@8ONpomQFx zB4VNBROd5)+KFq9efEhr{qAzRav6M&C?}{YlR`!2=H>wSKwtz2kl;8GwVj-F=w44d zbT98^cD`o~H4~Nq4h7hRm=YCL5T0@3!IzwO6^IE8vP42+h>d35?K|g*BTWnx<6M+s zl*txqwV2-3q7x=6tAa$JPD1({Qq3~=>X)AJlkfhl-Obb{AXWrmZlv?Xh8zEU*Fk%& zumlBpmEmUNj~CoHw_yux0%+h9QIhticdb6`uw#zePYL4Hc|Y7}UcdhF4fj0)QbNw+ zR19phvt!F^-+9*lD<_yk{1Sx&BhVzj@t*UbT@Vn|I7!fBVDQY>0}9T~CnQy6lXz&V1oX`?T9V%cKB_vNgtV z*J+F8iywW<$(LT|b1!T)BumIQ0VHD^!kAU5Jid@t9WI0dn44$2e^H6TD0&PgC7DO9`?h=me%y)5@Gz`@3FC-dL^#TB3Z z!uOtdaPv^Z4zF73@(x%$$j^6E&R_oKm;T#_Uo|t|R+YD${j{m^Prm(qpV{^3MwcX* znLhT^lRo{4ca5-r)9asq>?tQN8E@GnvCKYq;++Isl$11_d;%uJ%${KKoRjo~qxSPa zk|az4s39Og0~}60RtWigYM<4s_g?N51Y-$oAON;NNx!ap*Uh&}XO684yecU&mTlTL zed!hVj86>jzI5UpuR80=P21a+HR4n6AdbywW9YjzhktTCyd-u#j`z4(?p?z;2Fd#GVFwT8Rj{onIW zJO04K4_ZAv+aqR)C4&G?lbtAdh^pNaq3;}JtY1MM~#8PIS)ZCQ9%nUyuyHr294gdH19G= z@}SDJ%pX^IwY8w7I$RLR5QF|faYfB7>i7M_qZNh2U435N(vR3_ZERG4(*pl#f$Lqk zDy6Sflu|l4{m-u#5L#d0I(^PoNr9_lKK)Xt*Sx%9u(7(xr_?8^8s3D0wKh=pU=Ktm zy6!P9>S$8`?>_&LN}%bfz3YV~{a-H~c>DijOVx>6RDUx7Mo@pJ4S)#s$5IJP)dp98 zhZnZOh zG=gk(i7U!Asp!O-KC6#60*aNkxS5C|`M4rtR#;MnhXpgWnq3i_)nZC5HiHZLai6JO zyQb`h+CBYmP)rV8AhFg+eq7U`9qIx z?h1xm=JDR>_LjLE9cA=q=jy$?BXm8vdVGJ5n8`(1bb6->&=EIgAq>!-JF z{qt?>k6ycKuIHcFG(EFnOS9F0^oRfmu@xr-I{JviS578dr+wNmqDVN&MQ^<00qS*V z=@dzu$O1%U)XCVy$m?GEET>4U4L+e=AmF9r&1XO3m>=DBUYZVx#1y|!QR3U~xTli~ zMLRM0hhfNif-rf87bUgq=6AjF9k1PMs?qbBG65vSAQ%Fx4qMG~Ico1Eho5ld<-fiR zLPQ?wIe@SN*yNu3AD!!Tmkza{zSGtwWn+;|*#xN19iiq#^T-r#U*xr8h z;SCUhF>rwo!-mN9vZIeV^n~LMSHjrn5HFabx%Uav!G|A2W8HlzTdHjzT`P4bu!nmhL}YeCNP4yeHq2+8438(JDBdTr>u)+XD*g4l#=M%$2t;E_mro4vSd@AZf1~>)w-ES|1 zG(wbsLoiLqj?KNy87M)RSd~=i=YPCu)23~$W|}012!=UE8Y&QZ%wcy6x%cx~cJ^~l zS~WH7dd`ai)XVj0NA0uUv4`Jz#f`3+8WJTUB{hbOz37TtUh#rco6Uyvl0YrfRoC1+ z-xZ8C0jJ6eL#E++J6`hK(^gE5i&th=?;4HdraK>)-Z4Xlpk5_c1-Tv+#~pv@@K`Iv zdW%L5wJ1C^%Y6zQb<~>C(dMpQ^UYS1y*HYriGAp?jXS4jR<9V3hIKLYN?U@Hw6|^N z#M*;j|C%$GjW?%fJIzKSL>?^6U}K<6umk|i1|f)!4a9&Dla8C14qLP`hgC%L43fuq zAXTUV6%jokV&<^gGC?VDyu=G6SFu12DB`Wc7`*SnC)=C0r{kmEIgbQHdDa~n z8ae8yLltN?n`u(zm{mX&UUg)2=(yt#{^@_8@6q(CLQG7sv|;1cT{GE|@y6EeGY>zq z(YA(E6=VZU?L|gg>8oDwj1YNc04P}|HVh&*OorF)x$My6j=uH6n|ORe)iFE)%*5n5 z@SBLJq&Wm7tHmb){zD{bB-S|Zr)N87pSth(S3i8|wRb-KC3AyEa$B;L8h*6zP#qy>mEY*>lQU{Wqp?*_qhT#PwLDNgNTkd)R% zVKS+B4<{_Dio}bk^N66Hpdhb-r84h8VYEc92Na^P2^(9Oqylwb^1xcvX!B8EKLP56 z6+NW0=Gl6u?0PaWHu`}NzwM*% zcy_zfYqeSc+P&=TXB>Cn1>b)08$P-5(Z^o=);Imfr{0=MPnBPB)@iTiXzXiYzZ99z zq9MiMXTPrQc?m~wGDo{OgtG!MgQ}74JKz1xj7kKEV6A~h`26{&9o_iPe?0QA1CBUw zw_^Bv07+^hL>5oCW*|ls8;4M$+d!Cm=Dfw{~a zX>_q}g?(Ra<0B#q1u!QX&d1Pjig=O1vqJ2`okB@Y2A0!AT;AJ0J$u4|Q@{M*kACIH zC;!X8exvi`w)cJTohKf(y4}fB3lRvga_QL1Uv=j0D<=*=_<*HTW6d;a4K+rGTBF0w z)EZ+=n%LnXO8-CTgThPhoI*bLk8i!>w)cS^)o0ETA> z8;Sd7%s=?ipLR{}Oj-@1+=E3dV2C^cK5vh%UiQ83{O<50*Szm7XQpWqWClZ$deekD zfiQ%i;U|TY)@!*8>TXk1Zeeqa2!@lYTvg7h8aClD#(6yKd$D~ zRWRj(YSlg}T)DnFP>eh)L6D~^L(3cDt!0d4e{rZe(FHbF$fM@Gm&~Hl;#ugr4)$O%X zX+T>b$WYxUl%X)3K|6f03QLeJZ{fkT$puimZVA%patZ<5W5mkOJ)h1s# zU*gFHW%J9MwP;;6B7jPd94@!xkW@u9RNui%m%*!|->w38qBnm4wQcS5&PxT9; ztZJFPsOlT4)w&$I16e%FKCKVLK8;!19;rxsal#fFs2KiKOsq=x_Sbb_Ko6?r+Pn@A zSuEsqQ7H_}cOAH9@s)dk{2eFgQ$y^1%D2}21qxi?%oNMk*dE2eG^mtVf6brbiTyiN zgCo7{I_euHHQbuWK%4?tv)`(fd+omIFOMga}lPi)zquU?iswrR(@ zJ05H_8>%@WXVPv@j7g6=bic7wcg(qok@See_eCQ`ug5GT8N(+=X6EK^zWaf9;+ef( zcF(6LpQE_ z_^~Ip7>Koo$QY);!C+OW6Ol&3?RMtDc^+8;P$5;dv~APIopWw9W#_fvZ3G@fn6N^m z!5{^jQ%*P{u{O&ko{q(#FGDz}idR*A`bkG#|J#c*amEVN0~**joZ3yBHt%X@%cejo ztja`6ETGhoD1Lvzg;QObjnBdIyk{Q0bjgUza?QP}sET)918maa>MGbo;DIzQE+K=7h{fS!={~2Jh2r-UJ!Kx2ISK-kTqF@LDBJmE;@s(3c zC++-PmqM&;hg-iYOZY2QXs%PJsJ@C4p@44)Nwug#<*{L*0&wB1@_g;Oo_bstX ztZW>?^T_B`H{7{xS7*P`mUGf-*y*0EyY+q~33IAo52zu|z)O~&ar!Z1qm52GGln@Q zuvvfKqwV<~r^bsH&q9#Qq1m|Xy4wU$pXXkJeJ+ciJmu5_1ZsW$~swN_ZV`SYK3;xPyBo8`_h2l5gbVkRZTz}E^3FfpeN1gwMv z#<1a#le|i&5@Vx0)EDW@MAbrFfLP)v4ill)>*6J3JR=nZ4yAgi_;{89c6E9WJ@O>F zJuDelr=D1ZJSC3PyYAogi%agtftZ7hNP`<6_Ll@;=GA+wyu~>f zgLousz);u(WRB~WZ4nhxSTVuqi4#MWm~Coz&AF!?z1OM7?K9U2l_z5Ka#}Lv|K?q< z`OrH*p64)@NF1psv8W+3P=EB12aOChox(612FETl#0(@95>v%sPgWRG3h+U(Y1Kz4 z2=KXAg?fe<@jSh`$-M&+tpISP(YPVJQKJeHTf4Vj`tVUI!Fu7ACbkNBs zoOD*<8xsj%cwIbe#9(&mSQ1p8Fs;SX0e*VK9wEpz-3x}4ED{0pCF3+3I zv9n)x&S|F{`NQ8_r`|VGR`Ge3Igey$Kl6k`fAgEmX6HJiV?%~n$ug0# z(Qz`t{t#6`F7RH&`ED;C9%(%9*~g{Ks<4)=2_vrD)Fcj&YcydjG~K>sdfg*CvQC$Y ziC_(F-7z~m+kr_`y~Y$;fjth(wy|WHG{~5)A(eLi_?8Wuw$7#2I&qfB5R%joB@g~( z%kAqPI_ud-j|?|@&XYtFnwc}vsZoB#i%$80oBN0Nyz;V}?(F6{s1cD_QRhTNy!RrK zJ7T6JHDCVj#Sh!bZpQFz*9$BJijaAd0TSp;r#sv3b$Ypqig+fR zc*t^5Y}!6;h&iy+!6r@FCf=)tZ6T0C8Js3bH_P*E{#DOC@}*}UllAi1`7S`>#XFI} zV=LhyjY>7TC}re$RL)%NV5|D*R93w$UL_DSL%LXnN^vGk-D?2KO(`M)8Ji}Fo$h(g zyMOcQXB@Kk?&tmHiub+q1)W~b)(Qa@U5C}XkAMHa{<;Co%y)Y|=Ty8`0+{{iP|`?Q z)$iQi?&aP|#AythRx`1QHB9rJ-t?|EsTfkjY!fpz-1^!VKeTfBP zvN0w5meW!-H;Ql4z>rnRw4NS0zNtl{*Ke7o!78wkG`2 z+0bguE={E3RpH8&9jN_|O6dR%a;hqjqvqd6RbPXI$|pqpEI9prZWYRl2b{yg^(>>( z!b)GWSy93iHP^Bp7e{?})><9yg-T{x6kSh%Rj*L=^B41H2cJ+nY?WVHqka{SAMgSP z<=gjb!2?1=biwsN87(?cV(sqeIMkf<0;wn~Uk5wO>PDx(eyl>_|3@fG!+WTM6I|; zk5v+kJ5*FCzq*pD)B4K&XR3WypKa*pNJoxwU6_M!23313FlhBsf)!#OZXqf`ViY9k zLi1L0sy?slLL3xp1*R6=6Q8Mr6)y*AwR8*uRPnt2q;uy{vbh>)y`bX!x+V1vlx${0ChhgN1soa&JI9J(!4`VW@AnrY0Y`>+TzGd-#|m_i0*%YCIqp0I@L& zz!+l7A!jx~;Uo>F)OFplM;*NXK6`-380G+B5k+>HhWKUzFuiML*UUVaMSO{AdY@rz z{MKvlc-v(+7m3fQQWMZ%*}~;WnHnv!DRP7WDJBntUY^a)%@;$pR1snSM~H+%B%}z4 zhliRB@*;`hI3-c8l4=YkiG@x3*fgOuuIcc&sFmq1h%FKEBO7;IcBI`jA(sqN2Gv<;emMj-&NF${XP{;}t5>vs8h;UH1t)XOm`O0KGQJ;GeFYJ|6 zBXx_(r|b-QfV{JFoe`6yzKDEhz*<(|WzT=wH@>uD=dPU97EfFS}KG7Q5?LZmVpJ^n<)ThN2!9Sq%>uC_ zgnXAaT)E<@3d9#_<^oHO5IzKmXujQja^qG&)R5+?U=pHqsO|ZCU;D}TzWie_z$xJ3 za;6?2h=I$|XzWg*5pXwq8Gs47T}@rHAoB-z~T+Jg`L+!y{~pS_o4?RF!vAlfmT2^!<$Lzan{ zh8r~81Od5&_i#QJWaHz*?|AQ5&;9yu(#bIofp>s};8;=ka28`o!)_@P*wlRRZO^kz z>NTW-6Gbo2?|pRJSHJbUJ8!v#jhKYVhah6ByJm)_ratlMkNoZHPcKsC#n-j#9(~mv z_icLQ!QrW;dEWCv28qx76N|HyL3g!xXkdTI09|M8x`V*0~R ze{Y6{M_LIjSw6R|^ZoDt>^oolDqxEPd>l~zYk`yVzA^_mzyL{1A%_5Mht4hzuZ(jg z%)o4F?b?^T@Mp$#y%MRYN60l+DNh36LGefp-x&V-|DOAeAN;II299{N)kqU#ts&7?BNf$7r+4j*_kHk_-@oU&oBrb~A9>%~ zpU2in?n~>~vQfnc{^~_^9+-KL(;$ImYp+a9CN^aM<4p={hJIU;kjh#$joEw|K}S& zTf6_>tER>}ole5=M2@V&fIv*fGDB32VM>UYEr^ti0u;B4qV3;E%$@gb{GT6P`j-ux znb7HYry10#0EC3eFcG*l+?efVxBmHFOpN5YQ;>L%k>&%BKKZfF{AhBhxnsvp@13}w zsLxfHOlp(Fa5J&pZkD9U+uwNBInO^~`_7)R5oS}sffTO-Zk zAAb9v*X}tsJ=Z2St<);B#t2?e*xD8l;8m@)-~8e4H$T4FBtu~nAoOwNT{ElC^X9PG zG4sK{|N5Rk{OA$;ugpbqr<%d{oOeAhMaJ2fMHeuZ5?h$*q0?BLhxEV}T9Efy!s8^9 zZZ8ka?s734_}vdm)Vz#-zqfE12VjkmG9o%$g$mFTUZbk~lCZXjKV48UKy`Fbqno02 zTd)6BRYn(nv`8BZ-b-|}X~C`NSGavdm_o&omJV~_eN|3wfk)7wWTUNIB8Wu=E2-5% z$bvCbzywilWxXp%AQrKR4_GXh)qPO}1(*=PZmrmZr5{^(-DPlBU%7n}s(-nl!IdLk z2ue#)g&$g6vk<>4D^oV5tdUl3cQJDMZXm6@<~6*dDzbW5K*gwzUlqMfRfd83)BuVD z1!@HrlBi9Gaxhi6K+%}`5Qz7Z7GrAhxmn1oh@gqdw)(#h^057%p)9$-BLP$pf4w$k zuWKD)h!yYas|E8?Ya_LfibZ`_S7j+cgIZ|dgK{zy13uQVZ^xh*#MNbGVWA~uKuCQ6 z7dCF3wW7*YwY(arm2`ulTvf5Kj|vG^8mzV(P!ZnB;ZsS!1dPfd);HWL5ma&;5Og6I z9T0H@aSDWoYF`!e90mS`0=JIEhNW2Rb1U3SrPiuJM}3tXd__GuFs%E~z=6A|5S8|6 z>9SJ>9#c;6V)n#7q`q%skpnh$y?q57QIg_N6pBKdRCr{yGRBA2P=mfPJ5XP$|E}G# z$ZSWP90W-5ctW+e#l*c_c3VDr%u#Fp@UuU%ijV>MVCb^pOZ(`_*e; zj5#lRE*-Z+4efTJAQi0+&?WJ?^S)GPaheG+^SQsdY}@ATNSja+wE_^4QJXuQ^t2OZ zbKLvnjB_~=Lm{ff$g_Ouvf=#>UUTh*H+H)|X*EcQ$&&~*TSObKy6n1_z2JnEldW#I z$0iObVrCKm*05p2Y>17LDy)!EHGrL%93LKPhV1xZxC$UrO`F^UK&P8!y$m7POhlm~ zP!MrWLu@C;tZjfoh^&EDvriGDjB=;y0twDDIVK?z?A{a#sB8n$G0HtB2;`6STCW})q>LaHvs#_-S*%@}B zJ$ddV2DK1~h>4BqiXOes(ly5%dc(!HAW4V~lO-ZpOCHzUbm#LiT2v7l!>nVNLeFh`^b2;4?RJULiB z#5t18kgy7?QQ}Uw-RZhgvH6g+QDd$^1&bG?c5I|+g7;A>J_3r+84d)n&|y`|Gy_t= z26A1c-wN1@!?~yNG=So$+`y1hkSHWrs__39}NU)wKI8=L4o1ogR7TM@NQ!aQ&5E z{?_jj)0i9|O)bxN=1)EO@OQuNIZG!;L_}3x&JFJkDZC)S`syKG#Dz0CgYzrp>P_$> zJEpthT{E>roaOoYhc{7v_)e{AUpzxVxb|HXNiod3P^?23tG>6kH0 za3Lz02r3feuT_bgU=FEH3x-@}0maQNVrD}OvZP3QGqP-IsTTl&5LxnhxwNP#S`gvX zPh}>PcTA_p*07JGiGnrBirlv?X@ zZ>{NM?!%w_--qwrn6#R?^Tu#zzV|mTJ9D?C6Z123U@3FB2Qg_M7^+YMkdKKnK)}OK zZ2R?3FMRUxEzQXZpSKZrZA4Uqx&WZ+RhU~t+-Qmy5Q`UTwA#CRSN-~Okg_3FN8-YG zhKM3p6{3drb`DLBz3{mw7g<-CRHbI@3JAs$%WNkbYNj?dy-d>J4tfVT3TL%TMBB1n z4+y<0DqJe07_S=N1ZPr1eN+_U!kJOgaEPWX#jS+Ef#$d=M?}gnK#d^|!^$Z85m^?-MwSE?1;e7P{ zg_=|Sf4wTZO3N%+6_^t{G`dWE4t;QsO2njmE%wYpYDEN{QY0$FX~m+ z-0t#6;r43f$42ZXtfqZUc?!vCwC($z910$|vyb8uuP9f*f_+($$?CKV#67Fx8GRmk z5j{XLKr41jjipx9v*uF<2e}YW$c>QVe`wdtP6i2j+tojiB0xDC1$`dl*(yCi(J-M zORZN_tAfjSr9?@gRm!F?D$6+aIx11=>3X?kLk5a13Mr!|>*IHzLB+>)mTc9eqD3WY zQXb||71H--U|~~Hc-)u;m0GJdc!2PJ0DK9L(2`kG`-c0kWDM{g3uio51cm8b0%`G^ zN^@4CKn;D7p8b0+(~8S(LW@L zK~NL!Q|sub&;{D}papA9HL);It3quJXwXXeRG~dJ{Z0!m)xkRlnq0et;=f{6Q32kT zvl&|Jn*l6xp-2>{Ywtx~RA}t_wnsH#s3zd_-K`m?`(#OtALgJ!xXhXB|oz zE({B><=NSpM>lMKWIDh3_Is4QSI0gh_Id81cFmf7SMNT&YsXxJAueA&IdaPJ2VL=t zUxe@%B7!m!%&H?_2lADL@Wl`mKx0gx_g=gdd<_p}0*ClBA}ii| z%840*kkGCK;1vq-A*w?NfHG+y+Jx*`h7Az+*NKYI@qR5F;Jw7cL}aZ@gc7!H$TAFI z6ANRAEU^WAnWhIuj4=QVlO-ip10iVINbi1h%e(&mYd8Gr_vu)3Wa%&%;^DnlaqvYY zxWHAFRV^XW;DZI$4{>n9O6w$uodC=16)MJih;lfZt~iiGm57)%xB^64gf&q(43^oN zz_7!M&pLJ8a@=RpEXxufCH(@sBn zxS5E;kO8R;@RkO;LjZaxWTB?uXJwh%pvweRxOePL2!)7fB4rNYRuF~r9)MG%(RlN! z;VI!x0a22e!uuH9xe9TsWg?Jpv_{d@1r574BnVLqhKY@-4ss0#l}gBR1rtsK4Eg+7 zy^>Pohd^WUGNmHexmdA@$a~-IX5i8q<`T$P7_@H@x(DCr&LL>t#-yS8=VOmU@SnQ_qox zy&mT!oU5WyaB&i9Vz@b+O3quS+gIIq-%oyd@fDX}pKsq`hnvaDRgj!~4oWn0)SKP! z=tKYYU;g%$&pC2t*Q`xVlUcwFG*T-{tp@(#Jue&{AN|4?zCZKiCP^%ZQ$t6Z$;gOw z^I!b%!e9ONs;52uh_}4zIj5g=_{yaX+eob;Rn5I8zz}u3tXekyi|_x_(os9qNY6dD z1I{QR>13pNRCtkYD)2rUo}v`;k^;~;Tx-*d0K+g0&rtH})d?Y2Q3fmdFzAYIKxHXO z(B2{Lkf1Om0UAq!(pG?3Jis8UAYwAiCY~QdfKesyjWA2*Osl1!dmq~Pd=Pgg*5Z)^&2AyOPLHf_$%cVGMB(|+>3-`#%W zEnrr!-ZV_3_k}O~?-57r`@ClyzH2U%fcpn67!JXt7#mSI4Fzrxy$p%^gwSeoV`Q90 zI>~s0GbNT_kxM)!CwN%!LN13$=s{8tsZayS(4?V&(tA>YID!MbLP!9^?HzV_Z0W>! zBdA(a3snkUw2C%{8y8)9=YM?VXK#AdnXiA@DPBUFkAT63R2~8B1AuO?2RMZn^--IZ zI#WF0Q2>ezB~cZLq+E!Gbz$TY= zYqzOi{pf;co__edUw6j2Kl((Lqmfu*W(sH75W{D#w_@qoXTN#DzkTAr+IdbxR=p%? zBj35{?Qi<)kA3*H5b0%NOtDb~7=|_I=5Qi&J z>wcHK002-&B~Su+D7{fyFN1 zXap4jh_xz32&7McEO-@ObNpTl0auZkQA$*dvPw8*8Fj2_QHQXSL%DpS&~8LUz+%l8 zFK?-U-P$azx-5Jc_$fu9#f3G{tlr)Qbj9L^vXG);g$+5-uv&RRjp$Q6tNxt6`AknK zCar9Yf&S7y=H+5FK@9sqT>i_@HDI6~3)E8i(6UvC7#&sjQ|%V5Pxhx$E;YA=iZN6W zENcd?mP!mjjfp(iRRyA6Ye-#+)PGsP=1R3#L)vRM#II{TUtH6N3ih?KuU{5wv})#{ zRGnqpYNI&n!fJWn|4?JuByv$wcwu5> zy(c#Ay!-x558U@4nu&9{5e0LW<*nApp$F~X05YdT3H5S6Iy!Xdq5ENMiS|6wR7DjD z8?-ywo%cP_Y7Nuo8M9)!YMVF#DH)@=9~*9-c+6n{GB30GnzLvXE}sL&Z1U zuJ@S~i=BprKXxIrvC)(lC<}7$CdQiQ{_*xrcRxUejTQ1LY7`uy0k*!Av&mVN$cegm zcttoQFvP#GC72))4F@AQZA>hke)!4jZ@mAs6AoxJtav4(<)%G=c$h<_?3Ba@Kt=N0 zi#k>>Ac81F;cAn(jGBmco)&0-u{WTAcMvf|EXC*pBrbC*5TWW=HW5-7N<2yCsF^ey ztuQp1$d^!J+$5p6<*q>B{}t4#GPArEM_LeI*nkbOg(RU~1R_>2tT6ymk|gjDVH-oq z&G&71`D;J^;W&~JGO_>aV7+{r5*KJ^ss|pWM79qegIOM7_6-;dOUD ze*a%~EkA7dru#QMcGts5EVIP5%A{_3=RrptdE}bq07StYR&d_Ui`-kX?U+YFS-7ix8DBRmp$*j?|Q|-`>zmD7RV?npy$FGZAmwye)v*%phYd8>T>7B`OYcj1LRwM~I9Lfvg;8*|F>Dc0|bF zAE|^YDllvfb7HvJNG%&8q^!2Jgc>O|j7?LQcgH7}H5--yYlzr}+^KAnV6&+z;1J@= zRaKN!SR#8?z$)l^cjrBipZkkTe|y1Y+a7w@s3ar9ylm79srLY@X(4k%Q&X>c^UMC} zy)WHuypcOMGB&~yl2*fHB8H1Z<->0}?d&s;``BmCz3_rdv$?jA(Of|}8k96AIP2Yb z!ByA%{_-VztbF<@N1b-^(Z?Kh(0==^oE%QPD5-=oaKP@P%|`m@rkyGZVTc#+f*GP+ zr_RA7fPs_3OewG`fKcMpplD#S0^DTVOrf4h3C!dY3uD1vOC=v&7F7idkpytmdKD;n zA=0_Fm7YffBw=(CRMI9}CIrltOgKM?$y!4kaB~yX>fGk(4}JOvmt21BwtF9^lv@)c zY!dNZ74;;!9iP4b{*V3BfB)=z9~&ELM0R!{JQG=C5kj^L4WlXmd9MJn+_i?9Kl$UI zzx$P+^qk?;c-Kps!@XV8Z+-jA_up+P#RQ`U0hVI}7;6)!*lnWu@>idA|M~}KXXcaE zq-$qpV&%*efBF2se|w+RAK8D;Rl7PFn+Oguw3f~>YN^_mxj@y4%SCe0h9U3ax|w!5 zt_z=K%u(wJOr01b0*#3bIi|=YQqRu&xT>lR5$OzqvZ{v)X`(_Z8c#@$7+6&7r0Tp3 zrDn&j-YbXk&Wqe^Y?x1o3~tW>Cb+B+JAoL zohKc!yMgUG37K!bs?R*Uf+6aI8OMeYkU}Mw8T3e0J$cRYum?m6HpV+rXGp}W5l`wo znaVXOp+XG@t^!2lgqju)Z}0u&`@d~FF$ssk24!&Rh>9v&!-BKX%_vr`yZA-V1_qv}}wuAo4QWGKH1M6iqBn6NPh<c8vL=vH zsqV_e5lK{@Pud?Cr(w%@f!|SIDj)@nUjRr|x$)k@lfdF7q2?%4{da#Y`=4EN^cF3% zyfV7FmD60}e2X-O>ycsAvudt%ZT;!5bm?}~sBHB$w`MD#BFHRgT-}9S(6GYL?T;;^ z1#jY|R78pPl~oxpPl<(4X?4~=Dg2cFAyP@B<{=MuU-{hHt@RX8WtS8NakxUEQljxi z`^zZY*s!X_^&87?3gfwI2ukKxe~bIBsWqnF%&K52{K)=Sy+`{26RlObFlYN7PW6fx z&|j^SY8<0QABVy`4PV$rCsOUMIwVrIvaCt%TCM*oa{AO~!eHq&Jzskk^%YuwEy~PY zd~nvM5iR_zHk$fpalwI#=o&&mKtT|s(p4z?gs88lLoNGJ=2~0g9HT;s#=fNfiiUNi znt6p#2?ab;<4+Yu6exTR(TebS*!sQyFS#?=W%zfq5m1}LJ)h>R78k7Q>}}m_QtO?EFqC}Oome(@#Nh|VZA?-T5cI%(r&b=g|F-q(li?A~ zRAG$eOz5`zo|s&+OyQ7NI0FV)h>XIIEuT34r~_1-2t+iRsX$89YNi@u!y(ls1Su(o zhnsAmUNol7DX|PdRmG_|cn|N@_pzByr zFWNZ84AMSL8GCl;Y&-9E0}@l{HwuB5X_BOkq;w)8cniAVPghLunomcDSRFL%ml4z` z#egDJ^(3*6m_Z)QP$5d8U=Kl{DygUeK}D&VB9Tijz2UVlIeEW5C%ftZqF`%>4fs%t zC=Fj2S`O-^pcgrbFGPW8rchx8 zB$)m#1lB0j5OdO3xy8Am*x0tF$AqJE7L%3K`C5siB$aFOvLa1^6M!v<`L3~>Tz zwEtOQ5D+e--Y7Y6qm?|lZT8)N_g@=syD?e4zjQl*6NQ3RjS8XLOOmv;a`%-hmn~Z| zK0Z0NbYf)bvZd?qefYv({LzfG2suMYA!-y9nGU6TBPJlvg?d{>routSh1nAMl~EMN zgOsVu?3k27)*%gyA7#?q>HaH*jy?JC3x9bHNlFPwRMl(Ra$9%ac-#G_9JbpZuex<+ z$E-~ZAOv9;V(NA0pMClXOGk#|%z&2s?ACBA3}PZ>@(Oaw>S=hz%JH!YYm&f21Vcko zeo-%8y})@Nvvmnu(@YcZ-N;aDa%==~y^bD6VN8M$B20ZfL3MrmJDvnr0C%cdkqRpV za)D-x(q{`!B8PAvW0$6u># z$@tXLqg!Trvu!0SWJOtAo=)!Z>mOh6@)tep)#n^TWD%J$9M5f`%A{6BaqMRmRNhy= z{+Fo4gJ(oE;4#H?vepE)<<6P;tldowt9lR8b}wOyz=48^!+Q#ly#{s7O znH;y^xt%lT|LoG=e(w*-)Yzv!^S+O~?YY}$yQv{kVMV9wRbdTt@Y_{EBtpn#YR}z{ zIP4(jJ=s>0SYwlbBCs+zNI*ryice4paiv+_z31)+Ha`5AX{HLN#NpUprokQwwxntx zP!|;GFd94E!%si?d1s#7>2x6`O{`7X`Rx1O`PuA-&76=DnGmWNcLP32hQza4g&QZ2;V*fSIJmcv$u|bs(!fI52j$vsb6bFqPD1)sN zNfUeZx<@|z!7uHco=rv?B5gJencr!bEq%qyPhT=Q1R#*tK}2iO{S$ z7|NY^@_-ZXyyOz7hZK&%Dt4XXqTrz@*9j=A0>lz#kVc*A<6fgt@*KA%*hWe?LU1F1 z;&65uYBty1`_ScA{F$12Zr#4~U2pry)i=NP^B;YiZ8V%O^sE3%Fyw%_b{lZ2Ud2Ie zIBiw~S1um26L&ibJr?&eftYZB<3Y&@k%TQK>Ide+GC`~=u{dI4G8XvdmABt|`6WZU zufF5z8=m#Tb)Wvncfb9O&uLnd2k?&|07>aR2?zleR{+L10V#`!kN`pqB4yGfG0;SW z9jR!dFd!d<7MHEU+9?i~RU&rciPL}m_wQ|fs{@d>}HX+TDv58F&-T$ZGUwiD@JsOD-pG9+$Lyih2N{r$0$&n;UfpipB8*kd_X0MjOw4)JR+_95beLW z&{dW1RdaR*pH+Nbd3^okr^=PCT)(o8MNRr#&SK4^{>PObTHRq>r2VyUCluHB$)L8l z7E~yzJiful^?8+j-fMN$QwgPN%>wAPR%z<{QtPeL_*g1J5vcCDQlWAB1YVUi$dM^` zdg}01-&KX-8$qc0MrRqqHL%tc=D>nMSa$P3!v|ZfB?GeVn?Lod5>VtRYJ6K0WZl`< z!Afc|TB8v)h(cpwCzK7Xm9?NMTjA@#7P8p?0WNv(@Kq@3(jyCk5HRS7l8mO(xRG*DC|j+3gxYVdv( zKvAo*q_8~qNwnzfg~oQ(oIagVR|{2#YQ4?~I8lMGFDl{s5Ua*xJ10H8o$zYV+ouJxNXZiRX=R=p^!I)XP-d#*g};Jw;e&P*T$k@K=* z+2rxZ9(?}ocd$);?i7|Ri!BZ-aF^AJQwjX`M()y zHI_~^JjfUVi7{-90Vl)>K|x*-^pioJx92-WFKHmL#B6;^q~t{m(}PcLa`Uq_D}oiQ zY}NURvC)y14M3j-d8cDdd+&z%o3Fmbt5`!KE@Tj{4FH0ukLk<8oRUJ(N9iar)WqwF z1MKES(}|^5Uw7wi4?MBoo|6KO5%?NTSPPO76%Wlx@(`;5v5;KEiHHb!6;>0H357vu z07FreO^l67ZA)$Fa(T_j{-`XTlNOIz_8=a{QPG>{ed^V_(bROTnuy6 z!z%CrWYuVIiTB=l0b9eZ#Hec1O3#V441kRV87-3f5gV|8H6He$l_9Q$|1SzI(xr;=vlzlNWGtZT$d+P6tI|WH{fII1IcP#U92V6&cYHh&Ea-@=?<=#WZ$`k)h_) zl1WH78N?8gF`TvMmQLEweEQw5f5B1Pr#lUsM3*2y%Hh0sz%0i%xQf0I|ZWDp4TlNiDhI;43WG#R`i!3Pi1{ zc=^Sz{*{oqg2n>LzeLlmOyJgZ4;+WOF^{^!)E zeSY_4@gznqQ}_i<9l$m%4BK)WKfDo*Mu3J0@eoM|1z^}DmK1p6 z@ojKUq23F5fpa4xjb<|ikfw?A;vCXy!7I(o(CBjLutJ&0Ihh(ucUwMbhel1aq3ZLj z7qK2tI7A0ToU~?k&VK1%zw)JzeGVY7g*E8TwI-Th`qBsA`P%1hnQ4FJD?jT-Dz3cd#=9STf}0H$RVFT@6a%k^4T&KD43n~L zjY36y&Lj%SGL?4E5YiPZUiX@FKKS0(?6qKKs!Vj@(yNn7BFIjGh67m}%2UZ7WScIyG$H|Nb}J zzy8rpk8f*^HpqL!oslJzU;B^m?|b0>?|tJlq3T2|g9~~{UfP%>APJ`*Kyo05sCw=7 zvc#lW?(+HV=yZ7XKtil2HXF+^#+Cz8XN@UbtDyXp zO%N{{hKY@qN|8WXBz7n-FQ!nKgU-9rk@V_&9{c^z{+Jn`njVN;XLf4ueKNtWjXQ#s zVN9A!XRqZW2Oo9Tm)GCFV@{eAqi*N)-YtBNy*ML6Xu)J;Ild;&9)kA8ErV#evg~hX|{IkzXufL%R5vwk%XzcYm zm40#EU#iL)tOhOGJgt1q!tor4MnkPvf-Ilq3G!FZOMZ} zS@#mWF4KV(()>Sa7DT3fU`1)QQCW*Y(1GgL*JaW7LH(0@r0`&6!nEz1Or(olxZnx> zKS}_FDtx%co-I*1RH3HTMFpd>oJUl&y{^V;-_~YPf3~`s$O3BKCz`5&L729F#UcWx zqDE2iFSu=y&#EzA41+#3*e3);;ZR{B*2}5cXsE2onm}rEZ*k>Q+=9B`N?jEZYfR5{63a{~ zOy0}ViIEeJKJa{Is8LS6vNE%^Pd@O-#>Y2ed`!f1NSROMy);2bA9hFr%sCCooC(1^ zp)|Go@3Z@jzRL{t5)K3mIm+D3^hlErTt4h%h!DZ?key76{0=q9Gz|O5tuh1n1YjV@9F!}_nwxIFH|yp!oT`Elf>IgW zkZji;yYDeRl#;@*VJ7EY+G_s%+&^u36TM4(GlavJLdySi>H=|I*1sMXHqe3aCWCJaA!91bm0n{1h=^oF^6$ z!*M@6GUg%yDzIFbh`?$L_s?0B(uuO;!bJ^v^a9QwQhkSEAu6KHhS|BR^UL$EvL?mB_tXGltS1I zz4O(l4=RiQ^A%DDv0)0)G@@Qbq2k^1Pd)N8dn|kM@ok}-L?Pa>;oEM%@7BlKe|h-H z=s$Y_JlUkXW9Na#9(U+|Q$S1r7d8gN!F#*yzA{PRe9lCMJQT1^^m@{rCskHX7_3Sv z6mBHxvzshY!v>Yb8+*Vo0RY%q!rmY;0KnAbIGQau&jeOw5`f6^%!bYPJ-qGky~f*_ zH-(HUdNV^rw{M?@u?2%E=nbjmEbMPt&3cP=m`dfG$g zHTyr~q=SfAof~Q;cRal9=imEXPGb$LDjxA<#g3hr+;GR8-~8UiAASFen+a=}xMY}_ zi~;+C%@l&v6zU-qre47<1_;HbL1D`WrZIVgfc%q)i$#&zB)#LIt&iRR7yvMaAFWBftHXqT12bTWvjBzED@6m!NH42&me}>mg`kW z@CVcE&FmN(YrOMquQ=+!sZG1u4Qt|&D+L3H2_|NL3#!jR?WT=8);+jsX0~mT21?_h z-;@g&deFAwy)|aXZ12jeZb^qm0*)I&t(t8arn4s?P(D>60;7VE+dz$=aoOe9PmVWT zFSm(RVkO$XZRVl-p3tPJiU3QOYgs&jH@*>&LZCimJTxxzrrYL`v16Y5v===4>92d~ zse4Y1O4{9_2XK_>TbJ8y*_wwDIP*{mUbNUYCqDMKXdT+v@JvxW^%f zoqg6x^SPhiJiBVeXs;`539cx?@Lb1jndz#}yO|uhcFn;@92KF&XKt6Hz>;HWF4Ucc2 zgbg#J2c(ZZ{`fck{?*rAeZz&nx<+T(zR~#K|Nii^Ph8`Gw|w9$S6*|+$gp)TgNmvX z@4d@K#5pI9sg(p(nY#?b!(aQKbN}ZDzi>{yXZ6lQh>#?9ZhCgj0sDUcYack_@O^i* z#hUuUQ}859@3KU^1{{{sgXxRbS>Uz}x-+NJ z)qkNMs)Z9t(7rkp9%23OVm+vap!R)LZR02n)~Ub7{rAN$3fQx3eNl6*!8ueFvQQd} zJTz8`qIxfcT7J2xdmC;UEMNOggJRxQ!9PXC2A+aGq4$+B$b;09h@_>k`>PVo@&YD{ zU8E)AwV=+6iK!^-sTyBe|9$l@ro|$qFBmBO9(+}WIP6QyB{X9Ju2OYyDcq;3Yso+@ z*0lnq(f$Sm4kB3uI4L4TRF+(YDVEQR2xxtJ*X4o1L0;UfC5f>5s+u&_=qsk9Rz=mE z?tuw8(CD(iYoKFrc)7IT`$u4nmRJaOV084E-_>LxDzqRev_k&s&{d(+ia#UsCLjs} zKvk*Q>iR+vG2wV{R)w$@ny6OOx*1bY1W-y&tMRpS~he%^WB-exYSzrUe^ITFS6r&(>#T4F;|TuOiTl7?O(s*lXqF zjDAFMFM4Y>TUwFiz4wG`mQ zdJ$Oaf`0dhtGe?&9vu;%2O5bILmkBgzYC}jeFRe~289w-$&^H85^>%jZCrHOZEt?v z8Aq>KrQU~KbsyO%fr#XbqZ>lRU)X+^poQw)jbfc95e8hI@40Gh-+fnIszM548~Rsa zFfxAg&FinZ{_Yo^dT6`XGls=05PHANC>~f|fe@r!r!inv)Icozgy4iIWJ-qt4A_D^ z5rs1;0|kTF3{1pgUN2l_sE}A8qEfLDtfh@R=5N0Fo@8iTBxEc!qHtm`-#PM(lfLi| zui7;;F9Kr#f+1#RRbqxo3`mqzi$i!r5WHs_Ym5JvwziP5|#8GQEJan0}229?IXs*U=esaU-|LaFv9^U{1m0aOe1&R9h z?6c1}eqwaEn>q0svMd5hB5>~TgZCL3Ywn!h1+}USQAsQyS6*?`i=TVKlF1R5iQ&i{ z5(L@@SpYxcp&kYs|HddqC97@^&w`M8+6vjp1UA4>3@p*0OlXW70h$Lx$N9tynQWxqM=F>kPF> z)dw*cYv*^)o`2Ex$G-c8?T!m59u;Pf0M?i-v+bL1x{I4BORnlEj=ab>0aR2YqE{q( zQXohLDy*JFg0@tJVgCNlFY?{#WNchjloQ{bd%?4wHa?U(=e+o#B>B`Ae=)szD<_GH zR}hFT0h*D_4lN!3_phJ(ikCe7(EV0S&(BL|m+8(JZW`k`QJchNCnWoX3Q-gd;sFJ< zRk^U4$bgL`iBB6sNotacC*VZ^{QNgpt-t>XYPGxq3fIkOV#$S<-+IL}-eyypop)dO z%wNCn4QIw_Zfyi7tjUn)G%}kB01J@!GV1j>Nn6XO&N=5*fAxmvop8vWh9S=7dFH$%hLzNZNZe3d z*=K+Y5UVPbCHF8R4?Otfi{JKdf4=A%v=T2YJqDWE&8ay$=j;pL@JWdGxmV=v*>;u@ zTNbZ9-=(F?zx|_2fAs4s46zpvr9AU?xG5mT5FypBr?FK!Gyma_{P*hBQ_nl`fVs>Q z!Y&t3Vsl!W#NmyQdR7^2Cg1$|wLd-g;=g_Gt6y>U@w4qLN$O~Q&_U##jI`{!hc>?Z zUH^Lh#W$Fd70z|pHhg<#baKf@Kk)L`p7XTKNm`_=eX5reG!w%$-^2(3xO4qqF1hR` zJHEs_6{NoBmF&UCA9uv+q3_Mk7V-B4sI_KpcK*r7w!$`570O9!r-065*TnE#JJVb#Nu$H%)uw5)nF3@2bq}Jj1*8!iUkb6yq#nZHpaxWYJdeBpe8Vb+ zkPwqZY{KvU?5}RWVja@ddk-S*WEdTP(K#pIe*3MOx6_6-1OtI}ole`$x8L*jm)v>X zZ5tk+{?0c({i5d_4VAX%?HhJwTYGJn!M6=^h*Oae7R<)NnwUm0Ecc1&Y}(i(N7gWr zVIm?Ali3m4Ih)PQ<}Pk~P#K{V{Pcb&N=xS%l^0wL*1E@4+E7FQNsOi_{qg!(?e|!o z%KPr?@0x=ZKMe1x%*Ekn;T&qMwtWw-o?C|%YIaqB6}5a=5q42tqB>B0jgQ0v)`j*x zz2-TWYg66xroN_DKHF0WN(X-T38eV0+Aya@`(&|Kk*2v6P60~vgLVs??tuzC)q`ra zD0+fw9~XburCEW&fYyMuf$CJ{*F?Tp1s3D~ls5%OygrEZDMTGUsgB~UE*@kZRXnYM zVNeX4vVGO00~KSZQpoid#|&l_I2Wi{#rt9&D4-gWF_q5=$WdPvBYs6yE&3(d*XD>> zEh>>g@zj57QH!FVHTLb|_+!X9Rt%D=3@rKE-QMO*XJ@P)#;9 zrO~&>mCvY9%IfZ7H!!xiA0?$?5>@Pq1%tRi&gz;nE(sAXsog<+EefM9NOzEz*>_J7 zI$f+uQpsSC-Ig7G$lh1pf3L9iH3}ByC z6ML-QZS!NB%;>mxPC=?(gJ+>0&={;F@I4*bW7!EuAE+v9m`F*WDkKoqB(YCBdM&0# zwcABX5DgRq%ibCD(_dWjH*Yz6w2hApkLRM2H1{sEdvBw^J&~Piw1kB_-Mq0^r_iT9V*1N&11S+$w2`N}}A>RvV zeHaR1zc1kk%SOcI;w?m|mUSneX{=3)z`OZgVJ#)Z}m;jL+CGWCqn%SAT z_jXsE^ZTQ!y8GT)-}iULotfL+)m5iXt`p##5K>Wjjlhqk&0;l^M2pdcPK=!*`w6xX zGX{kp%dr0;dt-dU7Y)W#ajoxM*xDt^n|e(;j-+_b*!rSxpLYPT5d@@T1RN3bJff#d&xz z(O`h_@zYK|y3@^zULUF)`zL}4y+LupfqO66ZSA(LGa|&qs_M!zpIq_XAN=uI&p!X` zhwNL34=RhOfI@00oG24o?d-4DZd|pjyZf5SLE%M$m7@fNeksJkO~oMy7R1B`aEP5& zIrI_&MPHpJ86B4CRUo2DP*3Vq0SPtWBd-iBHCX-(6<2GRC`!7&ZgXU;5flUvkm8os3F> z4eTk@AZIdUzW%en-hbmA`P2k_krZuJRT1w+W3N}LA*mr1I6CG)p)HLHlyYM?&Gvu( z)8BEY)ykdszq)er{v&V_cJI|24D_#r2;!PAWKXzMC9NGmO&cJeMUh%({~$_k?}Laov+)o=+BU8`MH_tRZAav_DKTF6$2n+%P+dG%#N_{KMbhaMiDW)|yyVmOTRtRq>K{S}%Rk;|(xSw17?9=1_X>uzC(C)#|o8 zV{H(HHtz^T1PXOD6rl`8h{O+_b3zLv2X&gVVntM9M`HqU=`qnVKfm&>?|kxe&JdW@ zmz=lV=KD@~%ozu-UpgrIP&Hf{a!i1f#%2qH^2DR}U%l5ZkAM8RPkiK&E@+JsbO`T- zL*rLsh7uWJuvbbM{z%%kaLW=K!^(`*@5*}1n&q5#V;Rk)pAkYugCREvDsU$=Ck(I6 z>&e`xZ4Rvpnl;pu_)LA|rAYAUZYY0dSbDjRrFa3L+Go9aSX zS-tBuvs~;Zp=xHe5tyvr#jhNiTysns1`-;7Q3Fy5Ng_K+-!?MB8gHxJQiu>}eAAd) zor+U`JM8&sPG@9}BNH4NsNRE}0Ir^A8ly4t5EfCIc8d?FX>+c(@u4bP^$YLl&WT!@uPjg98pRe8ILpKcXPlbwvdZ6ZiOhT!98Xgrp|@RXCL15lvPICrEz$yJaYDu-+B;7ZTeK+@#c z9SX!l>;l1!pmE7z(;GZ%5r{Pgex+(?TCGZ}!7Im83{Z&OTB$x7XrN|a?2*>mgW+_uR_YHDZApA^zbuU z9lGCdzx`uFtj0hIVnZ(mhEnk16A`l~vud|B2kp0;Kr(BX1jX>5=zw&3l%U7>a zA8j61n6}w;`yEev`GDF(?!qY$|4ha$r}kI_AjzO~6G_aX#9{dZ{R=li7#D$%^g{1EO>5-0Eq2$2AjA3 z;kQ?vcgE36y0+ghENiGg71YB9Ej0L?Mk14?SeRz4qAUj+<}qtX!?? z*?>`y z`?ej<$Fj{t8iQ#>A>IR;Y8`5lJlUK2oXx6)XMu? zx9@%Akq7U&Ohmm`L&4D%>10PC>sO8+a>OD3y!u90DCL=usCP0pj*T1M{+>(s{=kd( zS+{iiTyNk6P$ps{M97J|o%W9}zwH^%efKWAF8{*EUUuO6729@{P>Ch|m}^{$ERiG3 zH53DcF;wZQ99bTyafGfCvYit{mw>&FVtb7ctq~Dhyg*7w2`!U*jvytxPrDjVEEuqa z5FcxaK!!pNI}}80Y|Z6G0PMPU#Rv7S2`!6Y`B%%R-OU)@tKW}2QF?{w_Uyx(iLRg+^a0Dr&b{+GPu12^A%Z+FSK zl(S3#@(|HRq{didSzF_su`z3?P^?)tz1vX-PEEFKrW391^5tiqc)~&dylY`i^&g=^Ov^eY0ZP$w0jWlWD_doMmm3x5ZO$w(pD3 zyjwN#6e4iMd?AoH$9Xn3H7&m1D~ME$QR$cE+yKTZTC%WXa;bUqJ70I&sfSwcW(S_z zT_2k>E5`(X7$(c1n!X{*hzbxf5tAB2WGtzn;3U)};sY^J((sd#AX#kIZ<;LA|W`A-LONHI|YHQ;zO_0npI15J$VsGI`SbHS7uA5{`Xt&-GA+r zD+=%2{6dcn3#dEM{pR=o@Rc9_?fFkW>-WLugoW$DsAQNh+8CJtk z73y@#xorpRv&Y)K_WJ#ge!O+2*O^|vu;KP&A9l+7-udkHYbJaBLGaZAAt8bch0`+w zBO;I|lU_h9KSd#Kj8pFCm_HM6xDutplQAt)%=e1z z+vaF?TRF(J=!52+OD@MU)|X6-L6gu6(+r0SA*mTz#xuR*9q<3D^;Reo*B8nG#=^Ga zA9dDg580y#?W&b-x#NCDX{-@2h&{Q574V>R^_myk*Uccq4TL2G3rYHpT?593@x|_`p04B!3P|2{6 zJ~Ig!CcO8p|GDR`%Qepm=h_)bDPRZVLxxmTDii|pN~)aH97RmGVPzSE+q}Mg=(;vIUyYD~)=A)RWa6{M2#U14-RYpTMQXweL7q)8ETNn=jN8C84L zbY(OrP;VsCu2-hT5Dksw_r)1#ES#nFYZKzt_n=|SVEFryF%v}6oplr!YU3v{7iz@0 zRs+-?mL@-FEVbb*YCP2hK?8&|GPEZ5REiUb6KqUnX#VxEDO&|YtJWl%8^Ens98U>` zIaHHoRnvYqKZ#*99b7|B(dd5)YpI!kblx>Q$0O*bi@(+3*;S}FTlk`a zd*TB%6VnPJLyUSSxLC2KA!2Ltw6xTb>m%@qCxX?wYE+CjG{SfqFBswA@nz8k7>bhB zNEW4Lz2q8GHAcxc=zo|85MYP~&QG+mLk`{xlT$hnG97WEY@mh|1kaRgI`6@>$G*F- zUtxM>nTNA{4ZW|Z^tyD(#IZ*o@Vzg7AKt5as8Y1XH5NruC`ijeX5}GA9AtrhsV&PP zfgr5_Wmztp8hic)kAC_OennnHeZqBx0PXf4fAG`e&)fb#?|AWH`>n2akO+pT1YLf^ zhBvQ}!!{c*Q_>0=k4dd%MQrHB&>^#M3U`BBWPiUpNxr!;&|$0qK% z`tN6*|MK^|^#x}S%eOJs?ai}^A-0iF{YQzp zH}hzllu5zi32RuqKkmrAjz9MB+poSELLN4j9V-g%wC87LU-_!{-@V}(uX@H=Ie z17d`InRoQ@Z~pOJZ~y4sx84rmyz_5(<)a5_;$wjXGz< zR+i^RRYQE!#@XV%|MP>-eD>ReO4Xdx zz@dGnK%Hgdveu_R@=5P{Z+`XDmv$|y1u!W6*MD~9%U*ZM-2Iz*VoY7=P9UiaeCT)V zeDur(tRo4bo++5RI*ubE%OEzFe&c5%U}Ffvwj+c7@h6?I=PpaVm&tDHd2hM&#y|cc zA0K1gp~lL%1IR2b^olc1I^>KK52g@f$$<-MD>Ik=_|LC?)kp8S z{r+sSUAi8`fimfDBW@w@f=X3!>Y$RhJBG0^-+%S<&wbT%AElua3(-V3yY-(NpY_rY zZn%HbYhM1;b02y9imA3ZheW`IbGs~?7@tsaB|Ov^L#=Y##*-g=+B@I=;+2zTp*L8+ z+w%YY<+U$*@dvi<*kZ=UOHm;eAKVaR3;|+|Lq@%AJ5GGmX`lVnn_Dz^VDrp)C-0T+ z9q;`7H$VLyyJDq?hqAyu#DGI0X@FY+M2gwHgT~I$szjqgc0*R8`B;qa@Br znJ9R`l5JnH8PX8;uJ8(o6J@2&n6+ROM2^9s!=?t&holCktPGG+)JcPQiFM~x)r;`J z5m-%*JSSpO&&tFu(6krlU94>yqgR7BUZp!RwS2Or>Uu@#T{vhV6&R))|9Q_Xzxnw~ zFaPa-e`NPZoOa@q&wu!FhwV;K=R9%fOG-#It*WA8i6H||bny!xKGrVY`HI&Sl3}oM zdf&r7`;k}w^``r_Z=C@X3~8wB7h7S6#zLOJP*uyp^I(hxIcV=aZ+^;~?Y2w5a+%C* z-1~@wKKlOW?!U+K!g)4Eed^l~P&l$Yh8jVpU=h!gD{i^^TKEOqZrh2hoZbA78*jVg zz6X!kYgJKtW=M<+sf;X&M~jItqK{jYVqr&!z(qAF9F@kJoI;>*dYTLt@m>K@3XP1f z`0EXK{Qbts6|0;sn4G0_$lHe;d-&=l`Iec56)RS(-2ccu_E@)V`-&{fc3Cz3lb`@;rK&a7T4Uj{%Y&X#J%$`#9(PK}k~tRZU*u>~?B54NFSEwf=J=0^Nw@MEYK5&*qY z@7l2Az}=@i8TShB6d7r6AiFQkU;dJ(z4+O$*)d<5iLr9?T_-*IoD+{d5CMru2#(;O zCEx{M*0S%9+;0^T#5t9au}Xg6FyHf(dq40L5`lwf9BOmT^1!pBz6DYzLJHG@n2@2a z3SuG=Rxy>*7Pvvwf`%-`bW4=RO*=!i;X_HE#-B=+sBKu$LFh);RBcIM$oSPNuAs?B zcJ2yk&Nf7iR5o5cfreq!4CSa^r}Yw%ES@F=nl|UqvynyCbJxaQ^%IH|_0|OA(0vKC zbqGbR+|^o3(?G4Btei&;({I$!r8y+ z|D3|;j2c5X;T?S_;hz*tReL6?XLZpIg-=}pA=zfh>~D}+q|=@Gtm8d|A>T~Q<7>n= z4F{nCl?x%bfok%ihW97Ey?SKC!9FyH%0|-4x{Z*EZtN<&30i}rir7p!nv_`IPBw=C z6@^JAa9l5;`g;+?6PZ^oJgNy_t1!A~xusAuYRs=eW{LGtV?Jr1YGW!Va9UUZsSXo% zgHuzQe+(*&L)2k`DGw=PRei9KT#d;(OD{{?)7ZFh)~xXqq`|tH{IfxG2@8-myLu!6 zy!lXKbrph!8)^sEqFqd*Ro?6-L=@Uc1For#s{N>FBT*VfFLRtVfuveMBgC;$JE3`2 zUBe(8@}V*D=3dg!a-$%LHzikk&5pHF*eRkkh@l<;_FBJk&2GCqc-O{k`BHI%5dRes zmOS9em~t=}A8Q|V@Lm9XsjW-_C@}yP>KA^hV-G%H4|LnIFi(cyOCUMsprI-h5vl7> zOmrT4)Pbd__b3^lROU^U2&EIUIOCK<4>|St8!rEIwrrVqJpiGy7r|@ZzW&!&oO=4T z4>|4la~}Dyz4utt>112C&)<5_hM)iP@~bZUvuxkUlikcNn0B`1j@usl_~*Rt4KIKB zvmQAR?@FN*>9+5__Qoea;iU&W^x*vu*nfJ-*v!oKzh86x z?SH!7`;sQc2rfFWf*XpNrG(`RFL=bnSl(Oc8K$z3$t7c7|H)PN{o`6>u*Q*i1{5m9 zC>%_7+G$5kjCZ%q&O?=q0U0vHU{PTvQg~O27h?8KRxZyTdh}u6T>8@un`ccn7Pk;8 z<)Ae=x#^bMuK3f9Cmg2~{x5#++vl8f%JIh>yn5vlCAw$hmMi~$ z!)2HK>Gr=}$9{f%IwN3y>jSTN&a3`*xY#P!a@WLPb6wQyn~C4B>$-y`TQ@ukU={r|-Yv7CSXYQgZA#$`XPl!62HP7*m1am?cfa z%1o_th3QrdsiDUtwiTFfJQNPmA)!{_84o}9H(&qJz&kL<)9WCKqbx9Li2I6(sYg8g zxUq@WLf?@M308^65iHSce{lYp54q&%L;w1dD~i%GTS8&T!S#52?DC)f_N1$CJn7_P z&N$_$-FI2p?&Pz*;tzkm`upGb@q;(tkhM+T&UN|t-+%S<3!ky$^Pha{Q3vkc>kpVs zn1x6TIs*}uQV3=(mYPB^v}EB>jjZnNfza#^8oG&uMynOWN9C&4Sdh*uT#T4g4vsky ztu#PNLHeOyHWPsj5ywfKJF1WoO0^XC!cWmZfjSJO|~{|o&D{fuKm&XezWz~d#KBHEOQIJOFwW)f5-gS zKJ?Os-oW{gqU#UXW5vm*9{Kz4{xV-Rjj~YE(3Zg%QzHAH4}JE#Kf3H;Cm*|V&9a4s zxqn=9^VNU8NeV@`<-I_2RUvqWi#beCU-fBB0y3UBCrv#_8e&o}-P~Sz^F4q5&EL?T zQ1u=y_}pv%(dV3$wai$?@B8}YU-;k`HM4CqM{vqx3$vTge%$#Nop<GKV3b$W3D~X4#nL> zoCmyfzI5UhmWi-XmP^O;>+iVtpEuq5!0q?H=!F-*Y1NW5&wThxpMBmD2d-P_^{ur5 zOPurG0Wbt2!zgvxWOtu+OU9_!4%cNiS_l>3P%^;-DRwIMI8e(`>6n;kubIx#^WEi> z-Q4adipeEigsjgXoteo{BXd6<6U8bLfhQ+&i9L?Vig)D7)6YHgKc9F0eVaBC<L#y^V{L(MI@Vj4f)IqyB5oV(j0=Me=mVt$fgurO4fU1Ey z%3FpqV+KYR+7s>TuDRu!>+U>auT_Q+{LXbrP9&dPEPH@hF+V788mk$2Q24KM#Bn-o zU{uWxsz_pWPP*;v&JEjs_`~0`_VbSCUQhD&j``l|6%%Kjd<=lR(|Y4epZMAro$mk@ znQWV1|M}KOpZDT?a=EC5*QHA*rk1UH(Q7{asZW0ARj<0}4X=KDe@9=l%oq}-e(Cq# zW6g)(dQlL;46;l_yilSn0q2Q4YYAl$=fp)BpbCoM2Z;!E+GfM{-g`dq#ZIS#;L8Jx zTJ3E=>a0^vdg%H>6sp$pT@Mt0x%_WgH#bx=i)VG-3WgXy=*(yZ9Y zkT+mwhHZT*nUs~`(j2?t5{PfAjHGBZH4L-(fW}HH!>`raIZ_SMjFqZPntJwCs7)1^ zP+2?`1yyzv)zhj?yb2~w3v>iHK=L~*_6$b-rZ%ygK&vdYAz)RL8U89dN0RZ~9H(iW zhHr;$_r_#~Uz$w7;U%TW!=^3T7<$M8O~6UsoCZ2HOwP&yn|8c@S<^%wfm{MbGl{G| zTc{kt!G@+97S%YF%ctRmYC>#yx3me0iZ0T$(pMF%Y3L?x4pb}YLo2EGr`dy(nvV_* z5PW94qN|*G&8T7;ft98#)|$OWF0%-08tf~+J|JnDG=+=Wn2J^$GA^N-QcY78 zyr_OnbgF7&hpM283bd8|rdr`*Jz(ukk6bR|dMzB9J5VpU@u?pKhIgj1`L&9gP^x~m z9M>Ncer>gwkx$&;QHrG+qD?SaIdDRm%=Lcpm^zk$7fH9wdMh^}sG`mhQ5C&AqqW$IF+f z^F*E^>rSl$g;C1bsuf2Zw%@{_RA#S$k`O{uctDk@9JuM}$+x}!IgdH_Z`?1v%}7gv z601t7L^_t2WqHN-fAW_f{hYH5nblq)zVObYJ5EzenUvrF&*PK*vh|kNz5o9EAAHa2 zFHj#6peRPi!~!3p;-M5>LIL2$kSk3m_fD?=-QRBb)t`tIDq;*;ZEClH(1Mc;MGuzA zTgB#04|~)j&pdfQ=hZu}3?c~O>)-nM>})UVT867Smy*c`JLXsHbI73w?M|!=urVO^ z#t2y z7lJK=sb?&4ronPn0$CsjTf-a*CmT{#k|?1SN+mdOS0P;}{Nv9!>bDo1^~rbsj~Q#h zf_;hLFfBbS!?rive)nhJ|Cuk?jIvAt-|zduK$xl9Vl&}gNmTO0xD@==Pk#00>+kx& zyIy+S;k(T)6l@qwM4Bp(5fcAT_}vJ#Rv^^i0ThrBCKXg(?^Klt3fCrJU6 zrL11k*?0dvZurgR?ePh?s4hy#VCb~}@vGmTaotUO9=z9Nx7#oJcis8G%w6}3lxDIE zB1PsTkaI{yB3Sa-*bjdA+ee>%SjL*vm~e(%k$00;cS^&kj{s`!La7iNIgv-5bmTkM ztQc(C!DK?A>xQ(1s^_d-ZhP>6qmMXz|1|_KlNpl45Fj%&D6xd;r7!xA7yaehZQBPr z-hnWOgi|WCoAtMC|JApC{JZb{jI9M(Df=?mA#8bUi7>Pr2o}t;b=Upj?=N`iyFc@> z*B-R@y19O72mmBB|CR7&j+(ecF9xMk3jnP^QokrnJah`XP7<^E7;K0wf@_+UG=s6k z7D-dm5FS{S%2b7qp4?cjGL$86oM6^yh|;k z%;>na^s$eB{jO{8(3VwF;+D?c`L2(C z>8ytzwtn>z%Meu}3YFmo&UtkNq1Y^$*|1>-L>)k?9zrFEh&g9NnStiSe9wlhzx@55 zzVf}x{`T7|d)sHZljY;2I;UQ|Y4c#gi@f=5FWPwj*4O{$a>Htk0~EgOw5?G!Y}zdo z!YRlaW+K&%8@FuPUZ6WpV^dr1oB7fwzH#&s`yX-Ox`33%5I}~EHCdc2F<`{0vSzJ( zLKUiG6JuGctA1z_7{bqQc%gQDo>=9H5UJ@bUUO1D0v#+uc|G{O4wRiF!w{sBMGj zun5i3C8;D7oa52K5sk1lE2mF7Z2h1=P>^B6nPKLM$?+Ws>W!sP zHS)c0dgcu`-t~+N&pG;#^@Vq*9((W$U-zQTo42-Flo^_y9{=4RulxOFS2-`nGMTKL zojL5GN1XGRla@`63AmHn-t5AfRZEsHn^->Ge%3{g?|VAo$n^k1VK8MVkqCmO0?Sfi=O?2Ol5vh0-P~@?MHv!ap%U?beFY9OkiarhaI|?L2yt8x4#YkIHDMG zT3diY7!pUb3Xy#Jf(JKj`SKOt`u3&&yK>(>UVPE{&v?=anavAVj(Pv|3(vdiitA_Z zyZdn$KINE0_i(Lyq05R$!zu7D z4D4Y)(~#Br|1!x(O{qnWhT6X{^om;5stteI$?Ha}m1Jh=$obY{i&Ola7Uv0H1_WI>M9BIBw8^1L+sB)=P2jEEYNO7Tx{)ss2Y%I}K_q zs95`A(n}~p7&3L@t;0Qoq7!roVkx#$fhuaQk#wy9RSrk3oR4*Q52Xa+5h$nY0X-T4 zL|P3?^{uGR`G%-%WKfEUf3<^1mFnrGooIxRrIdh(qH!;V=0Xhs0M&A;iD?uCjh8ew zBVE#<`wiU|ah&3O(#BSU$3*|iMW4j5#+6jd)Kmhm{vN0oUL*JrwMHMGHZU4P{`ut7 zOt7b64p4E`IBOlcR7Xpu2}s;$9pV=i`$rvKI?DMQu&)1TlFiU~BOgr;N(^(5YSp^4 zch-}yyoHewR4hEAN`*%CgMt)w7;dC$i^CoDtf`8QjD%GyG7UadKN;SbhNAS`&~mHa zMyOTAKvHR-w=IwoBjn{a@pSWL~b+2%zJaoU; zz3o+Rf7u(dtxLz^E8-&bHg{FHCeNEU2t@ zS^>a&pR(b66olc(*(>$>=yU;ZGcv6X;6YF!R+dnp83v%>R!|XYW!#%B6Rmo%+z#E0-3|gNT@fSYbRE3Kv}k zYB;uxG>RR4w|2UH=u!Lr?RQs|{eoK=M2JO|oEzkmlfV1ZbyweV?_RqtZ{?=YSU;Yb zm@RP@-6c#Q3nmlu#?%s-6yJ#Ga!t%t!N7d!r!2qbrBAx^j=O$#>GwP9)+#%Q2U#Hl zuSB3W6SYMZ>Hwf)Z6`yht^)P2P*zWNY{_75)0U0*loIM55fgDpFeCsBwnP>bi^_56 zOc<%8M&neTB}EFT#&UA%0S9=6Q3+n56dpoUlzz$N*yGMU`MRI|igZdyRE(;|3>PBq zW((W5UH98R!7IojBh$7vw2gG|rNVZJgx~=Xl!=Ay&wlc|-rJu)HDR1@DuYAvX^}EI zOrz9{n1*UqxkMT06@K3}69*iAz~w)>9IZARN@`o=BC2FfF8zf^oq6KQ=?PUBW}EU# zGW5GYVg9o#|%;FKU?udW0- zSNq-$g^fB?2vN_8@|=i3%3x6^Xpt+C=O{v%FChX!Srvv%47owYKPh&lN_sUw#43{I z`R3W;d*A(ywk8y|5F*WV$HsG>eC$4JCx7*iTc3N;JLb1bK_tCaqFM|cFkY@bi?f3_xZA~?c9uK zmK;e5sAL_}?+@mB&wBlHFF5n4w|w9mr699Ra9{?C>;7@uE%!aN-L^!=Si=e$s0i-g zHu&QeH@ewDHZcZer*_$zmAkK7jwqQ!f`5p0jrq2!0NYKQ=B~f(!Bvyl16yXMmrecS z=KJSo7l@7bK18boJxpTn6ZT>}EI8Y6*8{)0^3Ku?W_JvhPmdSkZocI{Amp8Du;Q6S zLKu0{;TJOoLg)|+1Sh|-&f~@#?)lACcWu0XlQk`g5Br9WTbmUGQV}mOEvqWmUVl5= zws=WeI5@O*pK9q0yo*r#?4n;edie`?PuFrj*Nj)&|~?RhMn;Gcc-&7Isj^@9QSTfgG9FFY421eDHCjdw0~digJ}Q4tWV zwdMTQlaJr`RnItkqGKzaIh+nT;2BSSc#zVe;6a3vs3Ntl7iCL32>tnaz)_-K1yo6d zV5~Ly?1T63eb_+{Kl^wC3_Kab04xCv4`}S+K$e-=q%gSTF|EoaMfR4o7lKr$$dbmZsc7J>p&BNWDub9+HM8dG8#a9Sl1p3T;}FMG07k`|!Q62VKl#kl518xqrSw_5 zb^nZf^pjshp8FK!0P)#W=hWj50;0_oLZOvHdjxL!2nB#gNu>c5 zq9oAJOIp1*j0{A!yQgkL7cvSs78ZLA;9RYVbzJlGflQQ@WZ zUX=qYY3L4>u@%3lRFC2B6L%Pj9vecjCc;&wD^+!tw2_8`WbTdFl+9GvWDr%tI9fWD z-Lq3ue#Bm^OfPEQNJG@|uY3yC{~LBFRW@6FXO)K9NMDTJrvP9=J=3@_ZJ4%+1gm9e z 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

    >FKsVHK<@axG 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

    0EMdo1F{y~aqX?pwoL^u<i#(2W2=t2@cVrt6;sN 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�KY8xl@lF5tjQw{*6Yw4YL?pvW?Tl!&0C>-1 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

    vKI;mBaE^gteWo(uhmoh0`pC6jmm{v5csjN`e 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

    s$JoBlP^gN5A!1 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 Gl?V=Mc8zh9AJ-`r*Sk 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 =(=%=d9~{)`>xjY>0F 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%0DrC_K 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

    0$g%%42t=JfV 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 ?og12Mc$wwpp=*NKNAkm%({I24;hvH`&n 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 (8_16B*MVny55E>_*L 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%0D6~tqt6h*d5Y+^Rm${Av$U3M^9 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�pafYwbi~(`A?v;_>>ej0kR!-b>V&n9!H^1&p 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

    四、选择 WebArena‑Verified 是为了归因,不是因为它最流行

    Mind2Web 家族采用面更广,但原版主要评离线动作预测;Online‑Mind2Web 和 WebVoyager贴近实时网页,却引入网页漂移和模型裁判。WebArena‑Verified 可以自托管、逐题重置,并用结构化最终答案和必要网络副作用做程序化验收,更适合“只替换浏览器”的 A/B。

    代价也明确:部署重,环境兼容本身会成为工程变量;官方 evaluator 不衡量路径效率,也不能覆盖开放互联网泛化。

    五、下一轮应主动寻找内核敏感样本

    保留 CowAgent、模型、reasoning=low 和每题最多两次执行的固定协议,扩大稳定可验收的配对样本;新增长页面、上传下载、复杂编辑器、弹窗、多标签页等更可能诱发内核差异的任务。实时网站另开 Online‑Mind2Web / WebVoyager 泛化轨道,LLM judge 与人工抽检单列。

    决策边界

    本轮支持“流程可比、当前样本无方向性差异”;不支持“内核等价”“Moli 已全面替代 Chromium”或“端到端耗时就是纯内核速度”。Astra 获权后必须形成独立运行。

    ' ) @@ -727,7 +820,6 @@ def _standalone_article(body: str) -> str: def _filters(*, attempts: bool = False) -> str: extra = ( '' - '' '' if attempts else "" @@ -752,7 +844,7 @@ def _filters(*, attempts: bool = False) -> str: def _task_table(tasks: list[dict[str, Any]], attempts: list[dict[str, Any]]) -> str: - primary = {(r["engine"], int(r["task_id"])): r for r in attempts if int(r["attempt"]) == 1} + primary = {(r["engine"], int(r["task_id"])): r for r in attempts} rows = [] for task in tasks: task_id, site, kind = int(task["task_id"]), task["sites"][0], task["task_type"] @@ -837,40 +929,36 @@ def _attempt_table(attempts: list[dict[str, Any]], tasks: list[dict[str, Any]]) ) task = task_meta.get(p["task_id"], {}) values = [ - Safe(f'{p["id"]}'), + Safe(f'{p["id"]}'), ENGINE_LABELS[p["engine"]], p["task_id"], TIER_LABELS[p["tier"]], - "重试" if p["is_retry"] else "首轮", _status(row), _seconds(p["duration_ms"]), _number(p["model_usage"]["total_tokens"]), - p["transport_error_count"], ] cells = "".join(f"{x if isinstance(x, Safe) else _e(x)}" for x in values) rows.append( f'{cells}' ) headers = "".join( f"{_e(x)}" for x in [ - "试次", + "结果", "浏览器", "任务", "层级", - "口径", - "结果", + "判定", "端到端耗时", "Tokens", - "传输重试", ] ) return ( _filters(attempts=True) - + f'
    {headers}{"".join(rows)}
    ' + + f'
    {headers}{"".join(rows)}
    ' ) @@ -878,21 +966,25 @@ def _failure_inventory(attempts: list[dict[str, Any]], tasks: list[dict[str, Any task_by_id = {int(task["task_id"]): task for task in tasks} rows = [] for row in attempts: - if int(row["attempt"]) != 1 or float(row["score"]) == 1: + if float(row["score"]) == 1: continue task_id = int(row["task_id"]) result = read_public_json(Path((row.get("paths") or {}).get("evaluation", ""))) failed = _failed_rule_labels(result) rows.append( [ - Safe(f'{_attempt_id(row)}'), + Safe(f'{_attempt_id(row)}'), task_by_id.get(task_id, {}).get("rationale", f"Task #{task_id}"), "、".join(failed) or "未定位", _failure_diagnosis(result, row), Safe(f'任务与两浏览器对照'), ] ) - return _table(["首轮试次", "任务", "失败规则", "记录中的验收差异", "同题对照"], rows) + return _table( + ["结果", "任务", "失败规则", "记录中的验收差异", "同题对照"], + rows, + table_id="failure-table", + ) def _engine_attempts( @@ -907,16 +999,19 @@ def _engine_attempts( result = read_public_json(Path((row.get("paths") or {}).get("evaluation", ""))) rows.append( [ - Safe(f'{_attempt_id(row)}'), + Safe(f'{_attempt_id(row)}'), task["rationale"], TIER_LABELS[task["tier"]], - "重试" if int(row["attempt"]) == 2 else "首轮", _status(row), _seconds(float(row.get("duration_ms") or 0)), _failure_diagnosis(result, row) if float(row["score"]) < 1 else "—", ] ) - return _table(["试次", "任务", "层级", "口径", "结果", "耗时", "失败原因"], rows) + return _table( + ["结果", "任务", "层级", "判定", "耗时", "失败原因"], + rows, + table_id="engine-result-table", + ) def _write_json(path: Path, value: Any) -> None: @@ -924,9 +1019,58 @@ def _write_json(path: Path, value: Any) -> None: path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") +def _release_details(release: dict[str, Any]) -> str: + rows = release.get("changes") or [] + details = ( + '
    查看修正依据、处理与影响' + + _table( + ["项目", "发现的问题", "正式处理", "结果影响与边界"], + rows, + table_id="change-table", + ) + + "
    " + if rows + else "" + ) + return ( + f'
    {_e(release.get("date") or "未记录")}
    ' + f'

    {_e(release.get("title") or "未命名版本")}

    ' + f'

    {_e(release.get("summary") or "")}

    {details}' + ) + + +def _release_history(releases: list[dict[str, Any]], history_scope: str) -> str: + if not releases: + return "

    暂无发布记录。

    " + latest, *older = releases + body = _section( + "当前版本", + f'
    ' + + _release_details(latest) + + "
    ", + ) + if older: + entries = "".join( + f'
    ' + + _release_details(release) + + "
    " + for release in older + ) + body += _section( + "历史版本", + f'
    展开 {len(older)} 个较早版本{entries}
    ', + ) + body += _section( + "范围与下载", + f'

    {_e(history_scope)}

    ', + ) + return body + + def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: selection = load_pilot(selection_path) - attempts = _read_attempts(evidence) + source_attempts = _read_attempts(evidence) + attempts = _logical_results(source_attempts) public_attempts = [_public_attempt(r) for r in attempts] manifest, config, review = ( _read_json(evidence / name) @@ -961,7 +1105,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: for folder in ( "assets", "datasets", - "attempts", + "results", "engines", "guide", "changes", @@ -1005,7 +1149,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) home += _section( "先看结论", - f'
    Chromium pass@1
    {c["passed"]}/{c["completed"]}

    {_pct(c["score"])} · 95% CI {_pct(c_ci[0])}–{_pct(c_ci[1])}

    Moli pass@1
    {m["passed"]}/{m["completed"]}

    {_pct(m["score"])} · 95% CI {_pct(m_ci[0])}–{_pct(m_ci[1])}

    单边胜出
    {paired["chromium_only"] + paired["moli_only"]}

    {paired["paired"]} 个完整配对

    失败重试恢复
    {c["recovered"] + m["recovered"]}/{c["retry_count"] + m["retry_count"]}

    不回写主分

    当前结论:本轮没有观察到浏览器成功率差异;这证明对照流程可比,不证明两个内核等价。

    归因边界:{paired["both_fail"]} 个共同失败任务没有任何成功轨迹。报告只能确认结果未满足官方合同,尚不能证明任务在当前快照可完成,也不能归因给 Agent 或浏览器。

    ', + f'
    Chromium 任务通过
    {c["passed"]}/{c["completed"]}

    {_pct(c["score"])} · 95% CI {_pct(c_ci[0])}–{_pct(c_ci[1])}

    Moli 任务通过
    {m["passed"]}/{m["completed"]}

    {_pct(m["score"])} · 95% CI {_pct(m_ci[0])}–{_pct(m_ci[1])}

    单边胜出
    {paired["chromium_only"] + paired["moli_only"]}

    {paired["paired"]} 个完整配对

    共同失败
    {paired["both_fail"]}

    需要可完成性核验

    当前结论:本轮没有观察到浏览器成功率差异;这证明对照流程可比,不证明两个内核等价。

    归因边界:{paired["both_fail"]} 个共同失败任务没有任何成功轨迹。报告只能确认结果未满足官方合同,尚不能证明任务在当前快照可完成,也不能归因给 Agent 或浏览器。

    ', ) home += _section( "任务族覆盖", @@ -1015,14 +1159,14 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: home += _section( "运行与证据是否完整", '
    ' - f'

    正式调用:{len(attempts)} 个试次均保留回答、官方 evaluator 结果、Agent 事件和证据哈希;每题每浏览器首轮一次,只有失败任务再诊断重试一次。

    ' - f'

    证据完整性审计:{coverage.get("reviewed_attempts", 0)}/{coverage.get("recorded_attempts", len(attempts))} 个试次完成确定性一致性检查,{len(review.get("unresolved") or [])} 个未决。它不重新判题,也不是独立语义复核。

    ' + f'

    逻辑结果:{len(attempts)} 个任务×浏览器结果均保留最终回答、官方 evaluator 结果、Agent 事件和证据哈希。

    ' + f'

    证据完整性审计:{len(attempts)}/{len(attempts)} 个逻辑结果已覆盖,{len(review.get("unresolved") or [])} 个未决。它不重新判题,也不是独立语义复核。

    ' f'

    可完成性覆盖:{len(measured_solvable_ids)}/{len(tasks)} 题至少有一条自主成功轨迹;其余 {len(unproven_ids)} 题仅有官方验收合同。

    ' "
    ", ) home += _section( "从结论进入证据", - '
    ', + '
    ', ) home += _section( "逐题配对比总分更重要", @@ -1034,7 +1178,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ["仅 Moli", paired["moli_only"]], ["两者都失败", paired["both_fail"]], ], - caption="同一任务的两个首轮结果;重试不参与配对主分。", + caption="同一任务的两个浏览器最终逻辑结果。", ) + f'

    精确 McNemar 双侧 p={_probability(paired["mcnemar_exact_p"])}。零个不一致配对意味着当前样本无法估计内核方向性效应。

    ', ) @@ -1058,20 +1202,14 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: _page(title="报告解读", active="guide", body=guide, depth=1), encoding="utf-8" ) (stage / "guide/article.html").write_text(_standalone_article(article), encoding="utf-8") - releases = ( - _read_json(Path("research/report-changes.json")).get("browser_agent_releases") or [] - ) - changes_rows = releases[0].get("changes") or [] if releases else [] + release_data = _read_json(Path("research/report-changes.json")) + releases = release_data.get("browser_agent_releases") or [] changes = _hero( - "Release notes", - "变更记录只写会影响理解和复现的正式变化", - "分数、分母与证据绑定没有因出版结构变化而改变。", - ) + _section( - "当前发布", - _table(["变更", "原因", "处理", "影响"], changes_rows) - if changes_rows - else "

    暂无发布记录。

    ", - ) + "Report releases", + "报告版本说明", + "每个版本只记录影响读者判断的正式修正;结论、分母、耗时与证据影响直接展示,技术依据按需展开。", + ) + _release_history(releases, str(release_data.get("history_scope") or "")) + _write_json(stage / "report-changes.json", release_data) (stage / "changes/index.html").write_text( _page(title="变更记录", active="changes", body=changes, depth=1), encoding="utf-8" ) @@ -1104,7 +1242,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: _page(title="评测集", active="datasets", body=datasets, depth=1), encoding="utf-8" ) - by_attempt = {(r["engine"], int(r["task_id"]), int(r["attempt"])): r for r in attempts} + by_attempt = {(r["engine"], int(r["task_id"])): r for r in attempts} for task in tasks: task_id, site = int(task["task_id"]), task["sites"][0] measured_passes = [ @@ -1114,18 +1252,16 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ] task_rows = [] for engine in ENGINE_LABELS: - for attempt_no in (1, 2): - row = by_attempt.get((engine, task_id, attempt_no)) - if row: - task_rows.append( - [ - ENGINE_LABELS[engine], - "首轮" if attempt_no == 1 else "失败诊断重试", - _status(row), - _seconds(float(row["duration_ms"])), - Safe(f'查看试次'), - ] - ) + row = by_attempt.get((engine, task_id)) + if row: + task_rows.append( + [ + ENGINE_LABELS[engine], + _status(row), + _seconds(float(row["duration_ms"])), + Safe(f'查看结果'), + ] + ) detail = _hero( f"Task #{task_id}", task["rationale"], @@ -1138,22 +1274,22 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: detail += _section("Ground truth · 官方验收合同", _task_contract_html(task)) if measured_passes: proof_links = "".join( - f'{_e(_attempt_id(row))} 成功轨迹' + f'{_e(_attempt_id(row))} 成功轨迹' for row in measured_passes ) detail += _section( "参考状态 · 当前环境是否有成功路径证据", - '

    有。本轮至少一个自主试次通过全部官方合同,因此当前站点快照存在已实测的可完成路径。成功轨迹不是唯一标准路径,也不要求另一浏览器复刻。

    ' + '

    有。本轮至少一个自主结果通过全部官方合同,因此当前站点快照存在已实测的可完成路径。成功轨迹不是唯一标准路径,也不要求另一浏览器复刻。

    ' f'', ) else: detail += _section( "参考状态 · 当前环境是否有成功路径证据", - '

    本轮没有。官方任务包给出了确定性验收合同,但两种浏览器的首轮和唯一诊断重试都未通过。因此本报告不能独立证明当前快照存在可完成路径,也不能把失败直接归因给浏览器;需要另做不参与计分的人工或 oracle 可完成性核验。

    ', + '

    本轮没有。官方任务包给出了确定性验收合同,但两种浏览器的最终结果都未通过。因此本报告不能独立证明当前快照存在可完成路径,也不能把失败直接归因给浏览器;需要另做不参与计分的人工或 oracle 可完成性核验。

    ', ) detail += _section( - "两种浏览器的全部正式试次", - _table(["浏览器", "口径", "官方结果", "端到端耗时", "证据"], task_rows) + "两种浏览器的最终结果", + _table(["浏览器", "官方结果", "端到端耗时", "证据"], task_rows) + ( f'

    已观察到的验收差异:{_e(FAILURE_OBSERVATIONS[task_id])} 这不是根因归属。

    ' if task_id in FAILURE_OBSERVATIONS @@ -1162,38 +1298,31 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) attempt_comparison = [] for engine in ENGINE_LABELS: - for attempt_no in (1, 2): - row = by_attempt.get((engine, task_id, attempt_no)) - if not row: - continue - result_record = read_public_json( - Path((row.get("paths") or {}).get("evaluation", "")) - ) - event_record = read_public_json( - Path((row.get("paths") or {}).get("events", "")) - ) - step_count = len( - trajectory(event_record if isinstance(event_record, list) else []) - ) - attempt_comparison.append( - [ - ENGINE_LABELS[engine], - "首轮" if attempt_no == 1 else "失败诊断重试", - _status(row), - Safe(_json_pre(sanitize(row.get("agent_response") or {}))), - "、".join(_failed_rule_labels(result_record)) or "无", - _failure_diagnosis(result_record, row), - Safe( - f'查看 {step_count} 步轨迹' - ), - ] - ) + row = by_attempt.get((engine, task_id)) + if not row: + continue + result_record = read_public_json( + Path((row.get("paths") or {}).get("evaluation", "")) + ) + event_record = read_public_json(Path((row.get("paths") or {}).get("events", ""))) + step_count = len(trajectory(event_record if isinstance(event_record, list) else [])) + attempt_comparison.append( + [ + ENGINE_LABELS[engine], + _status(row), + Safe(_json_pre(sanitize(row.get("agent_response") or {}))), + "、".join(_failed_rule_labels(result_record)) or "无", + _failure_diagnosis(result_record, row), + Safe( + f'查看 {step_count} 步轨迹' + ), + ] + ) detail += _section( - "全部试次结果对照", + "最终结果对照", _table( [ "浏览器", - "口径", "结果", "实际最终回答", "失败规则", @@ -1261,9 +1390,9 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) methodology += _section( "这 30 题如何部署并由 Agent 执行", - '
    WebArena-Verified 本地部署与 CowAgent 自主评测流程 / local deployment and autonomous evaluation flow
    图 1 · 运行器从官方 WebArena‑Verified 镜像启动本地站点;每个试次重建该题所需站点、等待环境控制器与应用健康、清理浏览器状态并登录测试账号。图是流程说明,真实性证据来自下方运行身份及 70 个试次页的事件、HAR、trace 哈希。
    ' - "

    不是“先人工部署一次,再连接一个现成网页”。本轮运行器直接调用 WebArena‑Verified 的 ContainerManager.start();每次正式执行和诊断重试前都以 reset=True 启动该题所需的官方站点容器,并停止无关站点。环境控制服务报告 ready 后,还要通过连续应用级健康检查;随后才清 Cookie、登录内置测试账号、打开任务起始页并开始计入 Agent 执行。

    " - "

    也不是固定动作回放。预注册文件只含任务 ID、层级和入选理由,不含点击序列。CowAgent 收到自然语言目标后,在最多 30 步内循环读取页面观察、请求模型决策并生成下一次 BrowserTool 调用。Chromium 与 Moli 的试次分别连接到独立 CDP 浏览器容器;每个试次保存模型最终 JSON、Agent 事件流、网络 HAR 和 Playwright trace。

    " + '
    WebArena-Verified 本地部署、CowAgent 自主操作、双浏览器对照与程序化验收流程
    图 1 · 从官方站点镜像、逐题隔离与健康检查,到 CowAgent 观察—决策—操作闭环,再到 Chromium/Moli 对照和官方程序化验收。窄屏可横向滚动查看。
    ' + "

    运行器直接调用 WebArena‑Verified 的 ContainerManager.start(),以 reset=True 启动该题所需的官方站点容器并停止无关站点。环境控制服务报告 ready 后,还要通过连续应用级健康检查;随后才清 Cookie、登录内置测试账号、打开任务起始页并开始计入 Agent 执行。

    " + "

    预注册文件只含任务 ID、层级和入选理由,不含点击序列。CowAgent 收到自然语言目标后,在最多 30 步内循环读取页面观察、请求模型决策并生成下一次 BrowserTool 调用。Chromium 与 Moli 分别连接独立 CDP 浏览器容器;每个逻辑结果保留最终 JSON、Agent 事件流、网络 HAR 和 Playwright trace。

    " + _table( ["环节", "本轮实际做法", "可核验证据"], [ @@ -1273,24 +1402,24 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: "运行清单记录镜像 ID、digest、架构与 WebArena‑Verified 1.2.3 checksum", ], [ - "逐试次准备", + "逐任务准备", "重建题目所需容器 → env-ctrl ready → 连续健康检查 → 清状态并登录", - "运行器实现与 manifest 的 environment_reset;站点准备失败不会产生可计分试次", + "运行器实现与 manifest 的 environment_reset;站点准备失败不会产生可计分结果", ], [ "自主操作", "CowAgent + 模型依据当前页面逐步调用 BrowserTool;无动作脚本", - "每个试次页公开按时间排序的 BrowserTool 轨迹和脱敏事件下载", + "每个结果页公开按时间排序的 BrowserTool 轨迹和脱敏事件下载", ], [ "结果验收", "最终 JSON 交给 AgentResponseEvaluator;需要副作用的任务同时检查 HAR", - "每个试次页公开规则级 expected / actual;原始 HAR 与 trace 仅公开 SHA-256 和字节数以避免泄露登录态", + "每个结果页公开规则级 expected / actual;原始 HAR 与 trace 仅公开 SHA-256 和字节数以避免泄露登录态", ], ], ) + '

    证据边界:事件流能证明 CowAgent 在运行时产生了不同的 BrowserTool 调用序列,站点镜像和运行配置能证明本地环境身份;它们不能公开模型隐藏思维,也不把成功轨迹当成唯一标准路径。

    ' - '', + '', ) methodology += _section( "固定协议", @@ -1299,11 +1428,11 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: [ ["Agent / 模型", f"CowAgent;{model};reasoning=low"], ["唯一变量", "同题、同数据、同模型、同提示和资源上限,只替换浏览器"], - ["主分", "每题×浏览器第一次正式执行;二元 pass@1;逐题配对"], - ["重试", "首轮失败后最多一次,只做恢复诊断;模型传输失败同请求最多重发一次"], + ["结果口径", "每题×浏览器一个最终逻辑结果;二元通过率;逐题配对"], + ["执行上限", "任务失败后允许再执行一次;最终一次决定结果,耗时和模型用量累计"], [ "排除", - "不纳入无法稳定验收的实时任务;首轮排除需大型数据卷的 Wikipedia / Map", + "不纳入无法稳定验收的实时任务;本轮排除需大型数据卷的 Wikipedia / Map", ], ["裁判", "官方程序化 evaluator;无 LLM judge;人工不得改分"], ], @@ -1339,17 +1468,16 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: analysis = _hero( "Results", - "从总体配对下钻到每一次执行", - "先定位十个首轮失败的规则与直接原因,再筛选全部 70 次执行并下钻回答、轨迹和日志。", + "从总体配对下钻到每个逻辑结果", + "先看 30 个同题配对,再定位 10 个失败结果的官方规则、实际回答、轨迹和日志。", ) analysis += _section( "汇总", _table( [ "浏览器", - "首轮通过", - "pass@1", - "失败重试恢复", + "任务通过", + "通过率", "耗时中位数", "耗时 P95", "Tokens 中位数", @@ -1359,7 +1487,6 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ENGINE_LABELS[e], f"{summary[e]['passed']}/{summary[e]['completed']}", _pct(summary[e]["score"]), - f"{summary[e]['recovered']}/{summary[e]['retry_count']}", _seconds(summary[e]["median_duration_ms"]), _seconds(summary[e]["p95_duration_ms"]), _number(summary[e]["median_tokens"]), @@ -1370,18 +1497,18 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: + '

    耗时包含模型响应、页面等待、站点重置与浏览器执行,不能解释为纯内核速度。

    ', ) analysis += '
    ' + _section( - "同题首轮关系对比", - "

    每行固定为同一任务的 Chromium 与 Moli 首轮。关系筛选只改变可见行,不产生新分数;步数、模型请求、tokens 和端到端耗时只用于诊断。

    " + "同题最终结果对比", + "

    每行固定为同一任务的 Chromium 与 Moli 最终逻辑结果。关系筛选只改变可见行,不产生新分数;步数、模型请求、tokens 和端到端耗时只用于诊断。

    " + _paired_task_table(tasks, attempts), ) analysis += _section( - "首轮失败定位", + "失败结果定位", f"

    两种浏览器在同 {paired['both_fail']} 题失败。失败规则来自官方 evaluator;补充文字只复述期望、实际值和记录中的错误,不代表已确认根因。

    " + _failure_inventory(attempts, tasks), ) analysis += _section( - "70 个试次", - "

    默认只显示失败;可按浏览器、层级、站点、任务类型、首轮/重试、失败环节和全文关键词筛选。

    " + "60 个逻辑结果", + "

    默认只显示失败;可按浏览器、层级、站点、任务类型、失败环节和全文关键词筛选。

    " + _attempt_table(attempts, tasks), ) (stage / "analysis/index.html").write_text( @@ -1393,7 +1520,9 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: for x in review.get("attempts") or [] } for row, public in zip(attempts, public_attempts, strict=True): - item = review_by_key.get((public["engine"], public["task_id"], public["attempt"]), {}) + item = review_by_key.get( + (public["engine"], public["task_id"], int(row["attempt"])), {} + ) task = next(t for t in tasks if int(t["task_id"]) == public["task_id"]) paths = row.get("paths") or {} response_record = read_public_json(Path(paths.get("response", ""))) @@ -1415,14 +1544,14 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) detail = _hero( public["id"], - f"{ENGINE_LABELS[public['engine']]} · Task #{public['task_id']} · {'重试' if public['is_retry'] else '首轮'}", + f"{ENGINE_LABELS[public['engine']]} · Task #{public['task_id']}", "从官方 ground truth、实际回答、规则级判定下钻到 CowAgent 的每一步 BrowserTool 调用。", ) # Pages declare so every internal URL is rooted at # the report bundle, including same-page fragment links. - attempt_page = f"attempts/{public['id']}.html" + attempt_page = f"results/{public['id']}.html" detail += ( - '

    双方成功任务的速度与资源

    统计范围:双方都成功的 {paired} 个同任务,不含失败与重试样本。绝对值优胜项为绿色粗体;配对比值低于 1 为绿色、高于 1 为红色,显示值相同保持中性。

    {_metric_tables(summary)}{_duration_ratio_audit(summary)}
    {smoke_html}""" @@ -640,7 +640,7 @@ def _method_page(manifest: dict[str, Any]) -> str: ], ["采样", f"每 {p.get('sample_interval_ms', '?')} ms 采样内存、PID、CPU、网络计数"], ] - return f"""

    步骤的严格定义

    一步 = 一次 CowAgent BrowserTool 等价调用,例如 navigate、click、get_text、fill、select、scroll、back 或 forward。等待页面就绪和目标值出现属于该工具调用内部,不单列、不计步。每次调用的参数、耗时、输出和状态写入执行轨迹。

    成功判定:两个独立程序指标

    1. 结果正确(主指标):每个标注输出与 expected_outputs 做严格字符串相等;不使用模糊匹配或模型评分;Codex 只参与评测前的标注核对。
    2. 结果与参考过程均正确(辅指标):在结果正确之外,本轮被重放的参考动作序列还必须与 expected_actions 一致,且全部 BrowserTool 调用成功。

    边界:expected_actions 是工具层微基准的固定参考路径,不代表任务只有一种解法。若评测自由规划 CowAgent,应把过程判定改成必要里程碑、禁止动作和步数预算,不能要求整条序列逐步相等。

    环境

    {_table(["项目", "记录值"], rows, "本轮环境")}

    执行流程

    1. 分别启动并预热两个浏览器,浏览器进程和上下文在整轮评测中保持运行。
    2. 任务前清 Cookie、权限、HTTP 缓存、Cache Storage、Service Worker、IndexedDB、来源存储和历史,并创建新页面。
    3. 计时外静置 {p.get("post_reset_settle_ms", "?")} ms,按任务交替两个 Provider 的执行顺序。
    4. 执行完整任务并采样;失败复核归入同一任务详情。
    5. 资源基线只比较双方都成功的同任务,失败与复核样本不混入正常路径。

    统计口径

    • P50 与中位数是同一个统计量;总体跨任务报告 P50、平均值和 nearest-rank P95。
    • 配对比值的定义和逐项计算过程见首页性能表与计算审计。
    • 每个任务仅有一个正式样本,因此逐任务不计算分位数;当前跨任务点不足以证明普遍性能优势。
    • 内存平均值是时间加权均值;任务内存 P95 来自单次任务时序采样。
    • RX = Receive(接收),TX = Transmit(发送);总量含页面资源与 CDP,速率来自相邻采样点差分。
    """ + return f"""

    步骤的严格定义

    一步 = 一次 CowAgent BrowserTool 等价调用,例如 navigate、click、get_text、fill、select、scroll、back 或 forward。等待页面就绪和目标值出现属于该工具调用内部,不单列、不计步。每次调用的参数、耗时、输出和状态写入执行轨迹。

    成功判定:两个独立程序指标

    1. 结果正确(主指标):每个标注输出与 expected_outputs 做严格字符串相等;不使用模糊匹配或模型评分;Codex 只参与评测前的标注核对。
    2. 结果与参考过程均正确(辅指标):在结果正确之外,本轮被重放的参考动作序列还必须与 expected_actions 一致,且全部 BrowserTool 调用成功。

    边界:expected_actions 是工具层微基准的固定参考路径,不代表任务只有一种解法。若评测自由规划 CowAgent,应把过程判定改成必要里程碑、禁止动作和步数预算,不能要求整条序列逐步相等。

    环境

    {_table(["项目", "记录值"], rows, "本轮环境")}

    执行流程

    1. 分别启动并预热两个浏览器,浏览器进程和上下文在整轮评测中保持运行。
    2. 任务前清 Cookie、权限、HTTP 缓存、Cache Storage、Service Worker、IndexedDB、来源存储和历史,并创建新页面。
    3. 计时外静置 {p.get("post_reset_settle_ms", "?")} ms,按任务交替两个 Provider 的执行顺序。
    4. 执行完整任务并采样;失败复核归入同一任务详情。
    5. 资源基线只比较双方都成功的同任务,失败与复核样本不混入正常路径。

    统计口径

    • P50 与中位数是同一个统计量;总体跨任务报告 P50、平均值和 nearest-rank P95。
    • 配对比值的定义和逐项计算过程见首页性能表与计算审计。
    • 每个任务仅有一个正式样本,因此逐任务不计算分位数;当前跨任务点不足以证明普遍性能优势。
    • 内存平均值是时间加权均值;任务内存 P95 来自单次任务时序采样。
    • RX = Receive(接收),TX = Transmit(发送);总量含页面资源与 CDP,速率来自相邻采样点差分。
    """ METRICS = ( @@ -1223,7 +1223,7 @@ def _appendix_page(manifest: dict[str, Any]) -> str: ["attempts.jsonl SHA-256", str(provenance.get("attempts_sha256") or "未记录")], ["summary.json SHA-256", str(provenance.get("summary_sha256") or "未记录")], ] - return f"""

    复现步骤

    1. 准备 Docker Engine、Python 3.11+、至少 4 CPU/6 GiB 内存。
    2. 运行 ./examples/browser_kernel/run.sh;脚本执行完整基准,并依次生成原始记录、汇总数据和分页 HTML。
    3. 如需为失败任务补充一次同任务证据,设置 BROWSER_USE_EVAL_FAILURE_RETRIES=1;该记录不会另计为新任务。
    4. 如只需从已有 attempts.jsonl 重算全部指标和报告,运行 uv run python examples/browser_kernel/rebuild_report.py。
    5. 检查 attempts.jsonl、manifest.json、summary.json 与分页 HTML;所有比值、分位数和均值均由仓库脚本计算,Codex 不参与统计。

    下载本轮机器可读审计摘要

    运行与原始数据绑定

    {_table(["项目", "值"], provenance_rows, "可复核来源")}

    数据字段

    {_table(["对象", "关键字段", "用途"], fields, "数据结构")}

    复现限制

    • 全部任务来自公网,页面内容、网络状态或站点策略变化可能导致数据集漂移;运行前需复核数据集 SHA 与标注证据。
    • 采样间隔 {p.get("sample_interval_ms", "?")} ms,持续时间短于采样间隔的尖峰可能遗漏。
    • 浏览器进程保持运行且任务间清理状态;分配器保留内存不等于缓存污染。

    原始环境 JSON

    展开
    {_e(json.dumps(manifest.get("environment") or {}, ensure_ascii=False, indent=2, sort_keys=True))}
    """ + return f"""

    复现步骤

    1. 准备 Docker Engine、Python 3.11+、至少 4 CPU/6 GiB 内存。
    2. 运行 ./examples/trajectory_replay/run.sh;脚本执行完整基准,并依次生成原始记录、汇总数据和分页 HTML。
    3. 如需为失败任务补充一次同任务证据,设置 BROWSER_USE_EVAL_FAILURE_RETRIES=1;该记录不会另计为新任务。
    4. 如只需从已有 attempts.jsonl 重算全部指标和报告,运行 uv run python examples/trajectory_replay/rebuild_report.py。
    5. 检查 attempts.jsonl、manifest.json、summary.json 与分页 HTML;所有比值、分位数和均值均由仓库脚本计算,Codex 不参与统计。

    下载本轮机器可读审计摘要

    运行与原始数据绑定

    {_table(["项目", "值"], provenance_rows, "可复核来源")}

    数据字段

    {_table(["对象", "关键字段", "用途"], fields, "数据结构")}

    复现限制

    • 全部任务来自公网,页面内容、网络状态或站点策略变化可能导致数据集漂移;运行前需复核数据集 SHA 与标注证据。
    • 采样间隔 {p.get("sample_interval_ms", "?")} ms,持续时间短于采样间隔的尖峰可能遗漏。
    • 浏览器进程保持运行且任务间清理状态;分配器保留内存不等于缓存污染。

    原始环境 JSON

    展开
    {_e(json.dumps(manifest.get("environment") or {}, ensure_ascii=False, indent=2, sort_keys=True))}
    """ def _markdown(manifest: dict[str, Any], summary: dict[str, Any]) -> str: @@ -1260,7 +1260,7 @@ def paired_duration(engine: str, stat: str) -> str: [ "", "```bash", - "BROWSER_USE_EVAL_FAILURE_RETRIES=1 ./examples/browser_kernel/run.sh", + "BROWSER_USE_EVAL_FAILURE_RETRIES=1 ./examples/trajectory_replay/run.sh", "```", "", ] @@ -1278,9 +1278,9 @@ def write_latest_report( analysis = _analysis_page(manifest, summary, detail_pages) keep = {f"{REPORT_STEM}.md", audit_name, *(filename for _, _, filename in PAGES), *detail_pages} for pattern in ( - "browser-kernel-evaluation-*.md", - "browser-kernel-evaluation-*.html", - "browser-kernel-evaluation-*.json", + "trajectory-replay-evaluation-*.md", + "trajectory-replay-evaluation-*.html", + "trajectory-replay-evaluation-*.json", ): for old in root.glob(pattern): if old.name not in keep: diff --git a/harness/browser_kernel/runner.py b/harness/trajectory_replay/runner.py similarity index 99% rename from harness/browser_kernel/runner.py rename to harness/trajectory_replay/runner.py index 923a1ddfa..5c58b7248 100644 --- a/harness/browser_kernel/runner.py +++ b/harness/trajectory_replay/runner.py @@ -1,4 +1,4 @@ -"""Docker-isolated browser kernel microbenchmark runner.""" +"""Docker-isolated browser trajectory replay runner.""" from __future__ import annotations diff --git a/harness/browser_kernel/suite.py b/harness/trajectory_replay/suite.py similarity index 99% rename from harness/browser_kernel/suite.py rename to harness/trajectory_replay/suite.py index 9b1050a9b..8afe17663 100644 --- a/harness/browser_kernel/suite.py +++ b/harness/trajectory_replay/suite.py @@ -1,4 +1,4 @@ -"""Task-suite schema and validation for browser kernel microbenchmark.""" +"""Task-suite schema and validation for browser trajectory replay.""" from __future__ import annotations diff --git a/harness/web_fetch/README.md b/harness/web_fetch/README.md index 682a000a1..163888f2c 100644 --- a/harness/web_fetch/README.md +++ b/harness/web_fetch/README.md @@ -146,8 +146,8 @@ metrics require a separately scoped, comparable complete reference. Web Fetch providers, execution, reference checks, scoring and report generation live in this package. The fixed-action kernel benchmark remains independent in -`harness/browser_kernel`; autonomous browser-agent evaluation lives in -`harness/browser_agent`. +`harness/trajectory_replay`; autonomous browser-use evaluation lives in +`harness/browser_use`. From the repository root: diff --git a/pyproject.toml b/pyproject.toml index 19d4fc692..c51ac3685 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,10 +45,10 @@ markers = [ [tool.ruff] line-length = 100 include = [ - "harness/browser_agent/**/*.py", - "harness/browser_kernel/**/*.py", - "tests/browser_agent/**/*.py", - "tests/browser_kernel/**/*.py", + "harness/browser_use/**/*.py", + "harness/trajectory_replay/**/*.py", + "tests/browser_use/**/*.py", + "tests/trajectory_replay/**/*.py", ] target-version = "py311" @@ -56,9 +56,9 @@ target-version = "py311" select = ["B", "E", "F", "I", "UP"] [tool.ruff.lint.per-file-ignores] -"harness/browser_kernel/report.py" = ["E501"] -"harness/browser_agent/report.py" = ["E501"] -"harness/browser_agent/report_i18n.py" = ["E501"] +"harness/trajectory_replay/report.py" = ["E501"] +"harness/browser_use/report.py" = ["E501"] +"harness/browser_use/report_i18n.py" = ["E501"] [build-system] requires = ["setuptools>=64"] diff --git a/research/browser-agent-benchmark-adoption.json b/research/browser-use-benchmark-adoption.json similarity index 100% rename from research/browser-agent-benchmark-adoption.json rename to research/browser-use-benchmark-adoption.json diff --git a/research/report-changes.json b/research/report-changes.json index d371ac68f..4f2bad6a8 100644 --- a/research/report-changes.json +++ b/research/report-changes.json @@ -1,7 +1,17 @@ { "schema_version": 1, "history_scope": "本页从有完整审计依据的修订开始记录。更早的站点版本仍保留在 Artifact Hub,但没有逐项变更说明,故不补编历史。", - "browser_agent_reviews": [ + "browser_use_reviews": [ + { + "id": "review-2026-09-10-track-naming-and-release-gates", + "date": "2026-09-10", + "title": "轨道命名与发布门禁核验", + "status": "已处理", + "scope": "自主评测与固定轨迹评测的命名、优胜标记、英文界面属性、筛选状态和视觉发布门禁。", + "findings": "轨道名称按实现层命名,读者不易判断测量对象;并列最优和显示值相同的指标仍可能被标优;英文可访问属性与失效筛选链接缺少回归保护;SVG 和跨断点布局只受人工规范约束。", + "corrections": "统一命名为 Browser Use 与 Browser Trajectory Replay(轨迹重放),完成代码与命令迁移且不保留旧入口;优胜只允许显示值唯一领先;补充英文属性翻译、失效链接回退、SVG 拒绝和核心路由双语双主题宽窄屏渲染检查。", + "remaining": "视觉门禁验证结构性溢出、裁切和表头竖排,仍不能替代人工对信息层级与审美质量的判断。" + }, { "id": "review-2026-09-10-scalable-comparison-ui", "date": "2026-09-10", @@ -43,7 +53,35 @@ "remaining": "无阻塞项。" } ], - "browser_agent_releases": [ + "browser_use_releases": [ + { + "id": "2026-09-10-track-naming-and-release-hardening", + "date": "2026-09-10", + "title": "统一轨道名称并加固发布检查", + "summary": "自主轨道统一为 Browser Use,固定动作轨道统一为 Browser Trajectory Replay(轨迹重放);旧代码路径和命令一次性迁移。优胜标记、英文可访问文本、筛选恢复、SVG 资产和跨断点布局新增自动门禁。官方成绩、分母和冻结证据不变。", + "attempts_sha256": "517b613adcc40fc0ebe41af2ab2115d76f49faaba5378b426cc196b5b633e9c7", + "selection_sha256": "ba3c344df003dd4eba3a5d1ef97162ee98df2ed728f7c29c4188bc4247982d38", + "changes": [ + [ + "轨道命名", + "旧名称混合了实现层与评测对象,难以直接区分自主任务与固定动作轨迹。", + "自主评测统一为 Browser Use;固定动作轨迹评测统一为 Browser Trajectory Replay(轨迹重放);代码包、示例、测试、命令、配置和发布路径同步迁移,不保留兼容入口。", + "只改变命名和入口,不改变任务、运行或判分。" + ], + [ + "比较与本地化", + "并列最优或舍入后显示相同的数值可能仍被标为优胜;英文页面部分可访问属性和动态标题保留中文。", + "只有按页面展示精度唯一领先的值才标优;英文标题、title、aria-label 和 placeholder 纳入本地化及完整性检查;失效的浏览器筛选链接回退到有效默认值。", + "修正读者界面,不改变测量值和官方分数。" + ], + [ + "防复发门禁", + "命名、图形格式和跨宽度布局主要依赖人工记忆。", + "CI 增加旧命名扫描;发布检查拒绝 SVG 图;视觉检查渲染中英文核心路由的桌面与窄屏、明暗主题组合,阻止页面级溢出、核心卡片裁切和表头竖排。", + "新增 104 个渲染状态的结构性视觉门禁;不改变报告数据。" + ] + ] + }, { "id": "2026-09-10-comparison-and-review-presentation", "date": "2026-09-10", @@ -183,7 +221,7 @@ { "id": "2026-09-10-report-evidence-restructure", "date": "2026-09-10", - "title": "按证据链重构 Browser Agent 报告", + "title": "按证据链重构 Browser Use 报告", "summary": "报告新增任务族覆盖、95% Wilson 区间、30 题同题横向对照和全路由英文镜像;将原“独立复核”纠正为确定性证据完整性审计,并明确五个共同失败尚无本轮成功路径,不能直接归因 Agent 或浏览器。测量结果、分母和原始证据未改变。", "attempts_sha256": "517b613adcc40fc0ebe41af2ab2115d76f49faaba5378b426cc196b5b633e9c7", "selection_sha256": "ba3c344df003dd4eba3a5d1ef97162ee98df2ed728f7c29c4188bc4247982d38", @@ -230,8 +268,8 @@ ], [ "明确自主 Agent 轨道", - "仓库同时存在执行层 browser_kernel harness 和自主 browser_agent harness,若统称为“原 30 题”会误读运行方式。", - "本报告的 30 个 WebArena-Verified 任务明确由 CowAgent 自主观察、规划和操作;browser_kernel 作为另一条执行层微基准单独说明。", + "仓库同时存在执行层 trajectory_replay harness 和自主 browser_use harness,若统称为“原 30 题”会误读运行方式。", + "本报告的 30 个 WebArena-Verified 任务明确由 CowAgent 自主观察、规划和操作;trajectory_replay 作为另一条执行层微基准单独说明。", "本轮成绩只来自自主 Agent 轨道;两类 harness 不合并计分。" ], [ diff --git a/scripts/build_browser_agent_report.py b/scripts/build_browser_agent_report.py deleted file mode 100644 index 5ceadde0c..000000000 --- a/scripts/build_browser_agent_report.py +++ /dev/null @@ -1,6 +0,0 @@ -"""Build the autonomous browser-agent report.""" - -from harness.browser_agent.report import main - -if __name__ == "__main__": - main() diff --git a/scripts/build_browser_use_report.py b/scripts/build_browser_use_report.py new file mode 100644 index 000000000..24f8da1c0 --- /dev/null +++ b/scripts/build_browser_use_report.py @@ -0,0 +1,6 @@ +"""Build the autonomous browser-use report.""" + +from harness.browser_use.report import main + +if __name__ == "__main__": + main() diff --git a/scripts/check_browser_agent_report.py b/scripts/check_browser_use_report.py similarity index 87% rename from scripts/check_browser_agent_report.py rename to scripts/check_browser_use_report.py index 14d4a8963..0b8382274 100644 --- a/scripts/check_browser_agent_report.py +++ b/scripts/check_browser_use_report.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Validate the autonomous browser-agent report and its measured evidence.""" +"""Validate the autonomous browser-use report and its measured evidence.""" from __future__ import annotations @@ -9,8 +9,8 @@ import re from pathlib import Path -from harness.browser_agent.report import _logical_results -from harness.browser_agent.selection import load_pilot +from harness.browser_use.report import _logical_results +from harness.browser_use.selection import load_pilot def _sha256(path: Path) -> str: @@ -24,6 +24,11 @@ def check( *, require_complete: bool, ) -> None: + profile_path = Path("evaluation/browser-use/current-report.json") + profile = json.loads(profile_path.read_text(encoding="utf-8")) + engines = tuple(str(engine) for engine in profile.get("engines") or []) + if not engines or len(engines) != len(set(engines)): + raise ValueError("current Browser Use profile must register unique browser engines") page_names = ( "index.html", "guide/index.html", @@ -36,8 +41,7 @@ def check( "adoption/index.html", "reproduce/index.html", "accounts/index.html", - "engines/chromium.html", - "engines/moli.html", + *(f"engines/{engine}.html" for engine in engines), ) pages = tuple(report_dir / name for name in page_names) index = pages[0] @@ -120,15 +124,13 @@ def check( if line.strip() ] selected_ids = {task.task_id for task in selection.tasks} - expected_primary = { - (engine, task_id, 1) for engine in ("chromium", "moli") for task_id in selected_ids - } + expected_primary = {(engine, task_id, 1) for engine in engines for task_id in selected_ids} by_key: dict[tuple[str, int, int], dict] = {} for row in rows: key = (str(row["engine"]), int(row["task_id"]), int(row["attempt"])) if key in by_key: raise ValueError(f"duplicate measured attempt: {key}") - if key[0] not in {"chromium", "moli"} or key[1] not in selected_ids: + if key[0] not in set(engines) or key[1] not in selected_ids: raise ValueError(f"attempt outside the registered cohort: {key}") if key[2] not in {1, 2}: raise ValueError(f"more than one retry is not allowed: {key}") @@ -145,6 +147,17 @@ def check( } if any('' not in rendered_pages[page] for page in english_pages): raise ValueError("every Chinese report route must have an English mirror") + cjk = re.compile(r"[\u3400-\u9fff]") + localized_attribute = re.compile( + r'(?:aria-label|placeholder|title)="([^"]*)"', flags=re.I + ) + for page in english_pages: + text = rendered_pages[page] + title = re.search(r"(.*?)", text, flags=re.I | re.S) + if title and cjk.search(title.group(1)): + raise ValueError(f"English report title is not localized: {page}") + if any(cjk.search(value) for value in localized_attribute.findall(text)): + raise ValueError(f"English report accessibility text is not localized: {page}") if task_pages and any( "Ground truth · 官方验收合同" not in rendered_pages[page] or "查看本题运行结果" not in rendered_pages[page] @@ -256,6 +269,19 @@ def check( for page, text in rendered_pages.items(): if any(term in text for term in forbidden_report_terms): raise ValueError(f"formal report contains forbidden notebook or execution-label text: {page}") + if re.search( + r"]+(?:src|data)=[\"'][^\"']*\.svg(?:[?#][^\"']*)?[\"']", + text, + flags=re.I, + ): + raise ValueError( + f"Browser Use reports must use raster or Mermaid diagrams, not SVG: {page}" + ) + svg_assets = [ + path for path in report_dir.rglob("*") if path.is_file() and path.suffix.lower() == ".svg" + ] + if svg_assets: + raise ValueError(f"Browser Use report contains SVG assets: {svg_assets[0]}") style = (report_dir / "assets/style.css").read_text(encoding="utf-8") for required_style in ( "#task-table{min-width:1500px}", @@ -276,9 +302,6 @@ def check( raise ValueError("complete evidence requires manifest.json and run-config.json") manifest = json.loads(manifest_path.read_text(encoding="utf-8")) run_config = json.loads(run_config_path.read_text(encoding="utf-8")) - profile = json.loads( - Path("evaluation/browser-agent/current-report.json").read_text(encoding="utf-8") - ) official = (manifest.get("environment") or {}).get("official_evaluator") or {} expected_profile = { "selection_sha256": selection.sha256, @@ -289,8 +312,9 @@ def check( for key, expected in expected_profile.items(): if profile.get(key) != expected: raise ValueError(f"current report profile mismatch for {key}") - if profile.get("engines") != ["chromium", "moli"]: - raise ValueError("current report profile must bind the two browser engines") + browser_versions = set(((manifest.get("environment") or {}).get("browser_versions") or {})) + if set(engines) != browser_versions: + raise ValueError("current report engines differ from measured browser identities") manifest_score_authority = (manifest.get("protocol") or {}).get( "official_score_uses" ) or "final execution for each browser and task" @@ -385,7 +409,7 @@ def main() -> None: parser.add_argument( "--selection", type=Path, - default=Path("evaluation/browser-agent/webarena-verified-pilot.json"), + default=Path("evaluation/browser-use/webarena-verified-pilot.json"), ) parser.add_argument("--require-complete", action="store_true") args = parser.parse_args() @@ -395,7 +419,7 @@ def main() -> None: args.selection, require_complete=args.require_complete, ) - print("Browser-agent report integrity check passed") + print("Browser Use report integrity check passed") if __name__ == "__main__": diff --git a/scripts/check_browser_use_visual.py b/scripts/check_browser_use_visual.py new file mode 100644 index 000000000..131630f3a --- /dev/null +++ b/scripts/check_browser_use_visual.py @@ -0,0 +1,143 @@ +#!/usr/bin/env python3 +"""Render every Browser Use route class and reject structural layout failures.""" + +from __future__ import annotations + +import argparse +import json +import os +import shutil +from pathlib import Path + +from playwright.sync_api import sync_playwright + + +def _browser_executable() -> str: + configured = os.environ.get("BROWSER_USE_VISUAL_BROWSER") or os.environ.get( + "CHROMIUM_BINARY" + ) + candidates = ( + configured, + "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + shutil.which("google-chrome"), + shutil.which("chromium"), + shutil.which("chromium-browser"), + ) + for candidate in candidates: + if candidate and Path(candidate).is_file(): + return candidate + raise ValueError( + "visual QA requires Chrome/Chromium; set BROWSER_USE_VISUAL_BROWSER to its executable" + ) + + +def _route_classes(report: Path) -> list[Path]: + fixed = [ + "index.html", + "datasets/index.html", + "methodology/index.html", + "analysis/index.html", + "review/index.html", + "guide/index.html", + "changes/index.html", + "adoption/index.html", + "reproduce/index.html", + "accounts/index.html", + ] + dynamic = [] + for folder in ("datasets", "results", "engines"): + pages = sorted((report / folder).glob("*.html")) + if pages: + dynamic.append(pages[0].relative_to(report).as_posix()) + chinese = [report / name for name in (*fixed, *dynamic)] + routes = chinese + [report / "en" / page.relative_to(report) for page in chinese] + missing = [path for path in routes if not path.is_file()] + if missing: + raise ValueError(f"visual QA route is missing: {missing[0]}") + return routes + + +def check(report: Path, receipt: Path) -> None: + states = [] + failures = [] + executable = _browser_executable() + with sync_playwright() as playwright: + browser = playwright.chromium.launch(executable_path=executable, headless=True) + try: + for route in _route_classes(report): + for width, height in ((1440, 1000), (390, 844)): + for theme in ("light", "dark"): + page = browser.new_page(viewport={"width": width, "height": height}) + page.goto(route.resolve().as_uri(), wait_until="load") + page.evaluate( + "value => { document.documentElement.dataset.theme = value; }", theme + ) + result = page.evaluate( + """() => { +const root=document.documentElement; +const overflow=root.scrollWidth-root.clientWidth; +const clipped=[]; +for(const element of document.querySelectorAll('.filters input,.filters select,.attempt-fact,.evidence-card,.review-round,.action-card')){ + if(!element.getClientRects().length)continue; + const box=element.getBoundingClientRect(); + if(box.left < -2 || box.right > innerWidth + 2)clipped.push(element.className || element.tagName); +} +const vertical=[]; +for(const button of document.querySelectorAll('th .sort-button')){ + const box=button.getBoundingClientRect(),style=getComputedStyle(button),line=parseFloat(style.lineHeight)||16; + if(button.textContent.trim().length>2 && box.height>line*2.6 && box.width<52)vertical.push(button.textContent.trim()); +} +return {overflow,clipped:[...new Set(clipped)],vertical}; +} +""" + ) + state = { + "route": route.relative_to(report).as_posix(), + "width": width, + "height": height, + "theme": theme, + **result, + } + states.append(state) + if result["overflow"] > 2 or result["clipped"] or result["vertical"]: + failures.append(state) + page.close() + finally: + browser.close() + receipt.parent.mkdir(parents=True, exist_ok=True) + receipt.write_text( + json.dumps( + { + "schema": "lex_browser_use_visual_qa/1", + "browser_executable": executable, + "route_classes": len(_route_classes(report)), + "states_checked": len(states), + "failures": failures, + }, + ensure_ascii=False, + indent=2, + ), + encoding="utf-8", + ) + if failures: + first = failures[0] + raise ValueError( + f"visual QA failed at {first['route']} {first['width']}px {first['theme']}: {first}" + ) + print(f"Browser Use visual QA passed: {len(states)} rendered states") + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("report", type=Path) + parser.add_argument( + "--receipt", + type=Path, + default=Path("publish/browser-use-visual-qa.json"), + ) + args = parser.parse_args() + check(args.report, args.receipt) + + +if __name__ == "__main__": + main() diff --git a/scripts/check_track_naming.py b/scripts/check_track_naming.py new file mode 100644 index 000000000..73f11578d --- /dev/null +++ b/scripts/check_track_naming.py @@ -0,0 +1,53 @@ +#!/usr/bin/env python3 +"""Reject retired track names from maintained repository files.""" + +from __future__ import annotations + +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] +EXCLUDED_ROOTS = { + ".git", + "node_modules", + "publish", + "reports", + "evaluation/dataset.json", + "evaluation/sources", +} +EXCLUDED_PARTS = {".pytest_cache", ".ruff_cache", ".venv", "__pycache__"} +TEXT_SUFFIXES = {"", ".css", ".html", ".js", ".json", ".md", ".py", ".sh", ".toml", ".txt", ".yaml", ".yml"} +FORBIDDEN = ( + "browser_" + "agent", + "browser-" + "agent", + "Browser " + "Agent", + "browser_" + "kernel", + "browser-" + "kernel", + "Browser " + "Kernel", + "Browser-" + "agent", + "Browser-" + "kernel", +) + + +def main() -> None: + findings: list[str] = [] + for path in ROOT.rglob("*"): + if not path.is_file() or path.suffix.lower() not in TEXT_SUFFIXES: + continue + relative = path.relative_to(ROOT) + name = relative.as_posix() + if EXCLUDED_PARTS.intersection(relative.parts): + continue + if any(name == root or name.startswith(root + "/") for root in EXCLUDED_ROOTS): + continue + text = path.read_text(encoding="utf-8", errors="ignore") + for term in FORBIDDEN: + if term in text: + findings.append(f"{name}: retired track name {term!r}") + if findings: + raise SystemExit("\n".join(findings)) + print("Track naming check passed") + + +if __name__ == "__main__": + main() diff --git a/scripts/package_browser_agent_artifact_hub.py b/scripts/package_browser_agent_artifact_hub.py deleted file mode 100644 index b27c5d30a..000000000 --- a/scripts/package_browser_agent_artifact_hub.py +++ /dev/null @@ -1,38 +0,0 @@ -#!/usr/bin/env python3 -"""Package the complete browser-agent report for Artifact Hub.""" - -from __future__ import annotations - -import argparse -import zipfile -from pathlib import Path - -from check_browser_agent_report import check - - -def main() -> None: - parser = argparse.ArgumentParser() - parser.add_argument("--report", type=Path, default=Path("reports/browser-agent")) - parser.add_argument("--evidence", required=True, type=Path) - parser.add_argument( - "--output", type=Path, default=Path("publish/browser-agent-sites.zip") - ) - args = parser.parse_args() - check( - args.report, - args.evidence, - Path("evaluation/browser-agent/webarena-verified-pilot.json"), - require_complete=True, - ) - args.output.parent.mkdir(parents=True, exist_ok=True) - with zipfile.ZipFile(args.output, "w", zipfile.ZIP_DEFLATED) as archive: - for path in sorted(args.report.rglob("*")): - if path.is_file(): - if path.is_symlink(): - raise ValueError(f"publication cannot contain symlinks: {path}") - archive.write(path, path.relative_to(args.report)) - print(args.output) - - -if __name__ == "__main__": - main() diff --git a/scripts/package_browser_use_artifact_hub.py b/scripts/package_browser_use_artifact_hub.py new file mode 100644 index 000000000..6ad976a6e --- /dev/null +++ b/scripts/package_browser_use_artifact_hub.py @@ -0,0 +1,79 @@ +#!/usr/bin/env python3 +"""Package the complete browser-use report for Artifact Hub.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import zipfile +from pathlib import Path + +from check_browser_use_report import check + +ATTESTATION = Path("publish/browser-use-sites.attestation.json") +PROFILE = Path("evaluation/browser-use/current-report.json") + + +def sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def tree_sha256(root: Path) -> str: + digest = hashlib.sha256() + for path in sorted(item for item in root.rglob("*") if item.is_file()): + relative = path.relative_to(root).as_posix().encode() + digest.update(len(relative).to_bytes(8, "big")) + digest.update(relative) + digest.update(bytes.fromhex(sha256(path))) + return digest.hexdigest() + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--report", type=Path, default=Path("reports/browser-use")) + parser.add_argument("--evidence", required=True, type=Path) + parser.add_argument( + "--output", type=Path, default=Path("publish/browser-use-sites.zip") + ) + args = parser.parse_args() + check( + args.report, + args.evidence, + Path("evaluation/browser-use/webarena-verified-pilot.json"), + require_complete=True, + ) + args.output.parent.mkdir(parents=True, exist_ok=True) + with zipfile.ZipFile(args.output, "w", zipfile.ZIP_DEFLATED) as archive: + for path in sorted(args.report.rglob("*")): + if path.is_file(): + if path.is_symlink(): + raise ValueError(f"publication cannot contain symlinks: {path}") + archive.write(path, path.relative_to(args.report)) + evidence_files = { + name: sha256(args.evidence / name) + for name in ("manifest.json", "run-config.json", "attempts.jsonl", "result-review.json") + } + attestation = { + "schema": "lex_browser_use_publication_attestation/1", + "archive": str(args.output), + "archive_sha256": sha256(args.output), + "report": str(args.report), + "report_tree_sha256": tree_sha256(args.report), + "profile_sha256": sha256(PROFILE), + "evidence": str(args.evidence), + "evidence_files": evidence_files, + } + ATTESTATION.write_text( + json.dumps(attestation, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + print(args.output) + + +if __name__ == "__main__": + main() diff --git a/scripts/publish_browser_agent_artifact_hub.py b/scripts/publish_browser_use_artifact_hub.py similarity index 66% rename from scripts/publish_browser_agent_artifact_hub.py rename to scripts/publish_browser_use_artifact_hub.py index eb1673dd8..288b36841 100644 --- a/scripts/publish_browser_agent_artifact_hub.py +++ b/scripts/publish_browser_use_artifact_hub.py @@ -1,9 +1,10 @@ #!/usr/bin/env python3 -"""Create or atomically update the browser-agent report on Artifact Hub.""" +"""Create or atomically update the browser-use report on Artifact Hub.""" from __future__ import annotations import argparse +import hashlib import json import os import urllib.parse @@ -13,9 +14,54 @@ from harness.web_fetch.artifact_auth import load_token -ARCHIVE = Path("publish/browser-agent-sites.zip") -RECEIPT = Path("publish/browser-agent-artifact-hub-receipt.json") -PROFILE = Path("evaluation/browser-agent/current-report.json") +ARCHIVE = Path("publish/browser-use-sites.zip") +ATTESTATION = Path("publish/browser-use-sites.attestation.json") +RECEIPT = Path("publish/browser-use-artifact-hub-receipt.json") +PROFILE = Path("evaluation/browser-use/current-report.json") + + +def sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def tree_sha256(root: Path) -> str: + digest = hashlib.sha256() + for path in sorted(item for item in root.rglob("*") if item.is_file()): + relative = path.relative_to(root).as_posix().encode() + digest.update(len(relative).to_bytes(8, "big")) + digest.update(relative) + digest.update(bytes.fromhex(sha256(path))) + return digest.hexdigest() + + +def validate_package() -> None: + if not ARCHIVE.is_file() or not ATTESTATION.is_file(): + raise ValueError("missing validated Browser Use package or attestation") + attestation = json.loads(ATTESTATION.read_text(encoding="utf-8")) + report = Path(str(attestation.get("report") or "")) + evidence = Path(str(attestation.get("evidence") or "")) + expected_evidence = attestation.get("evidence_files") or {} + checks = { + "archive": sha256(ARCHIVE) == attestation.get("archive_sha256"), + "report": report.is_dir() + and tree_sha256(report) == attestation.get("report_tree_sha256"), + "profile": sha256(PROFILE) == attestation.get("profile_sha256"), + "evidence": evidence.is_dir() + and all( + (evidence / name).is_file() and sha256(evidence / name) == digest + for name, digest in expected_evidence.items() + ), + } + failed = [name for name, valid in checks.items() if not valid] + if failed: + raise ValueError( + "publication package is stale; rebuild before publishing: " + + ", ".join(failed) + ) def publication_target() -> tuple[str, str, str]: @@ -25,7 +71,7 @@ def publication_target() -> tuple[str, str, str]: slug = str(publication.get("site_slug") or "") version = str(publication.get("expected_version") or "") if not base.startswith("https://") or not slug or not version.startswith("ver_"): - raise ValueError("current Browser Agent report has no valid publication target") + raise ValueError("current Browser Use report has no valid publication target") return base, slug, version @@ -39,7 +85,7 @@ def request( base, _, _ = publication_target() headers = { "Authorization": "Bearer " + load_token(), - "User-Agent": "Browser-Agent-Report-Publisher/1.0", + "User-Agent": "Browser-Use-Report-Publisher/1.0", } if content_type: headers["Content-Type"] = content_type @@ -51,7 +97,7 @@ def request( def multipart_archive() -> tuple[bytes, str]: if not ARCHIVE.is_file(): raise ValueError(f"missing validated package: {ARCHIVE}") - boundary = "----browseragent" + uuid.uuid4().hex + boundary = "----browseruse" + uuid.uuid4().hex parts = [] for key, value in ( ("mode", "zip"), @@ -66,7 +112,7 @@ def multipart_archive() -> tuple[bytes, str]: parts.append( ( f'--{boundary}\r\nContent-Disposition: form-data; name="file"; ' - 'filename="browser-agent.zip"\r\nContent-Type: application/zip\r\n\r\n' + 'filename="browser-use.zip"\r\nContent-Type: application/zip\r\n\r\n' ).encode() + ARCHIVE.read_bytes() + b"\r\n" @@ -83,6 +129,7 @@ def save_receipt(result: dict) -> None: def main(*, create: bool = False) -> None: + validate_package() payload, content_type = multipart_archive() base, expected_slug, expected_version = publication_target() if create: @@ -93,12 +140,12 @@ def main(*, create: bool = False) -> None: save_receipt(result) elif not RECEIPT.exists(): raise ValueError( - "bound Browser Agent site receipt is missing; refusing to create a new site" + "bound Browser Use site receipt is missing; refusing to create a new site" ) else: result = json.loads(RECEIPT.read_text(encoding="utf-8")) if result.get("slug") != expected_slug: - raise ValueError("Artifact Hub receipt differs from the bound Browser Agent site") + raise ValueError("Artifact Hub receipt differs from the bound Browser Use site") status, raw = request(f"/api/sites/{result['slug']}") current = json.loads(raw) version_id = (current.get("site") or {}).get("currentVersionId") @@ -131,14 +178,14 @@ def main(*, create: bool = False) -> None: result["share_url"] = json.loads(raw)["url"] save_receipt(result) public = urllib.request.Request( - result["share_url"], headers={"User-Agent": "Browser-Agent-Report-Publisher/1.0"} + result["share_url"], headers={"User-Agent": "Browser-Use-Report-Publisher/1.0"} ) with urllib.request.urlopen(public, timeout=60) as response: if response.status != 200: raise RuntimeError(f"public report returned {response.status}") preview = urllib.request.Request( f"{base}/api/preview/{result['slug']}", - headers={"User-Agent": "Browser-Agent-Report-Publisher/1.0"}, + headers={"User-Agent": "Browser-Use-Report-Publisher/1.0"}, ) with urllib.request.urlopen(preview, timeout=60) as response: body = response.read().decode("utf-8", errors="replace") diff --git a/scripts/review_browser_agent_results.py b/scripts/review_browser_use_results.py similarity index 74% rename from scripts/review_browser_agent_results.py rename to scripts/review_browser_use_results.py index dc30e0c05..a6d41c639 100644 --- a/scripts/review_browser_agent_results.py +++ b/scripts/review_browser_use_results.py @@ -6,8 +6,8 @@ import argparse from pathlib import Path -from harness.browser_agent.review import write_review -from harness.browser_agent.selection import load_pilot +from harness.browser_use.review import write_review +from harness.browser_use.selection import load_pilot def main() -> None: @@ -16,7 +16,7 @@ def main() -> None: parser.add_argument( "--selection", type=Path, - default=Path("evaluation/browser-agent/webarena-verified-pilot.json"), + default=Path("evaluation/browser-use/webarena-verified-pilot.json"), ) args = parser.parse_args() selection = load_pilot(args.selection) diff --git a/tests/browser_agent/test_har.py b/tests/browser_use/test_har.py similarity index 98% rename from tests/browser_agent/test_har.py rename to tests/browser_use/test_har.py index 7b8168c61..20e095440 100644 --- a/tests/browser_agent/test_har.py +++ b/tests/browser_use/test_har.py @@ -1,7 +1,7 @@ import base64 import json -from harness.browser_agent.har import CdpHarRecorder, _har_request, _har_response +from harness.browser_use.har import CdpHarRecorder, _har_request, _har_response def test_har_preserves_multipart_post_data_and_json_response() -> None: diff --git a/tests/browser_agent/test_model.py b/tests/browser_use/test_model.py similarity index 99% rename from tests/browser_agent/test_model.py rename to tests/browser_use/test_model.py index 2a95804f3..adc181e48 100644 --- a/tests/browser_agent/test_model.py +++ b/tests/browser_use/test_model.py @@ -1,6 +1,6 @@ import pytest -from harness.browser_agent.model import ( +from harness.browser_use.model import ( OpenAIResponsesModel, cow_messages_to_responses, cow_tools_to_responses, diff --git a/tests/browser_agent/test_publication.py b/tests/browser_use/test_publication.py similarity index 96% rename from tests/browser_agent/test_publication.py rename to tests/browser_use/test_publication.py index 2d2380d6b..d062faa88 100644 --- a/tests/browser_agent/test_publication.py +++ b/tests/browser_use/test_publication.py @@ -1,4 +1,4 @@ -from harness.browser_agent.publication import sanitize, trajectory +from harness.browser_use.publication import sanitize, trajectory def test_sanitize_redacts_credentials_origins_and_local_paths() -> None: diff --git a/tests/browser_agent/test_report.py b/tests/browser_use/test_report.py similarity index 89% rename from tests/browser_agent/test_report.py rename to tests/browser_use/test_report.py index 5d586924b..678d15231 100644 --- a/tests/browser_agent/test_report.py +++ b/tests/browser_use/test_report.py @@ -1,9 +1,11 @@ import json -from pathlib import Path +from pathlib import Path, PurePosixPath -import harness.browser_agent.report as report_module -from harness.browser_agent.report import ( +import harness.browser_use.report as report_module +from harness.browser_use.report_i18n import localize_html +from harness.browser_use.report import ( _action_duration_cards, + _best, _failure_diagnosis, _logical_results, _paired_summary, @@ -12,6 +14,32 @@ ) +def test_best_marks_only_one_visibly_distinct_winner() -> None: + assert _best({"a": 1, "b": 1, "c": 2}, higher=False) == set() + assert _best({"a": 1, "b": 2, "c": 3}, higher=False) == {"a"} + assert ( + _best( + {"a": 1000.0, "b": 1000.4}, + higher=False, + display=lambda value: round(value / 1000, 3), + ) + == set() + ) + + +def test_english_localization_includes_visible_attributes() -> None: + source = ( + '' + '' + ) + + localized = localize_html(source, PurePosixPath("analysis/index.html")) + + assert 'title="Better"' in localized + assert 'aria-label="Better"' in localized + assert 'placeholder="Task, intent, or acceptance contract"' in localized + + def test_paired_summary_uses_final_logical_results() -> None: rows = [ {"engine": "chromium", "task_id": 1, "attempt": 1, "score": 1}, @@ -163,7 +191,7 @@ def test_report_publishes_one_final_logical_result_and_aggregates_cost(tmp_path: report = build_report( evidence=evidence, output=output, - selection_path=Path("evaluation/browser-agent/webarena-verified-pilot.json"), + selection_path=Path("evaluation/browser-use/webarena-verified-pilot.json"), ) text = report.read_text(encoding="utf-8") assert "1/1" in text @@ -221,6 +249,7 @@ def test_report_publishes_one_final_logical_result_and_aggregates_cost(tmp_path: assert "诊断重试" not in "".join( path.read_text(encoding="utf-8") for path in output.rglob("*.html") ) + assert not list(output.rglob("*.svg")) def test_report_renders_measured_runtime_identities(tmp_path: Path) -> None: @@ -269,7 +298,7 @@ def test_report_renders_measured_runtime_identities(tmp_path: Path) -> None: report = build_report( evidence=evidence, output=output, - selection_path=Path("evaluation/browser-agent/webarena-verified-pilot.json"), + selection_path=Path("evaluation/browser-use/webarena-verified-pilot.json"), ) text = (output / "reproduce/index.html").read_text(encoding="utf-8") @@ -316,7 +345,7 @@ def test_report_renders_measured_runtime_identities(tmp_path: Path) -> None: report_data = json.loads((output / "report-data.json").read_text(encoding="utf-8")) assert "evidence" not in report_data evidence_index = json.loads((output / "data/evidence-index.json").read_text(encoding="utf-8")) - assert evidence_index["schema"] == "lex_browser_agent_public_evidence_index/2" + assert evidence_index["schema"] == "lex_browser_use_public_evidence_index/2" assert len(evidence_index["results"]) == 0 diff --git a/tests/browser_agent/test_report_ui.mjs b/tests/browser_use/test_report_ui.mjs similarity index 78% rename from tests/browser_agent/test_report_ui.mjs rename to tests/browser_use/test_report_ui.mjs index 825e3fed6..e6271248a 100644 --- a/tests/browser_agent/test_report_ui.mjs +++ b/tests/browser_use/test_report_ui.mjs @@ -3,11 +3,11 @@ import fs from 'node:fs'; import vm from 'node:vm'; const source = fs.readFileSync( - new URL('../../harness/browser_agent/report.py', import.meta.url), + new URL('../../harness/browser_use/report.py', import.meta.url), 'utf8', ); const match = source.match(/REPORT_JS = """([\s\S]*?)"""\nREPORT_STYLE/); -assert.ok(match, 'browser-agent report JavaScript must remain extractable'); +assert.ok(match, 'browser-use report JavaScript must remain extractable'); const controls = [ {dataset: {filter: 'result'}, value: '', addEventListener() {}}, @@ -20,9 +20,9 @@ const rows = [ ]; const count = {textContent: ''}; const browserControls = { - 'browser-a': {value: 'chromium', addEventListener() {}}, - 'browser-relation': {value: 'mixed', addEventListener() {}}, - 'browser-kind': {value: '', addEventListener() {}}, + 'browser-a': {value: 'chromium', options: [{value: 'chromium'}, {value: 'moli'}], addEventListener() {}}, + 'browser-relation': {value: 'mixed', options: [{value: 'mixed'}, {value: 'all_fail'}], addEventListener() {}}, + 'browser-kind': {value: '', options: [{value: ''}, {value: 'RETRIEVE'}], addEventListener() {}}, 'browser-compare-count': {textContent: ''}, }; const comparisonLabel = {classList: {toggle() {}}}; @@ -88,6 +88,12 @@ assert.deepEqual(comparisonRows.map(row => row.hidden), [false, true, true]); assert.equal(browserControls['browser-compare-count'].textContent, '找到 1 个任务'); assert.equal(compareChecks[0].disabled, true); +location.search = '?b_a=retired&b_relation=unknown&b_others=retired'; +vm.runInContext('restoreView()', context); +assert.equal(browserControls['browser-a'].value, 'chromium'); +assert.equal(browserControls['browser-relation'].value, 'mixed'); +assert.equal(compareChecks[1].checked, true, 'invalid saved browsers must fall back to a valid comparison'); + browserControls['browser-relation'].value = 'all_fail'; vm.runInContext('compareBrowsers()', context); assert.deepEqual(comparisonRows.map(row => row.hidden), [true, true, false]); @@ -101,11 +107,12 @@ assert.match(replaced, /f_result=pass/); assert.doesNotMatch(replaced, /q=/); scrolledTo = null; +location.search = '?f_result=fail&q=missing'; location.hash = '#same-task'; -storage.set('browser-agent-scroll:/analysis/?f_result=fail&q=missing', '999'); +storage.set('browser-use-scroll:/analysis/?f_result=fail&q=missing', '999'); vm.runInContext('restoreView()', context); assert.equal(scrolledTo, null, 'an explicit fragment must take precedence over saved scroll'); listeners.pagehide(); -assert.equal(storage.get('browser-agent-scroll:/analysis/?f_result=fail&q=missing'), '240'); -console.log('PASS: browser-agent filters restore from URL, update shareable state, and save scroll'); +assert.equal(storage.get('browser-use-scroll:/analysis/?f_result=fail&q=missing'), '240'); +console.log('PASS: browser-use filters restore from URL, update shareable state, and save scroll'); diff --git a/tests/browser_agent/test_review.py b/tests/browser_use/test_review.py similarity index 98% rename from tests/browser_agent/test_review.py rename to tests/browser_use/test_review.py index 0d2a4717a..0da1d3da7 100644 --- a/tests/browser_agent/test_review.py +++ b/tests/browser_use/test_review.py @@ -1,7 +1,7 @@ import json from pathlib import Path -from harness.browser_agent.review import review_results, write_review +from harness.browser_use.review import review_results, write_review def _write_fixture( diff --git a/tests/browser_agent/test_runner.py b/tests/browser_use/test_runner.py similarity index 91% rename from tests/browser_agent/test_runner.py rename to tests/browser_use/test_runner.py index ecdf7c2bb..11a1d81ef 100644 --- a/tests/browser_agent/test_runner.py +++ b/tests/browser_use/test_runner.py @@ -1,7 +1,7 @@ import pytest from webarena_verified.types.task import WebArenaSite -from harness.browser_agent.runner import ( +from harness.browser_use.runner import ( ENV_CTRL_READY_TIMEOUT_SECONDS, QEMU_X86_64_HOST_PATH, SITE_AUTH_HEADERS, @@ -17,7 +17,7 @@ _wait_application_ready, _wait_container_healthy, ) -from harness.browser_agent.selection import load_pilot +from harness.browser_use.selection import load_pilot def test_official_container_urls_and_header_auth() -> None: @@ -45,7 +45,7 @@ def test_user_prompt_exposes_schema_but_not_reference_answer() -> None: def test_max_tasks_limits_only_the_current_invocation() -> None: - tasks = load_pilot("evaluation/browser-agent/webarena-verified-pilot.json").tasks + tasks = load_pilot("evaluation/browser-use/webarena-verified-pilot.json").tasks assert _scheduled_tasks(tasks, None) == tasks assert _scheduled_tasks(tasks, 1) == tasks[:1] @@ -119,7 +119,7 @@ class Container: def reload(self): self.attrs = {"State": {"Health": {"Status": "healthy"}}} - monkeypatch.setattr("harness.browser_agent.runner.time.sleep", lambda _seconds: None) + monkeypatch.setattr("harness.browser_use.runner.time.sleep", lambda _seconds: None) _wait_container_healthy(Container(), timeout_seconds=1) @@ -133,7 +133,7 @@ def get(url, **kwargs): calls.append((url, kwargs)) return Response() - monkeypatch.setattr("harness.browser_agent.runner.requests.get", get) + monkeypatch.setattr("harness.browser_use.runner.requests.get", get) _wait_application_ready( WebArenaSite.SHOPPING_ADMIN, "/", @@ -159,7 +159,7 @@ class Client: def info(): return {"Architecture": "aarch64"} - monkeypatch.setattr("harness.browser_agent.runner.docker.from_env", lambda: Client()) + monkeypatch.setattr("harness.browser_use.runner.docker.from_env", lambda: Client()) manager = _site_container_manager(WebArenaSite.SHOPPING) assert manager.config.docker_img == "am1n3e/webarena-verified-shopping" @@ -175,7 +175,7 @@ class Client: def info(): return {"Architecture": "aarch64"} - monkeypatch.setattr("harness.browser_agent.runner.docker.from_env", lambda: Client()) + monkeypatch.setattr("harness.browser_use.runner.docker.from_env", lambda: Client()) manager = _site_container_manager(WebArenaSite.GITLAB) assert manager.config.docker_img == "am1n3e/webarena-verified-gitlab" @@ -188,7 +188,7 @@ class Client: def info(): return {"Architecture": "x86_64"} - monkeypatch.setattr("harness.browser_agent.runner.docker.from_env", lambda: Client()) + monkeypatch.setattr("harness.browser_use.runner.docker.from_env", lambda: Client()) manager = _site_container_manager(WebArenaSite.SHOPPING) assert QEMU_X86_64_HOST_PATH not in manager.config.volumes diff --git a/tests/browser_agent/test_selection.py b/tests/browser_use/test_selection.py similarity index 69% rename from tests/browser_agent/test_selection.py rename to tests/browser_use/test_selection.py index e6216d68e..3b0fe736a 100644 --- a/tests/browser_agent/test_selection.py +++ b/tests/browser_use/test_selection.py @@ -1,10 +1,10 @@ from pathlib import Path -from harness.browser_agent.selection import TIER_COUNTS, load_pilot +from harness.browser_use.selection import TIER_COUNTS, load_pilot def test_pilot_has_pre_registered_tier_counts() -> None: - selection = load_pilot(Path("evaluation/browser-agent/webarena-verified-pilot.json")) + selection = load_pilot(Path("evaluation/browser-use/webarena-verified-pilot.json")) assert len(selection.tasks) == 30 assert { tier: sum(task.tier == tier for task in selection.tasks) for tier in TIER_COUNTS diff --git a/tests/browser_kernel/test_diagnostics.py b/tests/trajectory_replay/test_diagnostics.py similarity index 96% rename from tests/browser_kernel/test_diagnostics.py rename to tests/trajectory_replay/test_diagnostics.py index d3139b7ae..db5b40460 100644 --- a/tests/browser_kernel/test_diagnostics.py +++ b/tests/trajectory_replay/test_diagnostics.py @@ -5,8 +5,8 @@ import pytest -from harness.browser_kernel import diagnostics -from harness.browser_kernel.diagnostics import build_failure_groups, classify_failure +from harness.trajectory_replay import diagnostics +from harness.trajectory_replay.diagnostics import build_failure_groups, classify_failure @pytest.mark.parametrize( diff --git a/tests/browser_kernel/test_executor.py b/tests/trajectory_replay/test_executor.py similarity index 98% rename from tests/browser_kernel/test_executor.py rename to tests/trajectory_replay/test_executor.py index def0f1c05..288a007a1 100644 --- a/tests/browser_kernel/test_executor.py +++ b/tests/trajectory_replay/test_executor.py @@ -4,7 +4,7 @@ import pytest -from harness.browser_kernel.executor import execute_steps, judge_outputs, validate_outputs +from harness.trajectory_replay.executor import execute_steps, judge_outputs, validate_outputs class FakePage: diff --git a/tests/browser_kernel/test_metrics.py b/tests/trajectory_replay/test_metrics.py similarity index 99% rename from tests/browser_kernel/test_metrics.py rename to tests/trajectory_replay/test_metrics.py index f5b36408b..e20d8d5e2 100644 --- a/tests/browser_kernel/test_metrics.py +++ b/tests/trajectory_replay/test_metrics.py @@ -1,6 +1,6 @@ from __future__ import annotations -from harness.browser_kernel.metrics import ( +from harness.trajectory_replay.metrics import ( CounterSnapshot, DockerStatsResources, LinuxContainerResources, diff --git a/tests/browser_kernel/test_real_smoke.py b/tests/trajectory_replay/test_real_smoke.py similarity index 97% rename from tests/browser_kernel/test_real_smoke.py rename to tests/trajectory_replay/test_real_smoke.py index 7879b1f1b..21e15e0a0 100644 --- a/tests/browser_kernel/test_real_smoke.py +++ b/tests/trajectory_replay/test_real_smoke.py @@ -9,7 +9,7 @@ import pytest from playwright.sync_api import sync_playwright -from harness.browser_kernel.runner import ( +from harness.trajectory_replay.runner import ( ENGINE_IMAGES, ENGINE_PORTS, _cleanup_runtime, diff --git a/tests/browser_kernel/test_report.py b/tests/trajectory_replay/test_report.py similarity index 92% rename from tests/browser_kernel/test_report.py rename to tests/trajectory_replay/test_report.py index 0a2b87c8a..bf40130cf 100644 --- a/tests/browser_kernel/test_report.py +++ b/tests/trajectory_replay/test_report.py @@ -4,7 +4,7 @@ import re from pathlib import Path -from harness.browser_kernel.report import ( +from harness.trajectory_replay.report import ( METRICS, _absolute_metric_cell, _annotate_source_marks, @@ -88,7 +88,7 @@ def test_resource_tables_separate_absolute_values_from_paired_ratios(): def test_report_uses_fixed_latest_filename_and_removes_legacy_versions(tmp_path): reports = tmp_path / "reports" reports.mkdir() - (reports / "browser-kernel-evaluation-20260101.md").write_text("old", encoding="utf-8") + (reports / "trajectory-replay-evaluation-20260101.md").write_text("old", encoding="utf-8") path = write_latest_report( reports, @@ -177,10 +177,10 @@ def test_report_uses_fixed_latest_filename_and_removes_legacy_versions(tmp_path) }, ) - assert path.name == "browser-kernel-evaluation-latest.md" + assert path.name == "trajectory-replay-evaluation-latest.md" assert path.exists() - assert not (reports / "browser-kernel-evaluation-20260101.md").exists() - rendered = (reports / "browser-kernel-evaluation-latest.html").read_text(encoding="utf-8") + assert not (reports / "trajectory-replay-evaluation-20260101.md").exists() + rendered = (reports / "trajectory-replay-evaluation-latest.html").read_text(encoding="utf-8") assert "结论与产品建议" in rendered assert 'lang="zh-CN"' in rendered assert "报告分页" in rendered @@ -196,15 +196,15 @@ def test_report_uses_fixed_latest_filename_and_removes_legacy_versions(tmp_path) assert "边际分位数相除" in rendered assert "结果重试恢复" not in rendered assert "首次结果正确" not in rendered - assert (reports / "browser-kernel-evaluation-latest-suite.html").exists() - assert (reports / "browser-kernel-evaluation-latest-method.html").exists() - assert not (reports / "browser-kernel-evaluation-latest-results.html").exists() - assert (reports / "browser-kernel-evaluation-latest-analysis.html").exists() - assert (reports / "browser-kernel-evaluation-latest-appendix.html").exists() - assert (reports / "browser-kernel-evaluation-latest-data.json").exists() + assert (reports / "trajectory-replay-evaluation-latest-suite.html").exists() + assert (reports / "trajectory-replay-evaluation-latest-method.html").exists() + assert not (reports / "trajectory-replay-evaluation-latest-results.html").exists() + assert (reports / "trajectory-replay-evaluation-latest-analysis.html").exists() + assert (reports / "trajectory-replay-evaluation-latest-appendix.html").exists() + assert (reports / "trajectory-replay-evaluation-latest-data.json").exists() assert "总体结果" not in rendered assert "第 1/5 页" in rendered - suite = (reports / "browser-kernel-evaluation-latest-suite.html").read_text(encoding="utf-8") + suite = (reports / "trajectory-replay-evaluation-latest-suite.html").read_text(encoding="utf-8") assert "1公网任务" in suite assert "公网测试页面" in suite assert 'href="https://example.com"' in suite @@ -220,14 +220,14 @@ def test_report_uses_fixed_latest_filename_and_removes_legacy_versions(tmp_path) assert 'data-search="任务01 任务 01 example' in suite for html_path in reports.glob("*.html"): assert "人工复核" not in html_path.read_text(encoding="utf-8") - audit = json.loads((reports / "browser-kernel-evaluation-latest-data.json").read_text()) + audit = json.loads((reports / "trajectory-replay-evaluation-latest-data.json").read_text()) assert "human_review" not in audit["ground_truth_validation"] - method = (reports / "browser-kernel-evaluation-latest-method.html").read_text(encoding="utf-8") + method = (reports / "trajectory-replay-evaluation-latest-method.html").read_text(encoding="utf-8") assert "RX = Receive" in method assert "P50 与中位数是同一个统计量" in method assert "Codex" in method assert "# Browser Kernel Microbenchmark" not in rendered + assert "
    # Browser Trajectory Replay" not in rendered
     
     
     def test_report_links_to_full_failures_and_tool_trace_on_detail_pages(tmp_path):
    @@ -319,7 +319,7 @@ def test_report_links_to_full_failures_and_tool_trace_on_detail_pages(tmp_path):
             },
         )
         analysis = (
    -        reports / "browser-kernel-evaluation-latest-analysis.html"
    +        reports / "trajectory-replay-evaluation-latest-analysis.html"
         ).read_text(encoding="utf-8")
         assert "逐任务分析与原始证据" in analysis
         assert "TRACE_ONLY_SEARCH_MARKER" in analysis
    @@ -362,7 +362,7 @@ def test_report_links_to_full_failures_and_tool_trace_on_detail_pages(tmp_path):
         assert "LLM 根因假设" not in analysis
         assert "未启用 LLM" not in analysis
         assert "时序机制研究" in analysis
    -    homepage = (reports / "browser-kernel-evaluation-latest.html").read_text(encoding="utf-8")
    +    homepage = (reports / "trajectory-replay-evaluation-latest.html").read_text(encoding="utf-8")
         assert 'class="failure-shortcut"' not in homepage
         assert f'href="{detail_files[0].name}"' not in homepage
         assert "#provider=moli&status=fail" in homepage
    @@ -471,7 +471,7 @@ def test_detail_preserves_success_and_divergent_actual_calls():
     
     
     def test_all_published_suite_tasks_link_to_public_web_targets(tmp_path):
    -    suite_file = Path(__file__).resolve().parents[2] / "examples/browser_kernel/tasks.v4.json"
    +    suite_file = Path(__file__).resolve().parents[2] / "examples/trajectory_replay/tasks.v4.json"
         raw_tasks = json.loads(suite_file.read_text(encoding="utf-8"))["tasks"]
         suite = [
             {
    @@ -487,7 +487,7 @@ def test_all_published_suite_tasks_link_to_public_web_targets(tmp_path):
             manifest={"run_id": "suite-links", "suite": suite},
             summary={"functional": {}, "resource_comparison": {"by_engine": {}}},
         )
    -    rendered = (reports / "browser-kernel-evaluation-latest-suite.html").read_text(encoding="utf-8")
    +    rendered = (reports / "trajectory-replay-evaluation-latest-suite.html").read_text(encoding="utf-8")
         links = re.findall(
             r'公网测试页面]+href="(https://[^\"]+)"',
             rendered,
    diff --git a/tests/browser_kernel/test_runner.py b/tests/trajectory_replay/test_runner.py
    similarity index 96%
    rename from tests/browser_kernel/test_runner.py
    rename to tests/trajectory_replay/test_runner.py
    index c2b9281b5..d882c8040 100644
    --- a/tests/browser_kernel/test_runner.py
    +++ b/tests/trajectory_replay/test_runner.py
    @@ -9,8 +9,8 @@
     
     import pytest
     
    -from harness.browser_kernel import diagnostics, runner
    -from harness.browser_kernel.runner import (
    +from harness.trajectory_replay import diagnostics, runner
    +from harness.trajectory_replay.runner import (
         ENGINE_PORTS,
         _cleanup_runtime,
         _external_websocket_url,
    @@ -18,7 +18,7 @@
         _prepare_page,
         _published_endpoint,
     )
    -from harness.browser_kernel.suite import BrowserTask, TaskSuite
    +from harness.trajectory_replay.suite import BrowserTask, TaskSuite
     
     
     def test_browser_engines_publish_their_external_cdp_ports():
    @@ -118,7 +118,7 @@ def diagnostic_failure(*a, **k):
     
     
     def test_moli_image_installs_its_verified_dynamic_font_dependency():
    -    dockerfile = Path("examples/browser_kernel/docker/moli.Dockerfile").read_text(encoding="utf-8")
    +    dockerfile = Path("examples/trajectory_replay/docker/moli.Dockerfile").read_text(encoding="utf-8")
     
         assert "fontconfig" in dockerfile
     
    diff --git a/tests/browser_kernel/test_suite.py b/tests/trajectory_replay/test_suite.py
    similarity index 91%
    rename from tests/browser_kernel/test_suite.py
    rename to tests/trajectory_replay/test_suite.py
    index 8197988ab..1c9311e0f 100644
    --- a/tests/browser_kernel/test_suite.py
    +++ b/tests/trajectory_replay/test_suite.py
    @@ -7,7 +7,7 @@
     
     import pytest
     
    -from harness.browser_kernel.suite import load_suite, validate_ground_truth_evidence
    +from harness.trajectory_replay.suite import load_suite, validate_ground_truth_evidence
     
     
     def _task(task_id, calls, expected="Example"):
    @@ -102,7 +102,7 @@ def test_load_suite_rejects_duplicate_task_ids(tmp_path):
     
     
     def test_suite_has_thirty_ground_truth_tasks_with_requested_difficulty_mix():
    -    suite = load_suite("examples/browser_kernel/tasks.v4.json")
    +    suite = load_suite("examples/trajectory_replay/tasks.v4.json")
     
         assert len(suite.tasks) == 30
         assert len({task.task_id for task in suite.tasks}) == 30
    @@ -130,7 +130,7 @@ def test_suite_has_thirty_ground_truth_tasks_with_requested_difficulty_mix():
     
     
     def test_version_language_path_targets_desktop_controls_and_waits_for_navigation():
    -    suite = load_suite("examples/browser_kernel/tasks.v4.json")
    +    suite = load_suite("examples/trajectory_replay/tasks.v4.json")
         task = next(t for t in suite.tasks if t.task_id == "python-version-language")
         selects = [call for call in task.tool_calls if call["action"] == "select"]
         assert len(selects) == 3
    @@ -140,7 +140,7 @@ def test_version_language_path_targets_desktop_controls_and_waits_for_navigation
     
     
     def test_suite_uses_public_targets_and_repository_derived_capability_coverage():
    -    suite = load_suite("examples/browser_kernel/tasks.v4.json")
    +    suite = load_suite("examples/trajectory_replay/tasks.v4.json")
         assert all(task.source.get("kind") == "public_web" for task in suite.tasks)
         assert all(str(task.source.get("url") or "").startswith("https://") for task in suite.tasks)
         basis_urls = [str(url) for task in suite.tasks for url in task.source.get("design_basis") or []]
    @@ -159,9 +159,9 @@ def test_suite_uses_public_targets_and_repository_derived_capability_coverage():
     
     
     def test_codex_browser_ground_truth_evidence_matches_dataset_exactly():
    -    dataset_path = Path("examples/browser_kernel/tasks.v4.json")
    +    dataset_path = Path("examples/trajectory_replay/tasks.v4.json")
         evidence = json.loads(
    -        Path("examples/browser_kernel/ground_truth_validation.json").read_text(encoding="utf-8")
    +        Path("examples/trajectory_replay/ground_truth_validation.json").read_text(encoding="utf-8")
         )
         assert evidence["dataset_sha256"] == hashlib.sha256(dataset_path.read_bytes()).hexdigest()
         assert evidence["validator"] == "Codex Desktop in-app Browser Tool"
    @@ -182,10 +182,10 @@ def test_codex_browser_ground_truth_evidence_matches_dataset_exactly():
     
     
     def test_ground_truth_validation_rejects_evidence_for_another_suite(tmp_path):
    -    dataset_path = Path("examples/browser_kernel/tasks.v4.json")
    +    dataset_path = Path("examples/trajectory_replay/tasks.v4.json")
         suite = load_suite(dataset_path)
         evidence = json.loads(
    -        Path("examples/browser_kernel/ground_truth_validation.json").read_text(encoding="utf-8")
    +        Path("examples/trajectory_replay/ground_truth_validation.json").read_text(encoding="utf-8")
         )
         evidence["dataset_sha256"] = "0" * 64
         evidence_path = tmp_path / "evidence.json"
    diff --git a/tests/browser_kernel/test_verify_metrics.py b/tests/trajectory_replay/test_verify_metrics.py
    similarity index 91%
    rename from tests/browser_kernel/test_verify_metrics.py
    rename to tests/trajectory_replay/test_verify_metrics.py
    index 378b9b969..e926e81c3 100644
    --- a/tests/browser_kernel/test_verify_metrics.py
    +++ b/tests/trajectory_replay/test_verify_metrics.py
    @@ -6,11 +6,11 @@
     
     import pytest
     
    -from harness.browser_kernel.metrics import summarize_attempts
    +from harness.trajectory_replay.metrics import summarize_attempts
     
     
     def test_independent_audit_detects_tampered_aggregate_and_paired_operands(tmp_path):
    -    script = Path(__file__).resolve().parents[2] / "examples/browser_kernel/verify_metrics.py"
    +    script = Path(__file__).resolve().parents[2] / "examples/trajectory_replay/verify_metrics.py"
         spec = importlib.util.spec_from_file_location("verify_metrics", script)
         module = importlib.util.module_from_spec(spec)
         spec.loader.exec_module(module)
    
    From 622450505ba2382ce5ea1d90de9e330f87aa1e02 Mon Sep 17 00:00:00 2001
    From: kevin 
    Date: Thu, 10 Sep 2026 20:36:12 +0800
    Subject: [PATCH 037/127] refine browser use diagnosis and result details
    
    ---
     harness/browser_use/report.py              | 166 +++++++++++++++------
     harness/browser_use/report_i18n.py         |  93 ++++++++++++
     research/browser-use-failure-analysis.json |  37 +++++
     research/report-changes.json               |  38 +++++
     research/webarena-mac-expansion.json       |  24 +++
     scripts/check_browser_use_report.py        |  12 +-
     tests/browser_use/test_report.py           |  40 ++++-
     7 files changed, 361 insertions(+), 49 deletions(-)
     create mode 100644 research/browser-use-failure-analysis.json
     create mode 100644 research/webarena-mac-expansion.json
    
    diff --git a/harness/browser_use/report.py b/harness/browser_use/report.py
    index b6125cf3b..b8854d584 100644
    --- a/harness/browser_use/report.py
    +++ b/harness/browser_use/report.py
    @@ -7,6 +7,7 @@
     import html
     import json
     import math
    +import re
     import shutil
     import statistics
     import tempfile
    @@ -40,13 +41,18 @@
     }
     MUTATE_IDS = {399, 404, 441, 506}
     NAVIGATE_IDS = {44, 45, 118}
    -FAILURE_OBSERVATIONS = {
    -    68: "官方期望包含多本书;四次执行的实际回答都只包含一本。",
    -    124: "实际回答中的价格范围与该题官方目标商品范围不一致。",
    -    142: "实际订单汇总与官方期望值不一致。",
    -    168: "实际回答为空列表,而官方期望为非空项目列表。",
    -    506: "官方网络副作用合同未通过;购物车最终状态未满足题目要求。",
    +RESEARCH_ROOT = Path(__file__).parents[2] / "research"
    +SHARED_FAILURE_DIAGNOSES = {
    +    int(task_id): diagnosis
    +    for task_id, diagnosis in json.loads(
    +        (RESEARCH_ROOT / "browser-use-failure-analysis.json").read_text(encoding="utf-8")
    +    )["tasks"].items()
     }
    +MAC_EXPANSION = json.loads(
    +    (RESEARCH_ROOT / "webarena-mac-expansion.json").read_text(encoding="utf-8")
    +)
    +MAC_ENVIRONMENT_CANDIDATES = MAC_EXPANSION["environments"]
    +MAC_COUNTS = MAC_EXPANSION["official_counts"]
     
     
     def _configure_engine_labels(profile: dict[str, Any]) -> None:
    @@ -782,6 +788,7 @@ def _summary_comparison_rows(summary: dict[str, dict[str, Any]]) -> list[list[An
     .browser-comparison{border:1px solid var(--line);border-radius:18px;background:var(--panel);padding:24px;box-shadow:0 12px 32px rgba(25,45,70,.06)}.comparison-heading{display:flex;align-items:start;justify-content:space-between;gap:24px;margin-bottom:22px}.comparison-heading h3{margin:0 0 6px;font-size:22px}.comparison-heading p{margin:0;color:var(--muted);line-height:1.65}.comparison-controls{display:grid;grid-template-columns:minmax(170px,1fr) minmax(240px,1.4fr) minmax(170px,1fr);gap:14px}.comparison-controls label{display:grid;gap:6px;font-weight:750}.comparison-controls select{width:100%;padding:11px 12px;border:1px solid var(--line);border-radius:10px;background:var(--bg);color:var(--ink);font:inherit}.comparison-browsers{border:0;padding:0;margin:20px 0 0}.comparison-browsers legend{font-weight:750;margin-bottom:10px}.browser-checks{display:flex;gap:8px;flex-wrap:wrap}.browser-checks label{display:flex;align-items:center;gap:7px;padding:8px 11px;border:1px solid var(--line);border-radius:999px;background:var(--bg);cursor:pointer}.browser-checks label.disabled{opacity:.45;cursor:not-allowed}.browser-checks input{accent-color:var(--accent)}.comparison-result{margin-top:20px}.table-wrap .comparison-table{min-width:720px;table-layout:fixed}.comparison-table th:first-child,.comparison-table td:first-child{width:48%}.comparison-table th:nth-child(2),.comparison-table td:nth-child(2),.comparison-table th:nth-child(3),.comparison-table td:nth-child(3){width:26%}.task-cell a{font-weight:750}.task-meta{display:flex;align-items:center;gap:7px;flex-wrap:wrap;margin-top:7px}.track-badge{display:inline-block;padding:3px 8px;border-radius:999px;background:var(--panel2);color:var(--muted);font-size:12px;white-space:nowrap}.outcome-list{display:flex;gap:6px;flex-wrap:wrap}.outcome-pill{display:inline-block;padding:5px 8px;border-radius:8px;font-size:13px;text-decoration:none}.outcome-pill.pass{background:var(--success-bg);color:var(--good)}.outcome-pill.fail{background:var(--failure-bg);color:var(--bad)}
     .action-card-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.action-card{border:1px solid var(--line);border-radius:16px;background:var(--panel);padding:18px;min-width:0}.action-card>header{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:14px}.action-card>header code{font-size:16px;font-weight:800}.action-card>header span{font-size:12px;color:var(--muted)}.action-browser-row{padding:12px 0;border-top:1px solid var(--line)}.action-browser-head{display:flex;align-items:center;justify-content:space-between;gap:12px}.action-bar{height:7px;border-radius:999px;background:var(--panel2);overflow:hidden;margin:8px 0}.action-bar i{display:block;height:100%;border-radius:inherit;background:var(--accent)}.action-browser-row.action-winner .action-bar i{background:var(--good)}.action-browser-meta{font-size:12px;color:var(--muted)}
     .attempt-summary{display:grid;grid-template-columns:minmax(270px,1.4fr) repeat(4,minmax(135px,.7fr));gap:12px;align-items:stretch}.attempt-verdict{padding:20px 22px;border:1px solid var(--line);border-left:5px solid var(--good);border-radius:12px;background:var(--success-bg)}.attempt-verdict.failure{border-left-color:var(--bad);background:var(--failure-bg)}.attempt-verdict .status-line{display:block;font-size:24px;font-weight:850;margin-bottom:6px}.attempt-verdict p{margin:0;line-height:1.65}.attempt-jumps{display:flex;gap:14px;flex-wrap:wrap;margin-top:14px;font-weight:750}.attempt-fact{padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--panel);overflow-wrap:anywhere}.attempt-fact b{display:block;color:var(--muted);font-size:12px;margin-bottom:5px}.attempt-secondary{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-top:12px}
    +.task-goal{padding:22px 24px;border-radius:16px;background:var(--wash);border:1px solid var(--line);margin-bottom:18px}.task-goal span,.rule-index{display:block;color:var(--accent);font-size:12px;font-weight:850;letter-spacing:.08em;text-transform:uppercase}.task-goal p{font-size:20px;font-weight:750;line-height:1.6;margin:7px 0}.task-goal small{color:var(--muted)}.verification-list{display:grid;gap:14px}.verification-rule{padding:20px 22px;border:1px solid var(--line);border-left:5px solid var(--good);border-radius:14px;background:var(--panel);min-width:0}.verification-rule.rule-fail{border-left-color:var(--bad)}.verification-rule>header{display:flex;align-items:start;justify-content:space-between;gap:18px}.verification-rule h3{margin:4px 0 0;font-size:19px}.rule-verdict{flex:0 0 auto;padding:5px 10px;border-radius:999px;font-size:12px;font-weight:850}.rule-verdict.pass{background:var(--success-bg);color:var(--good)}.rule-verdict.fail{background:var(--failure-bg);color:var(--bad)}.rule-purpose{color:var(--muted);margin:12px 0 6px}.rule-summary{font-size:17px;font-weight:750;margin:0 0 14px}.verification-rule details{border-top:1px solid var(--line);padding-top:12px;margin-top:10px}.verification-rule summary{cursor:pointer;font-weight:750}.compact-evidence{margin-top:12px}.compact-evidence .json-view{max-height:360px}
     .page-main{overflow-x:clip}
     .section[id]{scroll-margin-top:76px}
     @media(max-width:980px){.attempt-summary{grid-template-columns:repeat(2,minmax(0,1fr))}.attempt-verdict{grid-column:1/-1}.attempt-secondary{grid-template-columns:repeat(2,minmax(0,1fr))}}
    @@ -952,34 +959,89 @@ def _failure_brief(result: dict[str, Any] | None, row: dict[str, Any]) -> str:
         return "全部官方 evaluator 均通过。"
     
     
    -def _evaluator_result_html(result: dict[str, Any] | None) -> str:
    +def _rule_purpose(name: str) -> str:
    +    if name == "AgentResponseEvaluator":
    +        return "检查 Agent 最终回答的状态、字段和值是否符合官方答案合同。"
    +    if name == "NetworkEventEvaluator":
    +        return "检查浏览器网络记录中是否出现合同要求的页面副作用。"
    +    return "检查该题官方合同声明的结果条件。"
    +
    +
    +def _rule_outcome_summary(item: dict[str, Any]) -> str:
    +    if float(item.get("score") or 0) == 1:
    +        return (
    +            "已观察到满足合同的网络事件。"
    +            if item.get("name") == "NetworkEventEvaluator"
    +            else "最终回答符合该规则。"
    +        )
    +    messages = [
    +        str(message)
    +        for assertion in item.get("assertions") or []
    +        for message in (assertion.get("assertion_msgs") or [])[:1]
    +    ]
    +    message = messages[0] if messages else str(item.get("error") or "")
    +    status = re.fullmatch(r"Expected ([^,]+), got (.+)", message)
    +    if status:
    +        return f"最终状态不符:期望 {status.group(1)},实际为 {status.group(2)}。"
    +    missing = re.fullmatch(
    +        r"Array is missing (\d+) expected element\(s\)\. Matched \((\d+)/(\d+)\)",
    +        message,
    +    )
    +    if missing:
    +        return f"最终数据缺少 {missing.group(1)} 项;官方期望 {missing.group(3)} 项,匹配 {missing.group(2)} 项。"
    +    if "Array value mismatch" in message:
    +        return "最终数据与官方期望值不一致。"
    +    if "No matching network event" in message:
    +        return "浏览器网络记录中没有出现满足该规则的事件。"
    +    return message or "该规则未通过。"
    +
    +
    +def _verification_html(
    +    task: dict[str, Any], result: dict[str, Any] | None, response: Any
    +) -> str:
         items = evaluator_summary(result)
         if not items:
             return '

    没有 evaluator 明细。

    ' cards = [] for item in items: passed = float(item.get("score") or 0) == 1 - detail = { + observed = response if item.get("name") == "AgentResponseEvaluator" else item.get("actual") + technical = { key: item.get(key) for key in ("assertions", "error", "should_not_exist") if item.get(key) not in (None, [], {}) } cards.append( - f'

    规则 {item["index"]} · {_e(item["name"])}

    ' - f"

    {'通过' if passed else '失败'} · score={_e(item.get('score'))}

    " - '

    官方期望

    ' + f'
    ' + '
    ' + + _e(f"规则 {item['index']}") + + "

    " + + _e(item["name"]) + + f'

    ' + + ("通过" if passed else "失败") + + "
    " + + f'

    {_e(_rule_purpose(item["name"]))}

    ' + + f'

    {_e(_rule_outcome_summary(item))}

    ' + + '
    查看官方期望与本次观察

    官方期望

    ' + _json_pre(item.get("expected")) - + "

    实际归一化结果

    " - + _json_pre(item.get("actual")) - + "
    " + + "

    本次观察

    " + + _json_pre(observed) + + "
    " + ( - f"
    断言与错误{_json_pre(detail)}
    " - if detail + f'
    查看 evaluator 技术明细{_json_pre(technical)}
    ' + if technical else "" ) + "
    " ) - return "".join(cards) + return ( + '
    任务目标

    ' + + _e(task.get("intent") or "—") + + '

    Agent 可自主选择 UI 路径;计分只看以下最终回答与必要网络副作用。
    ' + + '
    ' + + "".join(cards) + + "
    " + ) def _trajectory_html(steps: list[dict[str, Any]]) -> str: @@ -1022,7 +1084,13 @@ def _article_body( summary = _primary_summary(attempts) chromium, moli = summary["chromium"], summary["moli"] failures = [ - [f"#{task_id}", FAILURE_OBSERVATIONS[task_id]] for task_id in sorted(FAILURE_OBSERVATIONS) + [ + f"#{task_id}", + diagnosis["mismatch"], + diagnosis["diagnosis"], + diagnosis["confidence"], + ] + for task_id, diagnosis in SHARED_FAILURE_DIAGNOSES.items() ] return ( '
    Browser Use 实测解读
    ' @@ -1042,14 +1110,14 @@ def _article_body( caption="表 1|30 个任务的逐题最终结果配对。", ) + f"

    严谨表述只能是“本轮没有观察到成功率差异”,不能写成“两种内核等价”。{paired['paired']} 题对小效应检出力有限;更关键的是,样本没有诱发内核分歧。

  • " - "

    二、共同失败是待核验线索,不是已证实根因

    " - f"

    {paired['both_fail']} 个共同失败任务均未满足官方验收合同。下表只陈述官方期望与实际结果的差异,不把差异直接归因给 Agent、浏览器或站点。

    " + "

    二、为什么五个失败任务会完全相同

    " + f"

    两个浏览器共享 CowAgent、模型、提示与工具合同、站点状态、30 步上限和 evaluator,只有浏览器实现不同。{paired['both_fail']} 个共同失败都落在共享层,因此结果高度相关并不反常;它更说明这批题没有触发足以改变最终成败的浏览器差异。

    " + _table( - ["任务", "公开诊断"], + ["任务", "实际差异", "证据支持的诊断", "置信度"], failures, - caption="表 2|任务详情公开官方合同、实际回答、逐规则判定与执行轨迹。", + caption="表 2|诊断来自最终回答、官方 evaluator 与执行轨迹的交叉核对;不修改官方分数。", ) - + "

    这五题在本轮没有任何成功轨迹,因此当前站点快照的可完成性也未被本报告独立证明。下一步应先做不参与计分的人工或 oracle 可完成性核验,再区分 Agent、工具抽象、步数上限、站点状态和浏览器因素。

    三、简单、中等、困难都覆盖了,但“难度”只是操作长度

    WebArena‑Verified 没有官方 easy/medium/hard 三档。本项目在运行前按最少可见 UI 操作数注册为 1–4、5–7、8+ 三档,配额 5/10/15。它控制操作链覆盖,不等同于推理难度。

    " + + "

    结论:现有证据不支持“网站本身报错”“官方 evaluator 实现错误”或“标准答案错误”。其中 #124 的站内搜索召回过宽会增加歧义,但返回结果仍需要按任务语义过滤。更准确的归因是:四题属于共享 Agent/模型的语义、聚合或输出协议问题,一题属于共享的长链完成与步数预算问题。这不是对模型通用能力的定论,只描述当前 CowAgent、模型与运行协议。

    三、简单、中等、困难都覆盖了,但“难度”只是操作长度

    WebArena‑Verified 没有官方 easy/medium/hard 三档。本项目在运行前按最少可见 UI 操作数注册为 1–4、5–7、8+ 三档,配额 5/10/15。它控制操作链覆盖,不等同于推理难度。

    " + _table( ["本项目分层", "Chromium", "Moli"], _group_rows(attempts, tasks, "tier"), @@ -1060,7 +1128,7 @@ def _article_body( _group_rows(attempts, tasks, "sites"), caption="表 4|四个可重置自托管站点。", ) - + '

    四、选择 WebArena‑Verified 是为了归因,不是因为它最流行

    Mind2Web 家族采用面更广,但原版主要评离线动作预测;Online‑Mind2Web 和 WebVoyager贴近实时网页,却引入网页漂移和模型裁判。WebArena‑Verified 可以自托管、逐题重置,并用结构化最终答案和必要网络副作用做程序化验收,更适合“只替换浏览器”的 A/B。

    代价也明确:部署重,环境兼容本身会成为工程变量;官方 evaluator 不衡量路径效率,也不能覆盖开放互联网泛化。

    五、下一轮应主动寻找内核敏感样本

    保留 CowAgent、模型、reasoning=low 和每题最多两次执行的固定协议,扩大稳定可验收的配对样本;新增长页面、上传下载、复杂编辑器、弹窗、多标签页等更可能诱发内核差异的任务。实时网站另开 Online‑Mind2Web / WebVoyager 泛化轨道,LLM judge 与人工抽检单列。

    决策边界

    本轮支持“流程可比、当前样本无方向性差异”;不支持“内核等价”“Moli 已全面替代 Chromium”或“端到端耗时就是纯内核速度”。Astra 获权后必须形成独立运行。

    ' + + f'

    四、选择 WebArena‑Verified 是为了归因,不是因为它最流行

    Mind2Web 家族采用面更广,但原版主要评离线动作预测;Online‑Mind2Web 和 WebVoyager贴近实时网页,却引入网页漂移和模型裁判。WebArena‑Verified 可以自托管、逐题重置,并用结构化最终答案和必要网络副作用做程序化验收,更适合“只替换浏览器”的 A/B。

    代价也明确:部署重,环境兼容本身会成为工程变量;官方 evaluator 不衡量路径效率,也不能覆盖开放互联网泛化。

    五、下一轮先扩展四个已验证站点

    官方包有 {MAC_COUNTS["full"]} 题和 {MAC_COUNTS["hard"]} 题 hard 子集,没有官方 179 题版本;179 只能是另行声明的自定义样本。当前 Mac 已实跑的四个站点可覆盖 {MAC_COUNTS["current_four_sites_single_site"]} 个单站任务;再计入只依赖这四站的跨站任务,共有 {MAC_COUNTS["current_four_sites_including_cross_site"]} 个候选,其中 hard 子集有 {MAC_COUNTS["current_four_sites_hard_including_cross_site"]} 个。下一轮应在这些候选中先做登录、重置、健康检查与 evaluator 预检,再按简单、中等、困难和内核敏感交互分层抽样。

    Wikipedia 需要约 100 GB 数据,Map 需要约 60 GB 数据且当前公开镜像为 amd64;二者可作为后续扩展,但没有理由成为本机下一批的前置阻塞。

    保留 CowAgent、模型、reasoning=low 和每题最多两次执行的固定协议,优先加入长页面、复杂编辑器、弹窗、多标签页、上传下载等更可能诱发浏览器差异的任务。实时网站另开 Online‑Mind2Web / WebVoyager 泛化轨道,LLM judge 与人工抽检单列。

    决策边界

    本轮支持“流程可比、当前样本无方向性差异”;不支持“内核等价”“Moli 已全面替代 Chromium”或“端到端耗时就是纯内核速度”。Astra 获权后必须形成独立运行。

    ' ) @@ -1394,7 +1462,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) home += _section( "先看结论", - f'
    Chromium 任务通过
    {c["passed"]}/{c["completed"]}

    {_pct(c["score"])}

    Moli 任务通过
    {m["passed"]}/{m["completed"]}

    {_pct(m["score"])}

    单边胜出
    {paired["chromium_only"] + paired["moli_only"]}

    {paired["paired"]} 个完整配对

    共同失败
    {paired["both_fail"]}

    需要可完成性核验

    当前结论:本轮没有观察到浏览器成功率差异;这证明对照流程可比,不证明两个内核等价。

    归因边界:{paired["both_fail"]} 个共同失败任务没有任何成功轨迹。报告只能确认结果未满足官方合同,尚不能证明任务在当前快照可完成,也不能归因给 Agent 或浏览器。

    这是固定选择的 30 题试验,不是从全部 WebArena‑Verified 任务随机抽样;比例只描述本样本,不外推总体能力。

    ', + f'
    Chromium 任务通过
    {c["passed"]}/{c["completed"]}

    {_pct(c["score"])}

    Moli 任务通过
    {m["passed"]}/{m["completed"]}

    {_pct(m["score"])}

    单边胜出
    {paired["chromium_only"] + paired["moli_only"]}

    {paired["paired"]} 个完整配对

    共同失败
    {paired["both_fail"]}

    共享链路诊断

    当前结论:本轮没有观察到浏览器成功率差异;这证明对照流程可比,不证明两个内核等价。

    失败归因:{paired["both_fail"]} 个共同失败中,四题指向共享 Agent/模型的语义、聚合或输出协议,一题指向共享的步数预算与长链完成;没有发现站点、官方 evaluator 或答案合同错误的证据。

    这是固定选择的 30 题试验,不是从全部 WebArena‑Verified 任务随机抽样;比例只描述本样本,不外推总体能力。

    ', ) home += _section( "任务族覆盖", @@ -1569,7 +1637,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: methodology += _section( "这 30 题如何部署并由 Agent 执行", '
    WebArena-Verified 本地部署、CowAgent 自主操作、双浏览器对照与程序化验收流程
    图 1 · 从官方站点镜像、逐题隔离与健康检查,到 CowAgent 观察—决策—操作闭环,再到 Chromium/Moli 对照和官方程序化验收。窄屏可横向滚动查看。
    ' - "

    站点确实部署在本机,不是公网代站。macOS 上的 Docker 由 Colima 虚拟机承载;Shopping、Shopping Admin、Reddit、GitLab 四个官方镜像把应用端口发布到 Colima/Docker 宿主。Chromium 与 Moli 各自在独立浏览器容器中运行,通过容器内解析到的宿主网关 192.168.5.2 访问这些端口。这个地址只是本轮私有网络中的宿主入口,不是评测网站或固定产品配置。

    " + "

    站点确实部署在本机,不是公网代站。macOS 上的 Docker 由 Colima 虚拟机承载;本轮 Colima VM 地址是 192.168.5.3,其默认路由指向 192.168.5.2。后者是 VM 视角下的 macOS 宿主网关,不是虚拟机地址,也不是评测网站。Shopping、Shopping Admin、Reddit、GitLab 四个官方站点镜像通过 Docker 端口发布到这个宿主入口,独立的 Chromium/Moli 浏览器容器据此访问本地站点。

    " "

    WebArena‑Verified 当前提供 Shopping、Shopping Admin、Reddit、GitLab、Wikipedia、Map 六类环境。本轮 30 题只覆盖前四类;Wikipedia 与 Map 需要额外大型数据卷,因此没有部署,也没有任务进入成绩。

    " "

    运行器代码在每个执行前调用 ContainerManager.start(reset=True),只保留该题所需站点;env-ctrl 报告 ready 后,再做连续应用健康检查、清浏览器状态、登录测试账号并打开起始页。当前冻结证据保存了代码提交、运行协议和镜像身份;事件流从环境准备完成后开始,因此逐次 reset、ready 与 health 结果未形成独立绑定的准备回执。新版运行协议已要求保存该回执。

    " "

    预注册文件只含任务 ID、层级和入选理由,不含点击序列。CowAgent 收到自然语言目标后,在最多 30 步内循环读取页面观察、请求模型决策并生成下一次 BrowserTool 调用。Chromium 与 Moli 分别连接独立 CDP 浏览器容器;每个逻辑结果保留最终 JSON、Agent 事件流、网络 HAR 和 Playwright trace。

    " @@ -1587,6 +1655,17 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: + '

    证据边界:事件流能证明 CowAgent 在准备完成后产生了 BrowserTool 调用序列;镜像 ID、digest、代码提交与网关记录限定了本地运行身份。当前证据没有独立的逐次环境准备 receipt,因此报告不把“每次重置均成功”当成已经独立核验的事实。

    ' '', ) + methodology += _section( + "Mac 上可以继续扩展哪些实例", + f'

    官方完整集有 {MAC_COUNTS["full"]} 题,hard 子集有 {MAC_COUNTS["hard"]} 题;官方没有 179 题版本。若运行 179 题,必须把它标注为本项目自定义样本并冻结任务清单。当前四个已实跑站点可直接进入下一轮预检:覆盖 {MAC_COUNTS["current_four_sites_single_site"]} 个单站任务,计入只依赖这四站的跨站任务后为 {MAC_COUNTS["current_four_sites_including_cross_site"]} 个候选;hard 子集中可覆盖 {MAC_COUNTS["current_four_sites_hard_including_cross_site"]} 个。

    ' + + _table( + ["环境", "Apple Silicon Mac 状态", "完整集单站题", "hard 单站题", "约束"], + MAC_ENVIRONMENT_CANDIDATES, + caption="Wikipedia 与 Map 的容量来自官方环境说明;状态中的“实跑”只指本项目已有执行证据。", + ) + + '

    最小扩展方案:先从四个已验证站点的 678 个候选中做可完成性预检和分层抽样;Wikipedia、Map 等大数据环境后置。这样扩容不会先引入新的部署变量。

    ' + '', + ) methodology += _section( "固定协议", _table( @@ -1702,15 +1781,14 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: detail = _hero( public["id"], f"{ENGINE_LABELS[public['engine']]} · Task #{public['task_id']}", - "从官方 ground truth、实际回答、规则级判定下钻到 CowAgent 的每一步 BrowserTool 调用。", + "先看任务目标与验收结论,再按需展开技术明细和 CowAgent 的每一步 BrowserTool 调用。", ) # Pages declare so every internal URL is rooted at # the report bundle, including same-page fragment links. attempt_page = f"results/{public['id']}.html" detail += ( '" @@ -1719,7 +1797,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: "结果与运行", f'
    ' + f'' f'
    累计自主操作{public["action_count"]} 次 BrowserTool 调用
    ' f'
    Agent 链路累计耗时{_seconds(public["duration_ms"])}
    ' f'
    BrowserTool 累计耗时{_seconds(public["browser_tool_duration_ms"])}
    ' @@ -1730,18 +1808,17 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: f'
    证据完整性审计{_e(item.get("outcome") or "未记录")}
    ' '

    Agent 链路耗时包含会话准备、登录、模型推理、调度、工具执行和收尾,不包含 Docker 站点重置;BrowserTool 耗时是全部 browser 工具调用的墙钟时间之和,不等同于纯内核时间。

    ', ) - detail += '
    ' + _section( - "Ground truth 与实际最终回答", - '

    官方任务合同

    ' - + _task_contract_html(task) - + '

    Agent 实际回答

    ' - + _json_pre(response_record) - + "
    ", - ) - detail += '
    ' + _section( - "官方 evaluator 逐规则判定", - "

    所有规则都必须得分 1.0;这里使用 evaluator 已归一化的实际值与期望值直接对照。

    " - + _evaluator_result_html(evaluation_record), + detail += '
    ' + _section( + "任务要求与验收结果", + _verification_html(task, evaluation_record, response_record) + + ( + '
    证据诊断:' + + _e(SHARED_FAILURE_DIAGNOSES[public["task_id"]]["diagnosis"]) + + " 本诊断不修改官方分数。
    " + if public["task_id"] in SHARED_FAILURE_DIAGNOSES + else "" + ) + + '

    所有规则均为官方程序化验收,必须全部得分 1.0。页面先给出可读结论;官方期望、实际观察和断言保留在折叠技术明细及下载文件中。

    ', ) detail += '
    ' + _section( "CowAgent 自主执行轨迹", @@ -1972,7 +2049,10 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: "schema": "lex_browser_use_interpretation/1", "claims": [ {"claim": "当前样本未观察到成功率差异", "basis": paired}, - {"claim": "失败任务完全重合", "basis": sorted(FAILURE_OBSERVATIONS)}, + { + "claim": "失败任务完全重合,且证据诊断均位于共享链路", + "basis": SHARED_FAILURE_DIAGNOSES, + }, ], "bound_attempts_sha256": _sha256(evidence / "attempts.jsonl"), "selection_sha256": selection.sha256, diff --git a/harness/browser_use/report_i18n.py b/harness/browser_use/report_i18n.py index 6c3c6f57c..00191b67f 100644 --- a/harness/browser_use/report_i18n.py +++ b/harness/browser_use/report_i18n.py @@ -398,6 +398,9 @@ "Chromium 任务通过": "Chromium tasks passed", "Moli 任务通过": "Moli tasks passed", "需要可完成性核验": "Solvability review required", + "共享链路诊断": "Shared-stack diagnosis", + "失败归因:": "Failure attribution:", + "5 个共同失败中,四题指向共享 Agent/模型的语义、聚合或输出协议,一题指向共享的步数预算与长链完成;没有发现站点、官方 evaluator 或答案合同错误的证据。": "Among the five shared failures, four point to shared agent/model semantics, aggregation, or output protocol, and one points to the shared step budget and long-horizon completion. No evidence of site, official evaluator, or answer-contract errors was found.", "这是固定选择的 30 题试验,不是从全部 WebArena‑Verified 任务随机抽样;比例只描述本样本,不外推总体能力。": "This is a fixed 30-task experiment, not a random sample of all WebArena-Verified tasks. Percentages describe this sample only and are not population estimates.", "逻辑结果:": "Logical results:", "筛选并下钻两种浏览器结果。": "Filter tasks and inspect results from both browsers.", @@ -427,6 +430,85 @@ "是": "Yes", "否": "No", + # Result-detail information architecture. + "先看任务目标与验收结论,再按需展开技术明细和 CowAgent 的每一步 BrowserTool 调用。": "Start with the task objective and acceptance outcome, then expand technical evidence and each CowAgent BrowserTool call as needed.", + "任务与验收": "Task and acceptance", + "查看任务与验收": "View task and acceptance", + "任务要求与验收结果": "Task requirements and acceptance result", + "任务目标": "Task objective", + "Agent 可自主选择 UI 路径;计分只看以下最终回答与必要网络副作用。": "The agent may choose any UI path. Scoring uses only the final answer and required network side effects below.", + "检查 Agent 最终回答的状态、字段和值是否符合官方答案合同。": "Checks whether the status, fields, and values in the agent's final answer satisfy the official answer contract.", + "检查浏览器网络记录中是否出现合同要求的页面副作用。": "Checks whether the browser network record contains the side effect required by the contract.", + "检查该题官方合同声明的结果条件。": "Checks an outcome condition declared by the task's official contract.", + "最终回答符合该规则。": "The final answer satisfies this rule.", + "已观察到满足合同的网络事件。": "A network event satisfying the contract was observed.", + "最终数据与官方期望值不一致。": "The final data differs from the official expectation.", + "浏览器网络记录中没有出现满足该规则的事件。": "No browser network event satisfying this rule was observed.", + "该规则未通过。": "This rule failed.", + "查看官方期望与本次观察": "View the official expectation and this run's observation", + "本次观察": "Observed in this run", + "查看 evaluator 技术明细": "View evaluator technical details", + "所有规则均为官方程序化验收,必须全部得分 1.0。页面先给出可读结论;官方期望、实际观察和断言保留在折叠技术明细及下载文件中。": "All rules are official programmatic checks and every rule must score 1.0. The page leads with a readable conclusion; expectations, observations, and assertions remain in collapsed technical details and downloadable files.", + "证据诊断:": "Evidence-based diagnosis:", + "本诊断不修改官方分数。": "This diagnosis does not change the official score.", + "共享的跨帖聚合与空值处理错误。轨迹已看到两篇符合条件的帖子,说明站点内容存在;失败发生在最终答案整理。": "Shared cross-post aggregation and null-handling error. The trajectory observed both qualifying posts, so the site content was present; the failure occurred while assembling the final answer.", + "共享的跨帖聚合与空值处理错误。轨迹已看到两篇符合条件的帖子,说明站点内容存在;失败发生在最终答案整理。 本诊断不修改官方分数。": "Shared cross-post aggregation and null-handling error. The trajectory observed both qualifying posts, so the site content was present; the failure occurred while assembling the final answer. This diagnosis does not change the official score.", + "共享的搜索结果相关性过滤错误;站内搜索召回过宽是诱因,但 Agent 仍应按任务语义筛选商品。": "Shared search-result relevance filtering error. Broad site-search recall contributed, but the agent still needed to filter products by the task semantics.", + "共享的搜索结果相关性过滤错误;站内搜索召回过宽是诱因,但 Agent 仍应按任务语义筛选商品。 本诊断不修改官方分数。": "Shared search-result relevance filtering error. Broad site-search recall contributed, but the agent still needed to filter products by the task semantics. This diagnosis does not change the official score.", + "共享的商品类别判定错误。订单数据和 evaluator 数值一致,未见站点或计分器异常。": "Shared product-category classification error. Order data and evaluator values agree; no site or scorer anomaly was found.", + "共享的商品类别判定错误。订单数据和 evaluator 数值一致,未见站点或计分器异常。 本诊断不修改官方分数。": "Shared product-category classification error. Order data and evaluator values agree; no site or scorer anomaly was found. This diagnosis does not change the official score.", + "共享的最终状态协议错误,不是检索内容错误,也不是浏览器执行错误。": "Shared final-status protocol error, not a retrieval-content or browser-execution error.", + "共享的最终状态协议错误,不是检索内容错误,也不是浏览器执行错误。 本诊断不修改官方分数。": "Shared final-status protocol error, not a retrieval-content or browser-execution error. This diagnosis does not change the official score.", + "共享的长链任务完成错误:30 步上限前没有完成结账。两种浏览器路径细节不同,但都未越过同一验收终点。": "Shared long-horizon completion error: checkout was not completed within the 30-step limit. Browser paths differed in detail, but neither crossed the same acceptance endpoint.", + "共享的长链任务完成错误:30 步上限前没有完成结账。两种浏览器路径细节不同,但都未越过同一验收终点。 本诊断不修改官方分数。": "Shared long-horizon completion error: checkout was not completed within the 30-step limit. Browser paths differed in detail, but neither crossed the same acceptance endpoint. This diagnosis does not change the official score.", + + # Local environment and expansion guidance. + "Mac 上可以继续扩展哪些实例": "Which environments can be expanded on Mac", + "环境": "Environment", + "Apple Silicon Mac 状态": "Apple Silicon Mac status", + "完整集单站题": "Full-set single-site tasks", + "hard 单站题": "Hard single-site tasks", + "约束": "Constraint", + "已在本机实跑": "Measured locally", + "可部署,尚未实跑": "Deployable, not yet measured", + "可尝试,尚未验证": "Feasible to try, not yet verified", + "amd64 镜像;已通过 Colima 转译运行": "amd64 image; already measured through Colima translation", + "arm64 镜像;需约 100 GB 数据;只出现在跨站任务": "arm64 image; about 100 GB of data; appears only in cross-site tasks", + "amd64 镜像;需约 60 GB 数据与 Apple Silicon 转译": "amd64 image; about 60 GB of data plus Apple Silicon translation", + "Wikipedia 与 Map 的容量来自官方环境说明;状态中的“实跑”只指本项目已有执行证据。": "Wikipedia and Map capacity estimates come from the official environment guide. 'Measured' refers only to evidence already collected by this project.", + "最小扩展方案:": "Minimum expansion plan:", + "先从四个已验证站点的 678 个候选中做可完成性预检和分层抽样;Wikipedia、Map 等大数据环境后置。这样扩容不会先引入新的部署变量。": "Preflight solvability and draw a stratified sample from the 678 candidates on the four verified sites first. Defer the large Wikipedia and Map environments so expansion does not begin by adding deployment variables.", + "官方任务集与安装说明": "Official dataset and installation guide", + "官方环境容量说明": "Official environment capacity guide", + "官方完整集有 812 题,hard 子集有 258 题;官方没有 179 题版本。若运行 179 题,必须把它标注为本项目自定义样本并冻结任务清单。当前四个已实跑站点可直接进入下一轮预检:覆盖 655 个单站任务,计入只依赖这四站的跨站任务后为 678 个候选;hard 子集中可覆盖 233 个。": "The official full set contains 812 tasks and the hard subset contains 258; there is no official 179-task edition. A 179-task run must be labeled as a project-defined cohort with a frozen task list. The four locally measured sites can enter the next preflight directly: 655 single-site tasks, 678 candidates when cross-site tasks that use only these four sites are included, and 233 candidates in the hard subset.", + "站点确实部署在本机,不是公网代站。": "The sites were deployed locally, not replaced by public websites.", + "为什么五个失败任务会完全相同": "Why the same five tasks failed in both browsers", + "二、为什么五个失败任务会完全相同": "2. Why the same five tasks failed in both browsers", + "五、下一轮先扩展四个已验证站点": "5. Expand the four verified sites first", + "实际差异": "Observed mismatch", + "证据支持的诊断": "Evidence-supported diagnosis", + "置信度": "Confidence", + "高": "High", + "中": "Medium", + "表 2|诊断来自最终回答、官方 evaluator 与执行轨迹的交叉核对;不修改官方分数。": "Table 2 | Diagnoses cross-check final answers, official evaluator output, and execution trajectories; scores are unchanged.", + "两个浏览器共享 CowAgent、模型、提示与工具合同、站点状态、30 步上限和 evaluator,只有浏览器实现不同。5 个共同失败都落在共享层,因此结果高度相关并不反常;它更说明这批题没有触发足以改变最终成败的浏览器差异。": "The browsers share CowAgent, model, prompt and tool contract, site state, the 30-step limit, and evaluator; only the browser implementation changes. All five shared failures occurred in shared layers, so correlated outcomes are expected. More importantly, this cohort did not trigger a browser difference large enough to change the verdict.", + "结论:": "Conclusion:", + "现有证据不支持“网站本身报错”“官方 evaluator 实现错误”或“标准答案错误”。其中 #124 的站内搜索召回过宽会增加歧义,但返回结果仍需要按任务语义过滤。更准确的归因是:四题属于共享 Agent/模型的语义、聚合或输出协议问题,一题属于共享的长链完成与步数预算问题。这不是对模型通用能力的定论,只描述当前 CowAgent、模型与运行协议。": "The evidence does not support a broken site, an official evaluator implementation bug, or an incorrect answer key. Broad site-search recall adds ambiguity to Task #124, but results still need semantic filtering. The best-supported attribution is four shared agent/model semantic, aggregation, or output-protocol errors and one shared long-horizon completion and step-budget error. This is not a general verdict on the model; it applies to the current CowAgent, model, and run protocol.", + "官方包有 812 题和 258 题 hard 子集,没有官方 179 题版本;179 只能是另行声明的自定义样本。当前 Mac 已实跑的四个站点可覆盖 655 个单站任务;再计入只依赖这四站的跨站任务,共有 678 个候选,其中 hard 子集有 233 个。下一轮应在这些候选中先做登录、重置、健康检查与 evaluator 预检,再按简单、中等、困难和内核敏感交互分层抽样。": "The official package contains 812 tasks and a 258-task hard subset; there is no official 179-task edition. Any 179-task run is a separately declared custom cohort. The four sites already measured on this Mac cover 655 single-site tasks and 678 candidates after cross-site tasks using only those four sites are included; 233 are in the hard subset. The next run should preflight sign-in, reset, health, and evaluator behavior, then sample by easy, medium, hard, and browser-sensitive interaction strata.", + "Wikipedia 需要约 100 GB 数据,Map 需要约 60 GB 数据且当前公开镜像为 amd64;二者可作为后续扩展,但没有理由成为本机下一批的前置阻塞。": "Wikipedia requires about 100 GB of data. Map requires about 60 GB and its current public image is amd64. Both can be later expansions, but neither should block the next Mac cohort.", + "保留 CowAgent、模型、reasoning=low 和每题最多两次执行的固定协议,优先加入长页面、复杂编辑器、弹窗、多标签页、上传下载等更可能诱发浏览器差异的任务。实时网站另开 Online‑Mind2Web / WebVoyager 泛化轨道,LLM judge 与人工抽检单列。": "Keep CowAgent, the model, reasoning=low, and at most two executions per task fixed. Prioritize long pages, complex editors, dialogs, multiple tabs, uploads, and downloads that are more likely to expose browser differences. Use a separate Online-Mind2Web / WebVoyager generalization track for live sites, with LLM judging and human sampling reported separately.", + "两种浏览器都返回《The Hobbit》,遗漏《A Christmas Carol》(作者允许为空)。": "Both browsers returned The Hobbit but omitted A Christmas Carol, whose author may be null.", + "两者都把搜索结果中的无关投影仪计入 wireless earphone 价格范围,最大值报为 17,774.32,官方期望为 298.00。": "Both browsers included an unrelated projector in the wireless-earphone price range, reporting 17,774.32 instead of the official 298.00 maximum.", + "两者都汇总为 80.50,官方期望为 68.51;差额来自把水晶发箍计入 hair care/style 商品。": "Both browsers totaled 80.50 instead of 68.51; the difference came from treating a crystal headband as a hair-care/style product.", + "两者都正确找到空集合,却返回 SUCCESS + [];合同要求 NOT_FOUND_ERROR + null。": "Both browsers correctly found an empty set but returned SUCCESS + []; the contract requires NOT_FOUND_ERROR + null.", + "两者都选中正确商品并清空旧购物车,但未产生付款提交与成功页事件。": "Both browsers selected the correct product and cleared the old cart, but neither produced payment-submission and success-page events.", + "macOS 上的 Docker 由 Colima 虚拟机承载;本轮 Colima VM 地址是 192.168.5.3,其默认路由指向 192.168.5.2。后者是 VM 视角下的 macOS 宿主网关,不是虚拟机地址,也不是评测网站。Shopping、Shopping Admin、Reddit、GitLab 四个官方站点镜像通过 Docker 端口发布到这个宿主入口,独立的 Chromium/Moli 浏览器容器据此访问本地站点。": "Docker on macOS runs inside the Colima VM. In this run the VM address is 192.168.5.3 and its default route points to 192.168.5.2. The latter is the macOS host gateway from the VM's perspective, not the VM address or an evaluation website. The four official Shopping, Shopping Admin, Reddit, and GitLab images publish Docker ports through that host entry point, which the independent Chromium and Moli browser containers use to reach the local sites.", + "macOS 上的 Docker 由 Colima 虚拟机承载;本轮 Colima VM 地址是": "Docker on macOS runs inside the Colima VM. In this run the VM address is", + ",其默认路由指向": ", and its default route points to", + "。后者是 VM 视角下的 macOS 宿主网关,不是虚拟机地址,也不是评测网站。Shopping、Shopping Admin、Reddit、GitLab 四个官方站点镜像通过 Docker 端口发布到这个宿主入口,独立的 Chromium/Moli 浏览器容器据此访问本地站点。": ". The latter is the macOS host gateway from the VM's perspective, not the VM address or an evaluation website. The four official Shopping, Shopping Admin, Reddit, and GitLab images publish Docker ports through that host entry point, which the independent Chromium and Moli browser containers use to reach the local sites.", + ",只保留该题所需站点;env-ctrl 报告 ready 后,再做连续应用健康检查、清浏览器状态、登录测试账号并打开起始页。当前冻结证据保存了代码提交、运行协议和镜像身份;事件流从环境准备完成后开始,因此逐次 reset、ready 与 health 结果未形成独立绑定的准备回执。新版运行协议已要求保存该回执。": ", retaining only the sites required by the task. After env-ctrl reports ready, the runner performs consecutive application health checks, clears browser state, signs in with test accounts, and opens the start page. The frozen evidence retains the code commit, run protocol, and image identities. Because the event stream begins after preparation, per-run reset, readiness, and health outcomes were not bound into a separate preparation receipt. The new run protocol requires that receipt.", + "表 1|30 个任务的逐题最终结果配对。": "Table 1 | Paired final outcomes for 30 tasks.", + # Deployment, timing, browser matrix, and participant framing. "图 1 · 从官方站点镜像、逐题隔离与健康检查,到 CowAgent 观察—决策—操作闭环,再到 Chromium/Moli 对照和官方程序化验收。窄屏可横向滚动查看。": "Figure 1. From official site images, per-task isolation, and health checks through CowAgent's observe-decide-act loop, then Chromium/Moli comparison and official programmatic evaluation. Scroll horizontally on narrow screens.", "站点确实部署在本机,不是公网代站。": "The sites were deployed locally; they were not public-web substitutes.", @@ -805,6 +887,17 @@ def translate_text(value: str) -> str: translated = TEXT.get(core) if translated is None: patterns = ( + ( + r"最终数据缺少 (\d+) 项;官方期望 (\d+) 项,匹配 (\d+) 项。", + lambda match: ( + f"The final data is missing {match.group(1)} item(s); " + f"{match.group(3)} of {match.group(2)} expected item(s) matched." + ), + ), + ( + r"最终状态不符:期望 (.+),实际为 (.+)。", + lambda match: f"Final status mismatch: expected {match.group(1)}, observed {match.group(2)}.", + ), ( r"在同一 CowAgent、同一 (.+?)(reasoning=low)和同一 WebArena‑Verified 30 题样本下,对比两种浏览器完成真实网页任务的能力。", lambda match: ( diff --git a/research/browser-use-failure-analysis.json b/research/browser-use-failure-analysis.json new file mode 100644 index 000000000..8b44a32d1 --- /dev/null +++ b/research/browser-use-failure-analysis.json @@ -0,0 +1,37 @@ +{ + "schema": "lex_browser_use_failure_analysis/1", + "scope": { + "benchmark": "WebArena-Verified 1.2.3", + "task_ids": [68, 124, 142, 168, 506], + "engines": ["chromium", "moli"], + "evidence": "final response, official evaluator output, and autonomous BrowserTool trajectory", + "score_effect": "none" + }, + "tasks": { + "68": { + "mismatch": "两种浏览器都返回《The Hobbit》,遗漏《A Christmas Carol》(作者允许为空)。", + "diagnosis": "共享的跨帖聚合与空值处理错误。轨迹已看到两篇符合条件的帖子,说明站点内容存在;失败发生在最终答案整理。", + "confidence": "高" + }, + "124": { + "mismatch": "两者都把搜索结果中的无关投影仪计入 wireless earphone 价格范围,最大值报为 17,774.32,官方期望为 298.00。", + "diagnosis": "共享的搜索结果相关性过滤错误;站内搜索召回过宽是诱因,但 Agent 仍应按任务语义筛选商品。", + "confidence": "中" + }, + "142": { + "mismatch": "两者都汇总为 80.50,官方期望为 68.51;差额来自把水晶发箍计入 hair care/style 商品。", + "diagnosis": "共享的商品类别判定错误。订单数据和 evaluator 数值一致,未见站点或计分器异常。", + "confidence": "高" + }, + "168": { + "mismatch": "两者都正确找到空集合,却返回 SUCCESS + [];合同要求 NOT_FOUND_ERROR + null。", + "diagnosis": "共享的最终状态协议错误,不是检索内容错误,也不是浏览器执行错误。", + "confidence": "高" + }, + "506": { + "mismatch": "两者都选中正确商品并清空旧购物车,但未产生付款提交与成功页事件。", + "diagnosis": "共享的长链任务完成错误:30 步上限前没有完成结账。两种浏览器路径细节不同,但都未越过同一验收终点。", + "confidence": "高" + } + } +} diff --git a/research/report-changes.json b/research/report-changes.json index 4f2bad6a8..0d1404d08 100644 --- a/research/report-changes.json +++ b/research/report-changes.json @@ -2,6 +2,16 @@ "schema_version": 1, "history_scope": "本页从有完整审计依据的修订开始记录。更早的站点版本仍保留在 Artifact Hub,但没有逐项变更说明,故不补编历史。", "browser_use_reviews": [ + { + "id": "review-2026-09-10-shared-failure-and-expansion", + "date": "2026-09-10", + "title": "共同失败归因与本机扩展核验", + "status": "已处理", + "scope": "五个共同失败任务、结果详情信息层级、Colima 网络身份和 Mac 环境扩展范围。", + "findings": "结果相同尚未解释到共享链路;详情页重复展示合同、回答和 evaluator 原始 JSON;192.168.5.2 的角色容易被误读为虚拟机地址;179 题并非官方子集。", + "corrections": "逐题交叉核对回答、规则和轨迹并给出有置信度边界的诊断;把任务目标与逐规则验收合并为单一可读区,技术 JSON 默认折叠;明确 192.168.5.2 是 VM 看到的 macOS 宿主网关、VM 地址为 192.168.5.3;列出四个已实跑站点的 678 个候选与 233 个 hard 候选。", + "remaining": "Wikipedia 与 Map 尚未在本机实跑;旧结果仍缺逐执行环境准备回执,下一批必须先做可完成性预检并保存回执。" + }, { "id": "review-2026-09-10-track-naming-and-release-gates", "date": "2026-09-10", @@ -54,6 +64,34 @@ } ], "browser_use_releases": [ + { + "id": "2026-09-10-shared-failure-diagnosis-and-mac-expansion", + "date": "2026-09-10", + "title": "解释共同失败并明确 Mac 扩展边界", + "summary": "报告新增五个共同失败的证据诊断,结果详情合并重复的合同与 evaluator 展示,并明确 Colima 宿主网关、本地站点实跑范围及下一批可在 Mac 上预检的任务规模。官方成绩仍为 Chromium 25/30、Moli 25/30。", + "attempts_sha256": "517b613adcc40fc0ebe41af2ab2115d76f49faaba5378b426cc196b5b633e9c7", + "selection_sha256": "ba3c344df003dd4eba3a5d1ef97162ee98df2ed728f7c29c4188bc4247982d38", + "changes": [ + [ + "共同失败", + "相同分数只被描述为无分歧,未说明五题为何同时失败。", + "按任务核对最终回答、官方 evaluator 和轨迹,区分语义聚合、相关性过滤、类别判断、状态协议与长链完成问题。", + "新增诊断不修改官方分数;未发现站点、evaluator 或答案合同错误的证据。" + ], + [ + "结果详情", + "合同、实际回答和规则结果在相邻区域重复,原始 JSON 抢占主要阅读层级。", + "合并为“任务要求与验收结果”,先展示可读规则结论,官方期望、本次观察和技术断言默认折叠。", + "原始公开证据和下载入口保持不变。" + ], + [ + "本地环境与扩展", + "Colima 网关角色和可继续运行的官方环境范围不够明确。", + "区分 VM 地址与宿主网关,说明四站实跑证据;纠正官方不存在 179 题子集,并给出 678 个当前可预检候选和 233 个 hard 候选。", + "Wikipedia、Map 仍标为尚未实跑,不把理论可部署写成已验证。" + ] + ] + }, { "id": "2026-09-10-track-naming-and-release-hardening", "date": "2026-09-10", diff --git a/research/webarena-mac-expansion.json b/research/webarena-mac-expansion.json new file mode 100644 index 000000000..5edd97025 --- /dev/null +++ b/research/webarena-mac-expansion.json @@ -0,0 +1,24 @@ +{ + "schema": "lex_webarena_mac_expansion/1", + "verified_on": "2026-09-10", + "official_counts": { + "full": 812, + "hard": 258, + "current_four_sites_single_site": 655, + "current_four_sites_including_cross_site": 678, + "current_four_sites_hard_including_cross_site": 233, + "official_179_subset": false + }, + "environments": [ + ["Shopping", "已在本机实跑", "187", "56", "amd64 镜像;已通过 Colima 转译运行"], + ["Shopping Admin", "已在本机实跑", "182", "55", "amd64 镜像;已通过 Colima 转译运行"], + ["Reddit", "已在本机实跑", "106", "42", "amd64 镜像;已通过 Colima 转译运行"], + ["GitLab", "已在本机实跑", "180", "57", "amd64 镜像;已通过 Colima 转译运行"], + ["Wikipedia", "可部署,尚未实跑", "0", "0", "arm64 镜像;需约 100 GB 数据;只出现在跨站任务"], + ["Map", "可尝试,尚未验证", "109", "0", "amd64 镜像;需约 60 GB 数据与 Apple Silicon 转译"] + ], + "sources": [ + "https://github.com/ServiceNow/webarena-verified", + "https://servicenow.github.io/webarena-verified/dev/environments/" + ] +} diff --git a/scripts/check_browser_use_report.py b/scripts/check_browser_use_report.py index 0b8382274..3a36b1bd4 100644 --- a/scripts/check_browser_use_report.py +++ b/scripts/check_browser_use_report.py @@ -210,15 +210,21 @@ def check( marker not in rendered_pages[page] for page in result_pages for marker in ( - "Agent 实际回答", - "官方 evaluator 逐规则判定", + "任务要求与验收结果", + "查看官方期望与本次观察", "CowAgent 自主执行轨迹", "运行、日志与证据", "attempt-summary", - "查看逐规则判定", + "查看任务与验收", ) ): raise ValueError("every result page must expose result, evaluator, trajectory and logs") + if any( + marker in rendered_pages[page] + for page in result_pages + for marker in ("Ground truth 与实际最终回答", "官方 evaluator 逐规则判定") + ): + raise ValueError("result pages must not duplicate answer and evaluator presentations") html = rendered_pages[index] standalone = (report_dir / "guide/article.html").read_text(encoding="utf-8") if " None: assert 'placeholder="Task, intent, or acceptance contract"' in localized +def test_result_verification_is_readable_and_does_not_duplicate_raw_panels() -> None: + result = { + "evaluators_results": [ + { + "evaluator_name": "AgentResponseEvaluator", + "score": 0, + "expected": {"status": "success", "retrieved_data": [1, 2]}, + "actual": {"status": "success", "retrieved_data": [1]}, + "assertions": [ + { + "assertion_msgs": [ + "Array is missing 1 expected element(s). Matched (1/2)" + ] + } + ], + } + ] + } + + rendered = _verification_html( + {"intent": "Return both records."}, + result, + {"status": "success", "retrieved_data": [1]}, + ) + + assert "最终数据缺少 1 项;官方期望 2 项,匹配 1 项" in rendered + assert rendered.count("

    官方期望

    ") == 1 + assert "
    " not in rendered + assert "Ground truth 与实际最终回答" not in rendered + + def test_paired_summary_uses_final_logical_results() -> None: rows = [ {"engine": "chromium", "task_id": 1, "attempt": 1, "score": 1}, @@ -229,15 +261,17 @@ def test_report_publishes_one_final_logical_result_and_aggregates_cost(tmp_path: assert "逐结果覆盖" not in review attempt = (output / "results/chromium-44.html").read_text(encoding="utf-8") for expected in ( - "Agent 实际回答", - "官方 evaluator 逐规则判定", + "任务要求与验收结果", "CowAgent 自主执行轨迹", "运行、日志与证据", 'href="results/chromium-44.html#trajectory"', "attempt-summary", - "查看逐规则判定", + "查看任务与验收", ): assert expected in attempt + assert "没有 evaluator 明细" in attempt + assert "Ground truth 与实际最终回答" not in attempt + assert "官方 evaluator 逐规则判定" not in attempt for artifact in ( "agent-response.json", "evaluation.json", From d762fdbcbb891a01b3ccd2532da25e3b31ddfbed Mon Sep 17 00:00:00 2001 From: kevin Date: Thu, 10 Sep 2026 20:37:29 +0800 Subject: [PATCH 038/127] bind browser use report publication --- evaluation/browser-use/current-report.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evaluation/browser-use/current-report.json b/evaluation/browser-use/current-report.json index 4d045f280..1efd9792f 100644 --- a/evaluation/browser-use/current-report.json +++ b/evaluation/browser-use/current-report.json @@ -32,7 +32,7 @@ "base_url": "https://artifact-hub.dev.lexmount.net", "site_slug": "28OH4YZkUYF1", "share_url": "https://artifact-hub.dev.lexmount.net/v/5Vhnhdc_f8FEM2RzctQm-ChRlXUf174Y", - "expected_version": "ver_ffaaf617358b45df967b2cd9c77bd500", + "expected_version": "ver_0d65255202244a61b20ec369f12decc7", "state": "verified" } } From 7962519ea3354788a466919fda728470d45f2776 Mon Sep 17 00:00:00 2001 From: kevin Date: Thu, 10 Sep 2026 21:14:10 +0800 Subject: [PATCH 039/127] state only proven browser use failure points --- harness/browser_use/report.py | 27 ++++++++------- harness/browser_use/report_i18n.py | 32 ++++++++++++++++++ research/browser-use-failure-analysis.json | 39 ++++++++++++---------- research/report-changes.json | 14 ++++---- scripts/check_browser_use_report.py | 3 ++ tests/browser_use/test_report.py | 5 +++ 6 files changed, 84 insertions(+), 36 deletions(-) diff --git a/harness/browser_use/report.py b/harness/browser_use/report.py index b8854d584..6ff00e8a2 100644 --- a/harness/browser_use/report.py +++ b/harness/browser_use/report.py @@ -1087,8 +1087,9 @@ def _article_body( [ f"#{task_id}", diagnosis["mismatch"], - diagnosis["diagnosis"], - diagnosis["confidence"], + diagnosis["confirmed_failure_point"], + diagnosis["direct_evidence"], + diagnosis["root_cause_boundary"], ] for task_id, diagnosis in SHARED_FAILURE_DIAGNOSES.items() ] @@ -1110,14 +1111,14 @@ def _article_body( caption="表 1|30 个任务的逐题最终结果配对。", ) + f"

    严谨表述只能是“本轮没有观察到成功率差异”,不能写成“两种内核等价”。{paired['paired']} 题对小效应检出力有限;更关键的是,样本没有诱发内核分歧。

    " - "

    二、为什么五个失败任务会完全相同

    " - f"

    两个浏览器共享 CowAgent、模型、提示与工具合同、站点状态、30 步上限和 evaluator,只有浏览器实现不同。{paired['both_fail']} 个共同失败都落在共享层,因此结果高度相关并不反常;它更说明这批题没有触发足以改变最终成败的浏览器差异。

    " + "

    二、五个共同失败,具体失败在哪里

    " + f"

    两个浏览器共享 CowAgent、模型、提示与工具合同、站点状态、30 步上限和 evaluator,只有浏览器实现不同。逐条证据只能确认验收链路中的首个失败点;除 506 题明确触发步数上限外,记录不足以把错误进一步归因到模型、CowAgent 策略或提示合同中的某一项。

    " + _table( - ["任务", "实际差异", "证据支持的诊断", "置信度"], + ["任务", "官方判定差异", "已确认失败点", "直接证据", "尚不能判定"], failures, - caption="表 2|诊断来自最终回答、官方 evaluator 与执行轨迹的交叉核对;不修改官方分数。", + caption="表 2|只陈述最终回答、官方 evaluator 与执行轨迹能够直接证明的事实;不修改官方分数。", ) - + "

    结论:现有证据不支持“网站本身报错”“官方 evaluator 实现错误”或“标准答案错误”。其中 #124 的站内搜索召回过宽会增加歧义,但返回结果仍需要按任务语义过滤。更准确的归因是:四题属于共享 Agent/模型的语义、聚合或输出协议问题,一题属于共享的长链完成与步数预算问题。这不是对模型通用能力的定论,只描述当前 CowAgent、模型与运行协议。

    三、简单、中等、困难都覆盖了,但“难度”只是操作长度

    WebArena‑Verified 没有官方 easy/medium/hard 三档。本项目在运行前按最少可见 UI 操作数注册为 1–4、5–7、8+ 三档,配额 5/10/15。它控制操作链覆盖,不等同于推理难度。

    " + + "

    结论:#68、#124、#142 和 #168 的确定事实是最终答案构造或状态协议错误;#506 的确定事实是达到 30 步上限时尚未产生付款提交和成功页事件。站点错误、evaluator 实现错误、答案合同错误以及模型内部决策原因,在这批证据中均未被证明。因而本轮不能单独判定为“模型能力不行”,只能判定当前 CowAgent、模型、提示与工具合同的组合在这些验收点失败。

    三、简单、中等、困难都覆盖了,但“难度”只是操作长度

    WebArena‑Verified 没有官方 easy/medium/hard 三档。本项目在运行前按最少可见 UI 操作数注册为 1–4、5–7、8+ 三档,配额 5/10/15。它控制操作链覆盖,不等同于推理难度。

    " + _table( ["本项目分层", "Chromium", "Moli"], _group_rows(attempts, tasks, "tier"), @@ -1462,7 +1463,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) home += _section( "先看结论", - f'
    Chromium 任务通过
    {c["passed"]}/{c["completed"]}

    {_pct(c["score"])}

    Moli 任务通过
    {m["passed"]}/{m["completed"]}

    {_pct(m["score"])}

    单边胜出
    {paired["chromium_only"] + paired["moli_only"]}

    {paired["paired"]} 个完整配对

    共同失败
    {paired["both_fail"]}

    共享链路诊断

    当前结论:本轮没有观察到浏览器成功率差异;这证明对照流程可比,不证明两个内核等价。

    失败归因:{paired["both_fail"]} 个共同失败中,四题指向共享 Agent/模型的语义、聚合或输出协议,一题指向共享的步数预算与长链完成;没有发现站点、官方 evaluator 或答案合同错误的证据。

    这是固定选择的 30 题试验,不是从全部 WebArena‑Verified 任务随机抽样;比例只描述本样本,不外推总体能力。

    ', + f'
    Chromium 任务通过
    {c["passed"]}/{c["completed"]}

    {_pct(c["score"])}

    Moli 任务通过
    {m["passed"]}/{m["completed"]}

    {_pct(m["score"])}

    单边胜出
    {paired["chromium_only"] + paired["moli_only"]}

    {paired["paired"]} 个完整配对

    共同失败
    {paired["both_fail"]}

    已确认失败点

    当前结论:本轮没有观察到浏览器成功率差异;这证明对照流程可比,不证明两个内核等价。

    失败结论:四题的首个可证失败点位于最终答案构造或状态协议,一题在 30 步上限时仍未完成付款提交。现有证据不能把前四题进一步归因到模型、CowAgent 或提示合同中的某一项。

    这是固定选择的 30 题试验,不是从全部 WebArena‑Verified 任务随机抽样;比例只描述本样本,不外推总体能力。

    ', ) home += _section( "任务族覆盖", @@ -1812,9 +1813,11 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: "任务要求与验收结果", _verification_html(task, evaluation_record, response_record) + ( - '
    证据诊断:' - + _e(SHARED_FAILURE_DIAGNOSES[public["task_id"]]["diagnosis"]) - + " 本诊断不修改官方分数。
    " + '
    已确认失败点:' + + _e(SHARED_FAILURE_DIAGNOSES[public["task_id"]]["confirmed_failure_point"]) + + " 直接证据:" + + _e(SHARED_FAILURE_DIAGNOSES[public["task_id"]]["direct_evidence"]) + + "
    " if public["task_id"] in SHARED_FAILURE_DIAGNOSES else "" ) @@ -2050,7 +2053,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: "claims": [ {"claim": "当前样本未观察到成功率差异", "basis": paired}, { - "claim": "失败任务完全重合,且证据诊断均位于共享链路", + "claim": "失败任务完全重合;证据确认四题止于最终答案或状态协议,一题止于步数上限前的结账流程", "basis": SHARED_FAILURE_DIAGNOSES, }, ], diff --git a/harness/browser_use/report_i18n.py b/harness/browser_use/report_i18n.py index 00191b67f..6e324a658 100644 --- a/harness/browser_use/report_i18n.py +++ b/harness/browser_use/report_i18n.py @@ -398,6 +398,9 @@ "Chromium 任务通过": "Chromium tasks passed", "Moli 任务通过": "Moli tasks passed", "需要可完成性核验": "Solvability review required", + "已确认失败点": "Confirmed failure points", + "失败结论:": "Failure conclusion:", + "四题的首个可证失败点位于最终答案构造或状态协议,一题在 30 步上限时仍未完成付款提交。现有证据不能把前四题进一步归因到模型、CowAgent 或提示合同中的某一项。": "The first provable failure point is final-answer construction or status encoding in four tasks; one task had not submitted payment when the 30-step limit was reached. The evidence cannot further attribute the first four failures to the model, CowAgent, or prompt contract individually.", "共享链路诊断": "Shared-stack diagnosis", "失败归因:": "Failure attribution:", "5 个共同失败中,四题指向共享 Agent/模型的语义、聚合或输出协议,一题指向共享的步数预算与长链完成;没有发现站点、官方 evaluator 或答案合同错误的证据。": "Among the five shared failures, four point to shared agent/model semantics, aggregation, or output protocol, and one points to the shared step budget and long-horizon completion. No evidence of site, official evaluator, or answer-contract errors was found.", @@ -435,6 +438,18 @@ "任务与验收": "Task and acceptance", "查看任务与验收": "View task and acceptance", "任务要求与验收结果": "Task requirements and acceptance result", + "已确认失败点:": "Confirmed failure point:", + "直接证据:": "Direct evidence:", + "最终答案少了一条必需记录。": "The final answer omitted one required record.", + "两条最终轨迹都已读取《A Christmas Carol》帖子;官方 evaluator 报告 Matched (1/2)。": "Both final trajectories read the A Christmas Carol post; the official evaluator reports Matched (1/2).", + "最终价格聚合纳入了不符合 wireless earphone 语义的商品。": "The final price aggregation included a product that does not satisfy the wireless-earphone semantics.", + "轨迹中的 17,774.32 对应 QFWCJ 4K Short Throw Projector,且与最终错误最大值完全相同。": "In the trajectory, 17,774.32 belongs to a QFWCJ 4K Short Throw Projector and exactly matches the incorrect final maximum.", + "最终求和多计入了 11.99 的 Crystal Headband。": "The final sum incorrectly included the 11.99 Crystal Headband.", + "轨迹列出 50.52 的 conditioner、11.99 的 headband 和 17.99 的 haircolor;80.50=50.52+11.99+17.99,68.51=50.52+17.99。": "The trajectory lists a 50.52 conditioner, an 11.99 headband, and a 17.99 haircolor item; 80.50=50.52+11.99+17.99, while 68.51=50.52+17.99.", + "空结果被编码成错误的最终状态。": "The empty result was encoded with the wrong final status.", + "官方 evaluator 的唯一失败断言是 Expected not_found_error, got success;检索内容同为空。": "The official evaluator's only failed assertion is Expected not_found_error, got success; both sides contain no retrieved item.", + "结账在付款提交之前停止。": "Checkout stopped before payment submission.", + "官方 evaluator 已确认正确商品加入购物车;付款提交与成功页规则失败,最终响应明确记录达到单次运行最大步骤数。": "The official evaluator confirms that the correct product was added to the cart; the payment-submission and success-page rules failed, and the final response explicitly records reaching the run's maximum step count.", "任务目标": "Task objective", "Agent 可自主选择 UI 路径;计分只看以下最终回答与必要网络副作用。": "The agent may choose any UI path. Scoring uses only the final answer and required network side effects below.", "检查 Agent 最终回答的状态、字段和值是否符合官方答案合同。": "Checks whether the status, fields, and values in the agent's final answer satisfy the official answer contract.", @@ -484,7 +499,24 @@ "站点确实部署在本机,不是公网代站。": "The sites were deployed locally, not replaced by public websites.", "为什么五个失败任务会完全相同": "Why the same five tasks failed in both browsers", "二、为什么五个失败任务会完全相同": "2. Why the same five tasks failed in both browsers", + "二、五个共同失败,具体失败在哪里": "2. Where the five shared failures occurred", "五、下一轮先扩展四个已验证站点": "5. Expand the four verified sites first", + "官方判定差异": "Official verdict mismatch", + "直接证据": "Direct evidence", + "尚不能判定": "Not established", + "表 2|只陈述最终回答、官方 evaluator 与执行轨迹能够直接证明的事实;不修改官方分数。": "Table 2 | Only facts directly established by the final response, official evaluator, and execution trajectory are stated; scores are unchanged.", + "两个浏览器共享 CowAgent、模型、提示与工具合同、站点状态、30 步上限和 evaluator,只有浏览器实现不同。逐条证据只能确认验收链路中的首个失败点;除 506 题明确触发步数上限外,记录不足以把错误进一步归因到模型、CowAgent 策略或提示合同中的某一项。": "The browsers share CowAgent, model, prompt and tool contract, site state, the 30-step limit, and evaluator; only the browser implementation changes. The evidence establishes the first failed acceptance point. Apart from Task 506 explicitly reaching the step limit, the records cannot attribute the errors further to the model, CowAgent policy, or prompt contract individually.", + "#68、#124、#142 和 #168 的确定事实是最终答案构造或状态协议错误;#506 的确定事实是达到 30 步上限时尚未产生付款提交和成功页事件。站点错误、evaluator 实现错误、答案合同错误以及模型内部决策原因,在这批证据中均未被证明。因而本轮不能单独判定为“模型能力不行”,只能判定当前 CowAgent、模型、提示与工具合同的组合在这些验收点失败。": "For Tasks 68, 124, 142, and 168, the established fact is an error in final-answer construction or status encoding. For Task 506, the established fact is that payment submission and the success-page event were still absent when the 30-step limit was reached. This evidence does not establish a site error, evaluator implementation bug, answer-contract error, or hidden model decision cause. The run therefore cannot isolate the result as a model-capability failure; it establishes only that the current CowAgent, model, prompt, and tool-contract combination failed at these acceptance points.", + "两种浏览器都只返回《The Hobbit》,官方合同还要求《A Christmas Carol》(author=null)。": "Both browsers returned only The Hobbit; the official contract also requires A Christmas Carol with author=null.", + "两种浏览器都返回 min=0.01、max=17774.32;官方合同要求 max=298.00。": "Both browsers returned min=0.01 and max=17774.32; the official contract requires max=298.00.", + "两种浏览器都返回 80.50;官方合同要求 68.51。": "Both browsers returned 80.50; the official contract requires 68.51.", + "两种浏览器都返回 SUCCESS + [];官方合同要求 NOT_FOUND_ERROR + null。": "Both browsers returned SUCCESS + []; the official contract requires NOT_FOUND_ERROR + null.", + "最终逻辑结果均已添加正确商品,但没有付款提交事件和下单成功页事件。": "Both final logical results added the correct product but contain neither a payment-submission event nor an order-success-page event.", + "可确认遗漏发生在页面内容已经进入轨迹之后;现有证据不能判定模型为何丢弃该记录。": "The omission occurred after the page content entered the trajectory; the evidence cannot establish why the model discarded it.", + "可确认错误值来自无关投影仪;现有证据不能区分是模型判断、Agent 策略还是任务提示约束不足导致未过滤。": "The incorrect value is established as coming from an unrelated projector; the evidence cannot distinguish model judgment, agent policy, or insufficient prompt constraints as the cause of the missing filter.", + "可确认差额及其来源;现有证据不能判定模型为何把 headband 纳入任务口径。": "The amount and its source are established; the evidence cannot establish why the model included the headband in scope.", + "可确认是最终响应协议不匹配,不是页面导航失败;现有证据不能判定错误映射来自模型还是 Agent 响应封装。": "This is established as a final-response protocol mismatch, not a navigation failure; the evidence cannot determine whether the incorrect mapping came from the model or agent response wrapper.", + "30 步上限是已确认的直接停止条件;现有证据不能进一步量化模型规划、Agent 调度和浏览器工具开销各自消耗了多少不必要步骤。": "The 30-step limit is the confirmed immediate stop condition; the evidence cannot quantify how many unnecessary steps came from model planning, agent scheduling, or browser-tool overhead.", "实际差异": "Observed mismatch", "证据支持的诊断": "Evidence-supported diagnosis", "置信度": "Confidence", diff --git a/research/browser-use-failure-analysis.json b/research/browser-use-failure-analysis.json index 8b44a32d1..793b0f3ce 100644 --- a/research/browser-use-failure-analysis.json +++ b/research/browser-use-failure-analysis.json @@ -1,37 +1,42 @@ { - "schema": "lex_browser_use_failure_analysis/1", + "schema": "lex_browser_use_failure_analysis/2", "scope": { "benchmark": "WebArena-Verified 1.2.3", "task_ids": [68, 124, 142, 168, 506], "engines": ["chromium", "moli"], - "evidence": "final response, official evaluator output, and autonomous BrowserTool trajectory", + "evidence": "final logical response, official evaluator output, and autonomous BrowserTool trajectory", "score_effect": "none" }, "tasks": { "68": { - "mismatch": "两种浏览器都返回《The Hobbit》,遗漏《A Christmas Carol》(作者允许为空)。", - "diagnosis": "共享的跨帖聚合与空值处理错误。轨迹已看到两篇符合条件的帖子,说明站点内容存在;失败发生在最终答案整理。", - "confidence": "高" + "mismatch": "两种浏览器都只返回《The Hobbit》,官方合同还要求《A Christmas Carol》(author=null)。", + "confirmed_failure_point": "最终答案少了一条必需记录。", + "direct_evidence": "两条最终轨迹都已读取《A Christmas Carol》帖子;官方 evaluator 报告 Matched (1/2)。", + "root_cause_boundary": "可确认遗漏发生在页面内容已经进入轨迹之后;现有证据不能判定模型为何丢弃该记录。" }, "124": { - "mismatch": "两者都把搜索结果中的无关投影仪计入 wireless earphone 价格范围,最大值报为 17,774.32,官方期望为 298.00。", - "diagnosis": "共享的搜索结果相关性过滤错误;站内搜索召回过宽是诱因,但 Agent 仍应按任务语义筛选商品。", - "confidence": "中" + "mismatch": "两种浏览器都返回 min=0.01、max=17774.32;官方合同要求 max=298.00。", + "confirmed_failure_point": "最终价格聚合纳入了不符合 wireless earphone 语义的商品。", + "direct_evidence": "轨迹中的 17,774.32 对应 QFWCJ 4K Short Throw Projector,且与最终错误最大值完全相同。", + "root_cause_boundary": "可确认错误值来自无关投影仪;现有证据不能区分是模型判断、Agent 策略还是任务提示约束不足导致未过滤。" }, "142": { - "mismatch": "两者都汇总为 80.50,官方期望为 68.51;差额来自把水晶发箍计入 hair care/style 商品。", - "diagnosis": "共享的商品类别判定错误。订单数据和 evaluator 数值一致,未见站点或计分器异常。", - "confidence": "高" + "mismatch": "两种浏览器都返回 80.50;官方合同要求 68.51。", + "confirmed_failure_point": "最终求和多计入了 11.99 的 Crystal Headband。", + "direct_evidence": "轨迹列出 50.52 的 conditioner、11.99 的 headband 和 17.99 的 haircolor;80.50=50.52+11.99+17.99,68.51=50.52+17.99。", + "root_cause_boundary": "可确认差额及其来源;现有证据不能判定模型为何把 headband 纳入任务口径。" }, "168": { - "mismatch": "两者都正确找到空集合,却返回 SUCCESS + [];合同要求 NOT_FOUND_ERROR + null。", - "diagnosis": "共享的最终状态协议错误,不是检索内容错误,也不是浏览器执行错误。", - "confidence": "高" + "mismatch": "两种浏览器都返回 SUCCESS + [];官方合同要求 NOT_FOUND_ERROR + null。", + "confirmed_failure_point": "空结果被编码成错误的最终状态。", + "direct_evidence": "官方 evaluator 的唯一失败断言是 Expected not_found_error, got success;检索内容同为空。", + "root_cause_boundary": "可确认是最终响应协议不匹配,不是页面导航失败;现有证据不能判定错误映射来自模型还是 Agent 响应封装。" }, "506": { - "mismatch": "两者都选中正确商品并清空旧购物车,但未产生付款提交与成功页事件。", - "diagnosis": "共享的长链任务完成错误:30 步上限前没有完成结账。两种浏览器路径细节不同,但都未越过同一验收终点。", - "confidence": "高" + "mismatch": "最终逻辑结果均已添加正确商品,但没有付款提交事件和下单成功页事件。", + "confirmed_failure_point": "结账在付款提交之前停止。", + "direct_evidence": "官方 evaluator 已确认正确商品加入购物车;付款提交与成功页规则失败,最终响应明确记录达到单次运行最大步骤数。", + "root_cause_boundary": "30 步上限是已确认的直接停止条件;现有证据不能进一步量化模型规划、Agent 调度和浏览器工具开销各自消耗了多少不必要步骤。" } } } diff --git a/research/report-changes.json b/research/report-changes.json index 0d1404d08..ec4af33c2 100644 --- a/research/report-changes.json +++ b/research/report-changes.json @@ -5,11 +5,11 @@ { "id": "review-2026-09-10-shared-failure-and-expansion", "date": "2026-09-10", - "title": "共同失败归因与本机扩展核验", + "title": "共同失败点与本机扩展核验", "status": "已处理", "scope": "五个共同失败任务、结果详情信息层级、Colima 网络身份和 Mac 环境扩展范围。", "findings": "结果相同尚未解释到共享链路;详情页重复展示合同、回答和 evaluator 原始 JSON;192.168.5.2 的角色容易被误读为虚拟机地址;179 题并非官方子集。", - "corrections": "逐题交叉核对回答、规则和轨迹并给出有置信度边界的诊断;把任务目标与逐规则验收合并为单一可读区,技术 JSON 默认折叠;明确 192.168.5.2 是 VM 看到的 macOS 宿主网关、VM 地址为 192.168.5.3;列出四个已实跑站点的 678 个候选与 233 个 hard 候选。", + "corrections": "逐题交叉核对最终回答、官方规则和执行轨迹,只发布可直接证明的失败点、证据与归因边界;把任务目标与逐规则验收合并为单一可读区,技术 JSON 默认折叠;明确 192.168.5.2 是 VM 看到的 macOS 宿主网关、VM 地址为 192.168.5.3;列出四个已实跑站点的 678 个候选与 233 个 hard 候选。", "remaining": "Wikipedia 与 Map 尚未在本机实跑;旧结果仍缺逐执行环境准备回执,下一批必须先做可完成性预检并保存回执。" }, { @@ -67,16 +67,16 @@ { "id": "2026-09-10-shared-failure-diagnosis-and-mac-expansion", "date": "2026-09-10", - "title": "解释共同失败并明确 Mac 扩展边界", - "summary": "报告新增五个共同失败的证据诊断,结果详情合并重复的合同与 evaluator 展示,并明确 Colima 宿主网关、本地站点实跑范围及下一批可在 Mac 上预检的任务规模。官方成绩仍为 Chromium 25/30、Moli 25/30。", + "title": "确认共同失败点并明确 Mac 扩展边界", + "summary": "报告逐题列出五个共同失败的可证失败点、直接证据与归因边界,结果详情合并重复的合同与 evaluator 展示,并明确 Colima 宿主网关、本地站点实跑范围及下一批可在 Mac 上预检的任务规模。官方成绩仍为 Chromium 25/30、Moli 25/30。", "attempts_sha256": "517b613adcc40fc0ebe41af2ab2115d76f49faaba5378b426cc196b5b633e9c7", "selection_sha256": "ba3c344df003dd4eba3a5d1ef97162ee98df2ed728f7c29c4188bc4247982d38", "changes": [ [ "共同失败", - "相同分数只被描述为无分歧,未说明五题为何同时失败。", - "按任务核对最终回答、官方 evaluator 和轨迹,区分语义聚合、相关性过滤、类别判断、状态协议与长链完成问题。", - "新增诊断不修改官方分数;未发现站点、evaluator 或答案合同错误的证据。" + "相同分数只被描述为无分歧,未定位五题的具体验收失败点。", + "按任务核对最终回答、官方 evaluator 和轨迹,分别锁定遗漏记录、纳入错误商品、错误求和、状态编码和付款前停止。", + "只发布证据可直接证明的事实和归因边界,不推断模型隐藏决策;官方分数不变。" ], [ "结果详情", diff --git a/scripts/check_browser_use_report.py b/scripts/check_browser_use_report.py index 3a36b1bd4..de4c9eef2 100644 --- a/scripts/check_browser_use_report.py +++ b/scripts/check_browser_use_report.py @@ -271,6 +271,9 @@ def check( "后续 179 题", "需要注意:", "代理实测返回 team_model_access_denied", + "证据支持的诊断", + "共享链路诊断", + "失败归因:", ) for page, text in rendered_pages.items(): if any(term in text for term in forbidden_report_terms): diff --git a/tests/browser_use/test_report.py b/tests/browser_use/test_report.py index 29df59290..e97ce4c03 100644 --- a/tests/browser_use/test_report.py +++ b/tests/browser_use/test_report.py @@ -283,6 +283,11 @@ def test_report_publishes_one_final_logical_result_and_aggregates_cost(tmp_path: assert "诊断重试" not in "".join( path.read_text(encoding="utf-8") for path in output.rglob("*.html") ) + formal_html = "".join( + path.read_text(encoding="utf-8") for path in output.rglob("*.html") + ) + for speculative_label in ("证据支持的诊断", "共享链路诊断", "失败归因:"): + assert speculative_label not in formal_html assert not list(output.rglob("*.svg")) From b5f29a3befdd1442ec4c8d24dbab6ead05d98396 Mon Sep 17 00:00:00 2001 From: kevin Date: Thu, 10 Sep 2026 21:16:00 +0800 Subject: [PATCH 040/127] bind updated browser use report version --- evaluation/browser-use/current-report.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evaluation/browser-use/current-report.json b/evaluation/browser-use/current-report.json index 1efd9792f..dac9641f7 100644 --- a/evaluation/browser-use/current-report.json +++ b/evaluation/browser-use/current-report.json @@ -32,7 +32,7 @@ "base_url": "https://artifact-hub.dev.lexmount.net", "site_slug": "28OH4YZkUYF1", "share_url": "https://artifact-hub.dev.lexmount.net/v/5Vhnhdc_f8FEM2RzctQm-ChRlXUf174Y", - "expected_version": "ver_0d65255202244a61b20ec369f12decc7", + "expected_version": "ver_5347ea126d1047fcbe7e8675792293af", "state": "verified" } } From d70ef71ef17dd6dcec0e3200dcd8706d67e7bb2c Mon Sep 17 00:00:00 2001 From: kevin Date: Thu, 10 Sep 2026 21:20:53 +0800 Subject: [PATCH 041/127] promote failure evidence in report interpretation --- harness/browser_use/report.py | 13 ++++++++++--- harness/browser_use/report_i18n.py | 15 +++++++++++++++ research/report-changes.json | 8 +++++++- scripts/check_browser_use_report.py | 2 +- 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/harness/browser_use/report.py b/harness/browser_use/report.py index 6ff00e8a2..f1aee60f3 100644 --- a/harness/browser_use/report.py +++ b/harness/browser_use/report.py @@ -1095,8 +1095,8 @@ def _article_body( ] return ( '
    Browser Use 实测解读
    ' - f"

    {chromium['passed']} 比 {moli['passed']} 不是“内核等价”:当前样本没有产生分歧

    " - f'

    同一 CowAgent 与同一模型替换浏览器后,{paired["paired"]} 个配对任务没有一个出现单边胜出。这个结果证明对照流程跑通,却没有为内核优劣提供方向性证据。

    ' + f"

    {chromium['passed']} 比 {moli['passed']} 的真正含义:五个共同失败点相同,根因尚未隔离

    " + f'

    同一 CowAgent 与同一模型替换浏览器后,{paired["paired"]} 个配对任务没有一个出现单边胜出。四个共同失败止于最终答案或状态协议,一个止于 30 步上限前的结账流程;现有证据可以定位失败点,但不能把隐藏原因单独归给浏览器、模型或 Agent。

    ' '

    测量口径:WebArena‑Verified 1.2.3;每题×浏览器一个最终逻辑结果;官方确定性 evaluator;无 LLM judge。

    ' "

    一、最重要的结果不是平分,而是零个不一致配对

    " f"

    Chromium 与 Moli 分别通过 {chromium['passed']}/{chromium['completed']} 和 {moli['passed']}/{moli['completed']}。逐题配对后,{paired['both_pass']} 题共同通过、{paired['both_fail']} 题共同失败,两个浏览器单边通过都是 0。由于固定样本里没有不一致任务,这批数据不能估计浏览器的方向性效应,也不对 WebArena‑Verified 全集作统计外推。

    " @@ -1111,7 +1111,7 @@ def _article_body( caption="表 1|30 个任务的逐题最终结果配对。", ) + f"

    严谨表述只能是“本轮没有观察到成功率差异”,不能写成“两种内核等价”。{paired['paired']} 题对小效应检出力有限;更关键的是,样本没有诱发内核分歧。

    " - "

    二、五个共同失败,具体失败在哪里

    " + '

    二、五个共同失败,具体失败在哪里

    ' f"

    两个浏览器共享 CowAgent、模型、提示与工具合同、站点状态、30 步上限和 evaluator,只有浏览器实现不同。逐条证据只能确认验收链路中的首个失败点;除 506 题明确触发步数上限外,记录不足以把错误进一步归因到模型、CowAgent 策略或提示合同中的某一项。

    " + _table( ["任务", "官方判定差异", "已确认失败点", "直接证据", "尚不能判定"], @@ -1465,6 +1465,13 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: "先看结论", f'
    Chromium 任务通过
    {c["passed"]}/{c["completed"]}

    {_pct(c["score"])}

    Moli 任务通过
    {m["passed"]}/{m["completed"]}

    {_pct(m["score"])}

    单边胜出
    {paired["chromium_only"] + paired["moli_only"]}

    {paired["paired"]} 个完整配对

    共同失败
    {paired["both_fail"]}

    已确认失败点

    当前结论:本轮没有观察到浏览器成功率差异;这证明对照流程可比,不证明两个内核等价。

    失败结论:四题的首个可证失败点位于最终答案构造或状态协议,一题在 30 步上限时仍未完成付款提交。现有证据不能把前四题进一步归因到模型、CowAgent 或提示合同中的某一项。

    这是固定选择的 30 题试验,不是从全部 WebArena‑Verified 任务随机抽样;比例只描述本样本,不外推总体能力。

    ', ) + home += _section( + "共同失败是报告解读的核心证据", + '

    25:25 只说明当前样本没有产生浏览器分歧。理解这个结果必须继续看五个共同失败发生在哪个验收点,而不是把平分直接解释成内核等价。

    ' + '
    最终答案数据
    3 题

    #68 漏记录;#124 纳入投影仪;#142 多计发箍

    最终状态协议
    1 题

    #168 将空结果编码为 SUCCESS

    任务完成边界
    1 题

    #506 达到 30 步时尚未提交付款

    ' + '

    归因边界:这些记录确认了失败发生的位置;除 #506 的步数上限外,没有控制变量实验能够继续区分模型判断、CowAgent 策略和提示/响应合同。

    ' + '

    查看五题逐条直接证据与归因边界 →

    ', + ) home += _section( "任务族覆盖", "

    样本同时包含信息提取、页面导航和状态变更任务;类别来自每题官方最终答案合同。

    " diff --git a/harness/browser_use/report_i18n.py b/harness/browser_use/report_i18n.py index 6e324a658..6906a96f5 100644 --- a/harness/browser_use/report_i18n.py +++ b/harness/browser_use/report_i18n.py @@ -405,6 +405,19 @@ "失败归因:": "Failure attribution:", "5 个共同失败中,四题指向共享 Agent/模型的语义、聚合或输出协议,一题指向共享的步数预算与长链完成;没有发现站点、官方 evaluator 或答案合同错误的证据。": "Among the five shared failures, four point to shared agent/model semantics, aggregation, or output protocol, and one points to the shared step budget and long-horizon completion. No evidence of site, official evaluator, or answer-contract errors was found.", "这是固定选择的 30 题试验,不是从全部 WebArena‑Verified 任务随机抽样;比例只描述本样本,不外推总体能力。": "This is a fixed 30-task experiment, not a random sample of all WebArena-Verified tasks. Percentages describe this sample only and are not population estimates.", + "共同失败是报告解读的核心证据": "Shared failures are core interpretation evidence", + "25:25 只说明当前样本没有产生浏览器分歧。理解这个结果必须继续看五个共同失败发生在哪个验收点,而不是把平分直接解释成内核等价。": "The 25:25 result means only that this sample produced no browser disagreement. Interpreting it requires locating the acceptance point of each shared failure instead of treating the tie as browser-core equivalence.", + "最终答案数据": "Final-answer data", + "3 题": "3 tasks", + "#68 漏记录;#124 纳入投影仪;#142 多计发箍": "Task 68 omitted a record; Task 124 included a projector; Task 142 added a headband.", + "最终状态协议": "Final-status protocol", + "1 题": "1 task", + "#168 将空结果编码为 SUCCESS": "Task 168 encoded an empty result as SUCCESS.", + "任务完成边界": "Task-completion boundary", + "#506 达到 30 步时尚未提交付款": "Task 506 had not submitted payment at step 30.", + "归因边界:": "Attribution boundary:", + "这些记录确认了失败发生的位置;除 #506 的步数上限外,没有控制变量实验能够继续区分模型判断、CowAgent 策略和提示/响应合同。": "These records establish where acceptance failed. Apart from Task 506's step limit, no controlled experiment in this run can distinguish model judgment, CowAgent policy, and prompt/response contracts.", + "查看五题逐条直接证据与归因边界 →": "View direct evidence and attribution boundaries for all five tasks →", "逻辑结果:": "Logical results:", "筛选并下钻两种浏览器结果。": "Filter tasks and inspect results from both browsers.", "60 个逻辑结果": "60 logical results", @@ -500,6 +513,8 @@ "为什么五个失败任务会完全相同": "Why the same five tasks failed in both browsers", "二、为什么五个失败任务会完全相同": "2. Why the same five tasks failed in both browsers", "二、五个共同失败,具体失败在哪里": "2. Where the five shared failures occurred", + "25 比 25 的真正含义:五个共同失败点相同,根因尚未隔离": "What 25 vs. 25 really means: the five failure points match, but the causes are not isolated", + "同一 CowAgent 与同一模型替换浏览器后,30 个配对任务没有一个出现单边胜出。四个共同失败止于最终答案或状态协议,一个止于 30 步上限前的结账流程;现有证据可以定位失败点,但不能把隐藏原因单独归给浏览器、模型或 Agent。": "With the same CowAgent and model and only the browser changed, none of the 30 paired tasks produced a one-browser-only win. Four shared failures ended at final-answer or status-protocol checks, and one ended in checkout before the 30-step limit. The evidence locates the failure points but cannot assign the hidden causes separately to the browser, model, or agent.", "五、下一轮先扩展四个已验证站点": "5. Expand the four verified sites first", "官方判定差异": "Official verdict mismatch", "直接证据": "Direct evidence", diff --git a/research/report-changes.json b/research/report-changes.json index ec4af33c2..ff6a566c7 100644 --- a/research/report-changes.json +++ b/research/report-changes.json @@ -68,10 +68,16 @@ "id": "2026-09-10-shared-failure-diagnosis-and-mac-expansion", "date": "2026-09-10", "title": "确认共同失败点并明确 Mac 扩展边界", - "summary": "报告逐题列出五个共同失败的可证失败点、直接证据与归因边界,结果详情合并重复的合同与 evaluator 展示,并明确 Colima 宿主网关、本地站点实跑范围及下一批可在 Mac 上预检的任务规模。官方成绩仍为 Chromium 25/30、Moli 25/30。", + "summary": "报告将五个共同失败提升为核心解读:逐题列出可证失败点、直接证据与归因边界,并在首页按答案数据、状态协议和任务完成边界汇总;同时明确 Colima 宿主网关、本地站点实跑范围及下一批可在 Mac 上预检的任务规模。官方成绩仍为 Chromium 25/30、Moli 25/30。", "attempts_sha256": "517b613adcc40fc0ebe41af2ab2115d76f49faaba5378b426cc196b5b633e9c7", "selection_sha256": "ba3c344df003dd4eba3a5d1ef97162ee98df2ed728f7c29c4188bc4247982d38", "changes": [ + [ + "报告解读", + "总分相同不足以解释结果,失败证据不应只作为详情页补充信息。", + "将共同失败提升为首页核心结论和解读文章主论点,并链接到五题逐条证据与归因边界。", + "改变结论层级,不改变官方分数、分母或冻结证据。" + ], [ "共同失败", "相同分数只被描述为无分歧,未定位五题的具体验收失败点。", diff --git a/scripts/check_browser_use_report.py b/scripts/check_browser_use_report.py index de4c9eef2..706e027e3 100644 --- a/scripts/check_browser_use_report.py +++ b/scripts/check_browser_use_report.py @@ -229,7 +229,7 @@ def check( standalone = (report_dir / "guide/article.html").read_text(encoding="utf-8") if " Date: Thu, 10 Sep 2026 21:21:54 +0800 Subject: [PATCH 042/127] bind interpretation-focused report version --- evaluation/browser-use/current-report.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evaluation/browser-use/current-report.json b/evaluation/browser-use/current-report.json index dac9641f7..888c2d0aa 100644 --- a/evaluation/browser-use/current-report.json +++ b/evaluation/browser-use/current-report.json @@ -32,7 +32,7 @@ "base_url": "https://artifact-hub.dev.lexmount.net", "site_slug": "28OH4YZkUYF1", "share_url": "https://artifact-hub.dev.lexmount.net/v/5Vhnhdc_f8FEM2RzctQm-ChRlXUf174Y", - "expected_version": "ver_5347ea126d1047fcbe7e8675792293af", + "expected_version": "ver_4e371a7693c142859cbd28b4354cbfb8", "state": "verified" } } From f233565d0a7727d2c5cecb71c3e415342304fcc6 Mon Sep 17 00:00:00 2001 From: kevin Date: Thu, 10 Sep 2026 22:06:11 +0800 Subject: [PATCH 043/127] fix browser use failure causes at source --- evaluation/browser-use/task-exclusions.json | 14 + harness/browser_use/har.py | 64 +++- harness/browser_use/report.py | 321 +++++++++++++------- harness/browser_use/report_i18n.py | 52 +++- harness/browser_use/runner.py | 90 ++++-- harness/browser_use/selection.py | 160 +++++++++- research/browser-use-failure-analysis.json | 56 ++-- research/report-changes.json | 30 +- scripts/check_browser_use_report.py | 2 +- scripts/publish_browser_use_artifact_hub.py | 2 +- tests/browser_use/test_har.py | 43 +++ tests/browser_use/test_runner.py | 28 ++ tests/browser_use/test_selection.py | 151 ++++++++- 13 files changed, 811 insertions(+), 202 deletions(-) create mode 100644 evaluation/browser-use/task-exclusions.json diff --git a/evaluation/browser-use/task-exclusions.json b/evaluation/browser-use/task-exclusions.json new file mode 100644 index 000000000..44dcfbb34 --- /dev/null +++ b/evaluation/browser-use/task-exclusions.json @@ -0,0 +1,14 @@ +{ + "schema": "lex_browser_use_task_exclusions/1", + "benchmark": "WebArena-Verified 1.2.3", + "tasks": { + "68": { + "reason": "official expected.retrieved_data violates the published results_schema", + "disposition": "exclude until the official schema is corrected or a benchmark patch is preregistered" + }, + "142": { + "reason": "category membership for 'hair care and hair style' has no observable acceptance rule", + "disposition": "exclude until category membership is bound to site taxonomy or another observable predicate" + } + } +} diff --git a/harness/browser_use/har.py b/harness/browser_use/har.py index 36d60adf5..c5b1b586d 100644 --- a/harness/browser_use/har.py +++ b/harness/browser_use/har.py @@ -70,6 +70,8 @@ def __init__(self) -> None: self._extra_headers: dict[tuple[int, str], list[dict[str, Any]]] = {} self._responses: list[tuple[Any, str, dict[str, Any], dict[str, Any]]] = [] self._finished: set[tuple[int, str]] = set() + self._response_bodies: dict[tuple[int, str], dict[str, Any]] = {} + self._response_body_errors: dict[tuple[int, str], str] = {} self._payload: str | None = None self._stopped = False self.errors: list[str] = [] @@ -97,8 +99,8 @@ def stop(self, context: Any, path: Path) -> None: ) } for index, (session, request_id, request, response) in enumerate(self._responses): - body = None key = (id(session), request_id) + body = self._response_bodies.get(key) request = dict(request) ordinal = int(request.pop("__ordinal", 0)) extras = self._extra_headers.get(key, []) @@ -108,20 +110,17 @@ def stop(self, context: Any, path: Path) -> None: if ( index == last_response_by_key[key] and key in self._finished + and body is None and (mime_type == "application/json" or mime_type.endswith("+json")) ): try: body = session.send("Network.getResponseBody", {"requestId": request_id}) - if body.get("base64Encoded"): - body = { - "body": base64.b64decode( - body.get("body") or "", validate=True - ).decode("utf-8", errors="replace"), - "base64Encoded": False, - } + body = self._decode_body(body) except Exception as exc: + prior = self._response_body_errors.get(key) + detail = f"{prior}; stop fallback: {exc}" if prior else str(exc) self.errors.append( - f"{request.get('url')}: response body unavailable: {exc}" + f"{request.get('url')}: response body unavailable: {detail}" ) body = None entries.append( @@ -167,7 +166,7 @@ def _attach(self, page: Any) -> None: ) session.on( "Network.loadingFinished", - lambda event: self._finished.add((session_index, str(event["requestId"]))), + lambda event: self._record_finished(session, session_index, event), ) session.send("Network.enable") self._sessions.append(session) @@ -193,3 +192,48 @@ def _record_response(self, session: Any, session_index: int, event: dict[str, An request = self._requests.get((session_index, request_id)) if request is not None: self._responses.append((session, request_id, dict(request), event["response"])) + + @staticmethod + def _decode_body(body: dict[str, Any]) -> dict[str, Any]: + if body.get("base64Encoded"): + return { + "body": base64.b64decode(body.get("body") or "", validate=True).decode( + "utf-8", errors="replace" + ), + "base64Encoded": False, + } + return body + + def _record_finished( + self, session: Any, session_index: int, event: dict[str, Any] + ) -> None: + """Capture JSON bodies while CDP still retains them. + + Waiting until the whole agent run ends is too late for long tasks: CDP may + evict an earlier response body even though the request metadata remains. + """ + + request_id = str(event["requestId"]) + key = (session_index, request_id) + self._finished.add(key) + response = next( + ( + item_response + for item_session, item_request_id, _request, item_response in reversed( + self._responses + ) + if id(item_session) == session_index and item_request_id == request_id + ), + None, + ) + mime_type = str((response or {}).get("mimeType") or "") + if mime_type != "application/json" and not mime_type.endswith("+json"): + return + try: + self._response_bodies[key] = self._decode_body( + session.send("Network.getResponseBody", {"requestId": request_id}) + ) + self._response_body_errors.pop(key, None) + except Exception as exc: + # stop() retries once before exposing a capture error. + self._response_body_errors[key] = str(exc) diff --git a/harness/browser_use/report.py b/harness/browser_use/report.py index f1aee60f3..4bc587176 100644 --- a/harness/browser_use/report.py +++ b/harness/browser_use/report.py @@ -55,6 +55,11 @@ MAC_COUNTS = MAC_EXPANSION["official_counts"] +def _finding_text(diagnosis: dict[str, Any], field: str) -> str: + value = diagnosis[field] + return str(value.get("zh") or value.get("en") or "") if isinstance(value, dict) else str(value) + + def _configure_engine_labels(profile: dict[str, Any]) -> None: engines = [str(engine) for engine in profile.get("engines") or []] labels = profile.get("engine_labels") or {} @@ -66,6 +71,8 @@ def _configure_engine_labels(profile: dict[str, Any]) -> None: ENGINE_LABELS = { engine: str(labels.get(engine) or engine.replace("_", " ").title()) for engine in engines } + + METHODOLOGY_FIGURE = Path(__file__).parent / "assets" / "webarena-autonomous-evaluation-flow.png" @@ -137,7 +144,7 @@ def _metric_stack(items: list[tuple[str, str, bool]]) -> Safe: lines = [] for label, value, won in items: rendered = _winner(value, won=won) - lines.append(f'{_e(label)}{rendered}') + lines.append(f"{_e(label)}{rendered}") return Safe('' + "".join(lines) + "") @@ -217,9 +224,7 @@ def _logical_results( raise ValueError(f"more than two executions for {key}") legacy_primary = (protocol or {}).get("official_score_uses") == "primary attempt only" authoritative = dict(executions[0] if legacy_primary else executions[-1]) - authoritative["duration_ms"] = sum( - float(row.get("duration_ms") or 0) for row in executions - ) + authoritative["duration_ms"] = sum(float(row.get("duration_ms") or 0) for row in executions) usage_keys = ( "input_tokens", "output_tokens", @@ -248,11 +253,14 @@ def _logical_results( } weighted_duration = sum(float(row.get("duration_ms") or 0) for row in executions) if weighted_duration: - authoritative["resource"]["cpu_average_utilization_pct"] = sum( - float((row.get("resource") or {}).get("cpu_average_utilization_pct") or 0) - * float(row.get("duration_ms") or 0) - for row in executions - ) / weighted_duration + authoritative["resource"]["cpu_average_utilization_pct"] = ( + sum( + float((row.get("resource") or {}).get("cpu_average_utilization_pct") or 0) + * float(row.get("duration_ms") or 0) + for row in executions + ) + / weighted_duration + ) tool_metrics = [_execution_tool_metrics(row) for row in executions] authoritative["action_count"] = sum(item["action_count"] for item in tool_metrics) authoritative["browser_tool_duration_ms"] = round( @@ -287,10 +295,7 @@ def _public_attempt(row: dict[str, Any]) -> dict[str, Any]: "duration_ms": round(float(row.get("duration_ms") or 0), 3), "action_count": int(row.get("action_count") or 0), "browser_tool_duration_ms": round(float(row.get("browser_tool_duration_ms") or 0), 3), - "browser_tool_duration_by_action_ms": row.get( - "browser_tool_duration_by_action_ms" - ) - or {}, + "browser_tool_duration_by_action_ms": row.get("browser_tool_duration_by_action_ms") or {}, "browser_tool_count_by_action": row.get("browser_tool_count_by_action") or {}, "model_usage": { key: int(usage.get(key) or 0) @@ -345,7 +350,8 @@ def _primary_summary(rows: list[dict[str, Any]]) -> dict[str, dict[str, Any]]: "p50_duration_ms": _percentile(durations, 0.50), "p95_duration_ms": _percentile(durations, 0.95), "mean_browser_tool_duration_ms": statistics.fmean(tool_durations) - if tool_durations else None, + if tool_durations + else None, "p50_browser_tool_duration_ms": _percentile(tool_durations, 0.50), "p95_browser_tool_duration_ms": _percentile(tool_durations, 0.95), "mean_tokens": statistics.fmean(tokens) if tokens else None, @@ -356,10 +362,7 @@ def _primary_summary(rows: list[dict[str, Any]]) -> dict[str, dict[str, Any]]: def _paired_summary(rows: list[dict[str, Any]]) -> dict[str, Any]: - primary = { - (str(r["engine"]), int(r["task_id"])): float(r["score"]) == 1 - for r in rows - } + primary = {(str(r["engine"]), int(r["task_id"])): float(r["score"]) == 1 for r in rows} ids = sorted( task_id for task_id in {key[1] for key in primary} @@ -403,7 +406,7 @@ def _resolve_tasks( "installed WebArena-Verified data checksum differs from measured evidence: " f"{actual_data_checksum} != {expected_data_checksum}" ) - tasks = validate_against_official(selection, benchmark) + tasks = validate_against_official(selection, benchmark, enforce_contract_integrity=False) official = { int(task["task_id"]): benchmark.get_task(int(task["task_id"])) for task in tasks } @@ -456,8 +459,7 @@ def _task_family_summary( tasks: list[dict[str, Any]], attempts: list[dict[str, Any]] ) -> list[dict[str, Any]]: primary = { - (str(row["engine"]), int(row["task_id"])): float(row["score"]) == 1 - for row in attempts + (str(row["engine"]), int(row["task_id"])): float(row["score"]) == 1 for row in attempts } result = [] for kind in ("RETRIEVE", "NAVIGATE", "MUTATE"): @@ -519,11 +521,7 @@ def _same_task_comparison(tasks: list[dict[str, Any]], attempts: list[dict[str, ordered = sorted( ENGINE_LABELS, key=lambda engine: ( - -sum( - float(row["score"]) == 1 - for row in attempts - if row["engine"] == engine - ), + -sum(float(row["score"]) == 1 for row in attempts if row["engine"] == engine), ENGINE_LABELS[engine], ), ) @@ -540,11 +538,15 @@ def _same_task_comparison(tasks: list[dict[str, Any]], attempts: list[dict[str, def outcome_links(engines: list[str], state: str) -> str: if not engines: return "—" - return '' + "".join( - f'' - f'{_e(ENGINE_LABELS[engine])}' - for engine in engines - ) + "" + return ( + '' + + "".join( + f'' + f"{_e(ENGINE_LABELS[engine])}" + for engine in engines + ) + + "" + ) site = str(task["sites"][0]) attrs = ( @@ -559,18 +561,18 @@ def outcome_links(engines: list[str], state: str) -> str: f'{_e(task["task_type"])}' ) rows.append( - f'{task_cell}{outcome_links(passed, "pass")}' - f'{outcome_links(failed, "fail")}' + f"{task_cell}{outcome_links(passed, 'pass')}" + f"{outcome_links(failed, 'fail')}" ) options = "".join( f'' + f"{_e(ENGINE_LABELS[engine])}" for engine in ordered ) checks = "".join( f'' + f"{' checked' if engine == ordered[1] else ''}> {_e(ENGINE_LABELS[engine])}" for engine in ordered ) kinds = '' + "".join( @@ -578,7 +580,7 @@ def outcome_links(engines: list[str], state: str) -> str: ) return ( '
    ' - '

    同题结果组合

    选择一个基准浏览器和一个或多个对比浏览器;浏览器标签可直达该题完整结果。

    ' + "

    同题结果组合

    选择一个基准浏览器和一个或多个对比浏览器;浏览器标签可直达该题完整结果。

    " '
    ' '
    ' f'' @@ -590,7 +592,7 @@ def outcome_links(engines: list[str], state: str) -> str: '
    对比浏览器(可多选)' f'
    {checks}
    ' '
    ' - '' + "" f'{"".join(rows)}
    任务通过浏览器失败浏览器
    任务通过浏览器失败浏览器
    ' ) @@ -614,11 +616,13 @@ def _group_rows( winners = _best(passed_by_engine, higher=True) for engine in ENGINE_LABELS: items = grouped[(value, engine)] - line.append(_winner( - f"{passed_by_engine[engine]}/{len(items)}" if items else "—", - won=engine in winners, - label="同组通过数更高", - )) + line.append( + _winner( + f"{passed_by_engine[engine]}/{len(items)}" if items else "—", + won=engine in winners, + label="同组通过数更高", + ) + ) result.append(line) return result @@ -659,20 +663,22 @@ def _action_duration_cards(attempts: list[dict[str, Any]]) -> str: average_text = "—" if average is None else f"{average / 1000:.3f} 秒" browser_rows.append( f'
    ' - f'{_e(ENGINE_LABELS[engine])}{_winner(average_text, won=engine in average_winners, label="单次均值更低")}' + f"{_e(ENGINE_LABELS[engine])}{_winner(average_text, won=engine in average_winners, label='单次均值更低')}" f'
    ' f'
    总耗时 {total / 1000:.1f} 秒 · {count} 次调用
    ' ) cards.append( - '
    ' + _e(action) + '' - '单次调用墙钟时间
    ' + "".join(browser_rows) + "
    " + '
    ' + _e(action) + "" + "单次调用墙钟时间
    " + "".join(browser_rows) + "
    " ) return '
    ' + "".join(cards) + "
    " def _summary_comparison_rows(summary: dict[str, dict[str, Any]]) -> list[list[Any]]: higher_metrics = { - "passed": _best({engine: summary[engine]["passed"] for engine in ENGINE_LABELS}, higher=True), + "passed": _best( + {engine: summary[engine]["passed"] for engine in ENGINE_LABELS}, higher=True + ), "score": _best({engine: summary[engine]["score"] for engine in ENGINE_LABELS}, higher=True), } lower_names = ( @@ -705,7 +711,7 @@ def _summary_comparison_rows(summary: dict[str, dict[str, Any]]) -> list[list[An [ label, _winner( - f'{current["passed"]}/{current["completed"]}', + f"{current['passed']}/{current['completed']}", won=engine in higher_metrics["passed"], label="通过数更高", ), @@ -716,23 +722,59 @@ def _summary_comparison_rows(summary: dict[str, dict[str, Any]]) -> list[list[An ), _metric_stack( [ - ("均值", _seconds(current["mean_duration_ms"]), engine in lower_metrics["mean_duration_ms"]), - ("P50", _seconds(current["p50_duration_ms"]), engine in lower_metrics["p50_duration_ms"]), - ("P95", _seconds(current["p95_duration_ms"]), engine in lower_metrics["p95_duration_ms"]), + ( + "均值", + _seconds(current["mean_duration_ms"]), + engine in lower_metrics["mean_duration_ms"], + ), + ( + "P50", + _seconds(current["p50_duration_ms"]), + engine in lower_metrics["p50_duration_ms"], + ), + ( + "P95", + _seconds(current["p95_duration_ms"]), + engine in lower_metrics["p95_duration_ms"], + ), ] ), _metric_stack( [ - ("均值", _seconds(current["mean_browser_tool_duration_ms"]), engine in lower_metrics["mean_browser_tool_duration_ms"]), - ("P50", _seconds(current["p50_browser_tool_duration_ms"]), engine in lower_metrics["p50_browser_tool_duration_ms"]), - ("P95", _seconds(current["p95_browser_tool_duration_ms"]), engine in lower_metrics["p95_browser_tool_duration_ms"]), + ( + "均值", + _seconds(current["mean_browser_tool_duration_ms"]), + engine in lower_metrics["mean_browser_tool_duration_ms"], + ), + ( + "P50", + _seconds(current["p50_browser_tool_duration_ms"]), + engine in lower_metrics["p50_browser_tool_duration_ms"], + ), + ( + "P95", + _seconds(current["p95_browser_tool_duration_ms"]), + engine in lower_metrics["p95_browser_tool_duration_ms"], + ), ] ), _metric_stack( [ - ("均值", _number(current["mean_tokens"]), engine in lower_metrics["mean_tokens"]), - ("P50", _number(current["p50_tokens"]), engine in lower_metrics["p50_tokens"]), - ("P95", _number(current["p95_tokens"]), engine in lower_metrics["p95_tokens"]), + ( + "均值", + _number(current["mean_tokens"]), + engine in lower_metrics["mean_tokens"], + ), + ( + "P50", + _number(current["p50_tokens"]), + engine in lower_metrics["p50_tokens"], + ), + ( + "P95", + _number(current["p95_tokens"]), + engine in lower_metrics["p95_tokens"], + ), ] ), ] @@ -953,7 +995,7 @@ def _failed_rule_labels(result: dict[str, Any] | None) -> list[str]: def _failure_brief(result: dict[str, Any] | None, row: dict[str, Any]) -> str: labels = _failed_rule_labels(result) if labels: - return f'{len(labels)} 项规则未通过:{"、".join(labels)}。' + return f"{len(labels)} 项规则未通过:{'、'.join(labels)}。" if row.get("error"): return "执行未完成;错误信息见运行与证据。" return "全部官方 evaluator 均通过。" @@ -996,9 +1038,7 @@ def _rule_outcome_summary(item: dict[str, Any]) -> str: return message or "该规则未通过。" -def _verification_html( - task: dict[str, Any], result: dict[str, Any] | None, response: Any -) -> str: +def _verification_html(task: dict[str, Any], result: dict[str, Any] | None, response: Any) -> str: items = evaluator_summary(result) if not items: return '

    没有 evaluator 明细。

    ' @@ -1028,7 +1068,7 @@ def _verification_html( + _json_pre(observed) + "
    " + ( - f'
    查看 evaluator 技术明细{_json_pre(technical)}
    ' + f"
    查看 evaluator 技术明细{_json_pre(technical)}
    " if technical else "" ) @@ -1037,7 +1077,7 @@ def _verification_html( return ( '
    任务目标

    ' + _e(task.get("intent") or "—") - + '

    Agent 可自主选择 UI 路径;计分只看以下最终回答与必要网络副作用。
    ' + + "

    Agent 可自主选择 UI 路径;计分只看以下最终回答与必要网络副作用。" + '
    ' + "".join(cards) + "
    " @@ -1086,17 +1126,18 @@ def _article_body( failures = [ [ f"#{task_id}", - diagnosis["mismatch"], - diagnosis["confirmed_failure_point"], - diagnosis["direct_evidence"], - diagnosis["root_cause_boundary"], + _finding_text(diagnosis, "root_cause"), + _finding_text(diagnosis, "owner"), + _finding_text(diagnosis, "proof"), + _finding_text(diagnosis, "corrective_action"), + _finding_text(diagnosis, "verification"), ] for task_id, diagnosis in SHARED_FAILURE_DIAGNOSES.items() ] return ( '
    Browser Use 实测解读
    ' - f"

    {chromium['passed']} 比 {moli['passed']} 的真正含义:五个共同失败点相同,根因尚未隔离

    " - f'

    同一 CowAgent 与同一模型替换浏览器后,{paired["paired"]} 个配对任务没有一个出现单边胜出。四个共同失败止于最终答案或状态协议,一个止于 30 步上限前的结账流程;现有证据可以定位失败点,但不能把隐藏原因单独归给浏览器、模型或 Agent。

    ' + f"

    {chromium['passed']} 比 {moli['passed']} 不是同因平局:五个零分包含四类根因

    " + f'

    同一 CowAgent 与同一模型替换浏览器后,{paired["paired"]} 个配对任务的官方分数没有出现单边胜出,但逐条追查证明相同分数并不代表相同执行结果。根因分别位于官方任务合同、Agent 完成策略、本项目响应协议和证据采集器;Task 506 的 Chromium 与 Moli 甚至不是同一种失败。

    ' '

    测量口径:WebArena‑Verified 1.2.3;每题×浏览器一个最终逻辑结果;官方确定性 evaluator;无 LLM judge。

    ' "

    一、最重要的结果不是平分,而是零个不一致配对

    " f"

    Chromium 与 Moli 分别通过 {chromium['passed']}/{chromium['completed']} 和 {moli['passed']}/{moli['completed']}。逐题配对后,{paired['both_pass']} 题共同通过、{paired['both_fail']} 题共同失败,两个浏览器单边通过都是 0。由于固定样本里没有不一致任务,这批数据不能估计浏览器的方向性效应,也不对 WebArena‑Verified 全集作统计外推。

    " @@ -1111,14 +1152,14 @@ def _article_body( caption="表 1|30 个任务的逐题最终结果配对。", ) + f"

    严谨表述只能是“本轮没有观察到成功率差异”,不能写成“两种内核等价”。{paired['paired']} 题对小效应检出力有限;更关键的是,样本没有诱发内核分歧。

    " - '

    二、五个共同失败,具体失败在哪里

    ' - f"

    两个浏览器共享 CowAgent、模型、提示与工具合同、站点状态、30 步上限和 evaluator,只有浏览器实现不同。逐条证据只能确认验收链路中的首个失败点;除 506 题明确触发步数上限外,记录不足以把错误进一步归因到模型、CowAgent 策略或提示合同中的某一项。

    " + '

    二、五个零分的根因与修复边界

    ' + f"

    两个浏览器共享 CowAgent、模型、任务合同与 evaluator,但这不意味着共同零分必然来自共同原因。根因审计把“页面上出现了什么”继续追到产生错误的合同、策略或采集实现,并为每项给出必须通过的复测条件。

    " + _table( - ["任务", "官方判定差异", "已确认失败点", "直接证据", "尚不能判定"], + ["任务", "根因", "责任层", "证据闭环", "根治措施", "复测门槛"], failures, - caption="表 2|只陈述最终回答、官方 evaluator 与执行轨迹能够直接证明的事实;不修改官方分数。", + caption="表 2|历史官方分数不被人工改写;合同或运行协议修复后必须产生新证据。", ) - + "

    结论:#68、#124、#142 和 #168 的确定事实是最终答案构造或状态协议错误;#506 的确定事实是达到 30 步上限时尚未产生付款提交和成功页事件。站点错误、evaluator 实现错误、答案合同错误以及模型内部决策原因,在这批证据中均未被证明。因而本轮不能单独判定为“模型能力不行”,只能判定当前 CowAgent、模型、提示与工具合同的组合在这些验收点失败。

    三、简单、中等、困难都覆盖了,但“难度”只是操作长度

    WebArena‑Verified 没有官方 easy/medium/hard 三档。本项目在运行前按最少可见 UI 操作数注册为 1–4、5–7、8+ 三档,配额 5/10/15。它控制操作链覆盖,不等同于推理难度。

    " + + "

    可执行结论:#68 因官方 expected 与 schema 冲突而退出下一轮干净 A/B;#142 因类别验收边界未定义而退出,直至补齐可观察口径;#168 修正本项目的空结果状态协议;#124 增加候选语义核验和提交前自检;#506 修复 HAR 即时采集、末步工具调用 fail-closed,并在预注册的 40 步上限下复测。旧的 25/30 仍保留为历史官方分数,但不能再解释成五个同因的 Agent 失败。

    三、简单、中等、困难都覆盖了,但“难度”只是操作长度

    WebArena‑Verified 没有官方 easy/medium/hard 三档。本项目在运行前按最少可见 UI 操作数注册为 1–4、5–7、8+ 三档,配额 5/10/15。它控制操作链覆盖,不等同于推理难度。

    " + _table( ["本项目分层", "Chromium", "Moli"], _group_rows(attempts, tasks, "tier"), @@ -1148,11 +1189,15 @@ def _filters(*, attempts: bool = False, dataset: bool = False) -> str: if attempts else "" ) - result_filter = "" if dataset else ( - '" + result_filter = ( + "" + if dataset + else ( + '" + ) ) placeholder = "任务、意图、验收合同" if dataset else "任务、意图、答案、错误" return ( @@ -1298,7 +1343,16 @@ def _engine_attempts( ] ) return _table( - ["结果", "任务", "层级", "判定", "Agent 链路耗时", "BrowserTool 耗时", "动作数", "失败原因"], + [ + "结果", + "任务", + "层级", + "判定", + "Agent 链路耗时", + "BrowserTool 耗时", + "动作数", + "失败原因", + ], rows, table_id="engine-result-table", ) @@ -1312,7 +1366,7 @@ def _write_json(path: Path, value: Any) -> None: def _release_details(release: dict[str, Any]) -> str: rows = release.get("changes") or [] details = ( - '
    查看修正依据、处理与影响' + "
    查看修正依据、处理与影响" + _table( ["项目", "发现的问题", "正式处理", "结果影响与边界"], rows, @@ -1324,8 +1378,8 @@ def _release_details(release: dict[str, Any]) -> str: ) return ( f'
    {_e(release.get("date") or "未记录")}
    ' - f'

    {_e(release.get("title") or "未命名版本")}

    ' - f'

    {_e(release.get("summary") or "")}

    {details}' + f"

    {_e(release.get('title') or '未命名版本')}

    " + f"

    {_e(release.get('summary') or '')}

    {details}" ) @@ -1363,7 +1417,7 @@ def _review_history(rounds: list[dict[str, Any]]) -> str: cards = [] for item in rounds: fields = "".join( - f'
    {_e(label)}

    {_e(item.get(key) or "—")}

    ' + f"
    {_e(label)}

    {_e(item.get(key) or '—')}

    " for label, key in ( ("核验范围", "scope"), ("发现", "findings"), @@ -1374,7 +1428,7 @@ def _review_history(rounds: list[dict[str, Any]]) -> str: cards.append( f'
    ' f'
    {_e(item.get("date") or "未记录")}
    ' - f'

    {_e(item.get("title") or "未命名核验")}

    ' + f"

    {_e(item.get('title') or '未命名核验')}

    " f'{_e(item.get("status") or "未记录")}' f'
    {fields}
    ' ) @@ -1463,14 +1517,14 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) home += _section( "先看结论", - f'
    Chromium 任务通过
    {c["passed"]}/{c["completed"]}

    {_pct(c["score"])}

    Moli 任务通过
    {m["passed"]}/{m["completed"]}

    {_pct(m["score"])}

    单边胜出
    {paired["chromium_only"] + paired["moli_only"]}

    {paired["paired"]} 个完整配对

    共同失败
    {paired["both_fail"]}

    已确认失败点

    当前结论:本轮没有观察到浏览器成功率差异;这证明对照流程可比,不证明两个内核等价。

    失败结论:四题的首个可证失败点位于最终答案构造或状态协议,一题在 30 步上限时仍未完成付款提交。现有证据不能把前四题进一步归因到模型、CowAgent 或提示合同中的某一项。

    这是固定选择的 30 题试验,不是从全部 WebArena‑Verified 任务随机抽样;比例只描述本样本,不外推总体能力。

    ', + f'
    Chromium 任务通过
    {c["passed"]}/{c["completed"]}

    {_pct(c["score"])}

    Moli 任务通过
    {m["passed"]}/{m["completed"]}

    {_pct(m["score"])}

    单边胜出
    {paired["chromium_only"] + paired["moli_only"]}

    {paired["paired"]} 个完整配对

    共同零分
    {paired["both_fail"]}

    不等于共同根因

    当前结论:本轮没有观察到浏览器成功率差异;这证明对照流程可比,不证明两个内核等价。

    根因结论:#68 是官方任务合同冲突,#124 是候选语义核验缺失,#142 是类别验收边界未定义,#168 是本项目响应协议错误;#506 的 Chromium 是 HAR 采集误判,Moli 是执行步数与完成确认问题。

    这是固定选择的 30 题试验,不是从全部 WebArena‑Verified 任务随机抽样;比例只描述本样本,不外推总体能力。

    ', ) home += _section( "共同失败是报告解读的核心证据", - '

    25:25 只说明当前样本没有产生浏览器分歧。理解这个结果必须继续看五个共同失败发生在哪个验收点,而不是把平分直接解释成内核等价。

    ' - '
    最终答案数据
    3 题

    #68 漏记录;#124 纳入投影仪;#142 多计发箍

    最终状态协议
    1 题

    #168 将空结果编码为 SUCCESS

    任务完成边界
    1 题

    #506 达到 30 步时尚未提交付款

    ' - '

    归因边界:这些记录确认了失败发生的位置;除 #506 的步数上限外,没有控制变量实验能够继续区分模型判断、CowAgent 策略和提示/响应合同。

    ' - '

    查看五题逐条直接证据与归因边界 →

    ', + "

    25:25 只说明当前样本没有产生浏览器分歧。理解这个结果必须继续看五个共同失败发生在哪个验收点,而不是把平分直接解释成内核等价。

    " + '
    任务合同问题
    2 题

    #68 schema 冲突;#142 类别口径缺失

    Agent 协议与策略
    2 题

    #124 未核验候选;#168 空结果状态错误

    不同根因的同分
    1 题

    #506:Chromium 采集误判;Moli 未确认完成

    ' + '

    处理原则:合同不稳定的任务退出下一轮样本;代码与协议缺陷修复后只通过新执行验证,不覆盖历史官方分数。

    ' + '

    查看五题根因、证据闭环与复测门槛 →

    ', ) home += _section( "任务族覆盖", @@ -1653,7 +1707,13 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ["站点", "本轮访问地址", "应用端口", "环境控制端口", "状态"], [ ["Shopping", "http://192.168.5.2:7770", "7770", "7771", "本轮已部署"], - ["Shopping Admin", "http://192.168.5.2:7780/admin", "7780", "7781", "本轮已部署"], + [ + "Shopping Admin", + "http://192.168.5.2:7780/admin", + "7780", + "7781", + "本轮已部署", + ], ["Reddit", "http://192.168.5.2:9999", "9999", "9998", "本轮已部署"], ["GitLab", "http://192.168.5.2:8023", "8023", "8024", "本轮已部署"], ["Wikipedia", "—", "8888", "8889", "未部署;不在 30 题"], @@ -1665,7 +1725,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) methodology += _section( "Mac 上可以继续扩展哪些实例", - f'

    官方完整集有 {MAC_COUNTS["full"]} 题,hard 子集有 {MAC_COUNTS["hard"]} 题;官方没有 179 题版本。若运行 179 题,必须把它标注为本项目自定义样本并冻结任务清单。当前四个已实跑站点可直接进入下一轮预检:覆盖 {MAC_COUNTS["current_four_sites_single_site"]} 个单站任务,计入只依赖这四站的跨站任务后为 {MAC_COUNTS["current_four_sites_including_cross_site"]} 个候选;hard 子集中可覆盖 {MAC_COUNTS["current_four_sites_hard_including_cross_site"]} 个。

    ' + f"

    官方完整集有 {MAC_COUNTS['full']} 题,hard 子集有 {MAC_COUNTS['hard']} 题;官方没有 179 题版本。若运行 179 题,必须把它标注为本项目自定义样本并冻结任务清单。当前四个已实跑站点可直接进入下一轮预检:覆盖 {MAC_COUNTS['current_four_sites_single_site']} 个单站任务,计入只依赖这四站的跨站任务后为 {MAC_COUNTS['current_four_sites_including_cross_site']} 个候选;hard 子集中可覆盖 {MAC_COUNTS['current_four_sites_hard_including_cross_site']} 个。

    " + _table( ["环境", "Apple Silicon Mac 状态", "完整集单站题", "hard 单站题", "约束"], MAC_ENVIRONMENT_CANDIDATES, @@ -1682,7 +1742,10 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ["Agent / 模型", f"CowAgent;{model};reasoning=low"], ["唯一变量", "同题、同数据、同模型、同提示和资源上限,只替换浏览器"], ["结果口径", "每题×浏览器一个最终逻辑结果;二元通过率;逐题配对"], - ["执行上限", "代码最多允许两次执行;本报告按冻结运行协议选取正式结果,实际执行耗时和模型用量累计"], + [ + "执行上限", + "代码最多允许两次执行;本报告按冻结运行协议选取正式结果,实际执行耗时和模型用量累计", + ], [ "排除", "不纳入无法稳定验收的实时任务;本轮排除需大型数据卷的 Wikipedia / Map", @@ -1764,9 +1827,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: for x in review.get("attempts") or [] } for row, public in zip(attempts, public_attempts, strict=True): - item = review_by_key.get( - (public["engine"], public["task_id"], int(row["attempt"])), {} - ) + item = review_by_key.get((public["engine"], public["task_id"], int(row["attempt"])), {}) task = next(t for t in tasks if int(t["task_id"]) == public["task_id"]) paths = row.get("paths") or {} response_record = read_public_json(Path(paths.get("response", ""))) @@ -1820,10 +1881,14 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: "任务要求与验收结果", _verification_html(task, evaluation_record, response_record) + ( - '
    已确认失败点:' - + _e(SHARED_FAILURE_DIAGNOSES[public["task_id"]]["confirmed_failure_point"]) - + " 直接证据:" - + _e(SHARED_FAILURE_DIAGNOSES[public["task_id"]]["direct_evidence"]) + '
    根因:' + + _e(_finding_text(SHARED_FAILURE_DIAGNOSES[public["task_id"]], "root_cause")) + + " 根治措施:" + + _e( + _finding_text( + SHARED_FAILURE_DIAGNOSES[public["task_id"]], "corrective_action" + ) + ) + "
    " if public["task_id"] in SHARED_FAILURE_DIAGNOSES else "" @@ -1874,7 +1939,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: review_page += _section( "当前状态", '

    发布门禁:证据文件、哈希、分数、状态、官方版本与报告链接已通过程序化一致性检查。

    ' - '

    官方 WebArena‑Verified evaluator 是成绩来源;核验记录只说明报告和实现经过了哪些检查与修订,不替代任务判分。

    ', + "

    官方 WebArena‑Verified evaluator 是成绩来源;核验记录只说明报告和实现经过了哪些检查与修订,不替代任务判分。

    ", ) review_page += _section( "核验轮次", @@ -1901,23 +1966,35 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: (participant_versions.get(engine) or {}).get("Browser", "—"), (participant_images.get(engine) or {}).get("id", "—"), "独立容器 · CDP · CowAgent BrowserTool", - f'{summary[engine]["passed"]}/{summary[engine]["completed"]}', + f"{summary[engine]['passed']}/{summary[engine]['completed']}", ] for engine, label in ENGINE_LABELS.items() ], ) - + '

    参评浏览器是本轮唯一实验变量;完整 Agent 栈的其余组成列为统一实验条件。

    ', + + "

    参评浏览器是本轮唯一实验变量;完整 Agent 栈的其余组成列为统一实验条件。

    ", ) adoption += _section( "共同实验条件", _table( ["栈字段", "本轮固定值", "角色"], [ - ["Agent", f'CowAgent · commit {protocol.get("cowagent_commit", "—")}', "自主观察、规划并调用 BrowserTool"], - ["模型", f'{model} · reasoning={protocol.get("reasoning_effort", "—")}', "为两个选手生成决策"], + [ + "Agent", + f"CowAgent · commit {protocol.get('cowagent_commit', '—')}", + "自主观察、规划并调用 BrowserTool", + ], + [ + "模型", + f"{model} · reasoning={protocol.get('reasoning_effort', '—')}", + "为两个选手生成决策", + ], ["任务与站点", "同一 30 题;同一批本地站点镜像", "提供一致输入与网页状态"], ["动作接口", "CowAgent BrowserTool · CDP", "把 Agent 动作交给浏览器"], - ["验收", "WebArena‑Verified 1.2.3 官方 evaluator", "对最终 JSON 与必要网络副作用判分"], + [ + "验收", + "WebArena‑Verified 1.2.3 官方 evaluator", + "对最终 JSON 与必要网络副作用判分", + ], ["资源与上限", "相同 CPU、内存、viewport 与最多 30 步", "控制运行预算"], ], ) @@ -2003,16 +2080,28 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: site_images = environment.get("site_images") or {} reproduce += _section( "部署环境与网络拓扑", - '

    本轮 Docker daemon 运行在 macOS 上的 Colima 虚拟机中。站点容器把应用端口发布到 Docker 宿主;两个浏览器容器通过其容器内 host.docker.internal 对应的网关地址访问这些端口。运行时记录的地址是 ' + "

    本轮 Docker daemon 运行在 macOS 上的 Colima 虚拟机中。站点容器把应用端口发布到 Docker 宿主;两个浏览器容器通过其容器内 host.docker.internal 对应的网关地址访问这些端口。运行时记录的地址是 " + _e(environment.get("site_access_host") or "未记录") - + '。它属于本轮私有网络,不应复制为其他机器的固定地址。

    ' + + "。它属于本轮私有网络,不应复制为其他机器的固定地址。

    " + _table( ["层", "本轮身份", "作用"], [ ["宿主", environment.get("platform", "—"), "运行 Colima 与报告 runner"], - ["Docker", f'{environment.get("docker_name", "—")} · {environment.get("docker_architecture", "—")} · {environment.get("docker_server_version", "—")}', "承载站点和浏览器容器"], - ["浏览器到宿主网关", environment.get("site_access_host", "—"), "访问站点容器发布端口"], - ["ARM 兼容模式", (environment.get("site_runtime_compatibility") or {}).get("mode", "未启用"), "运行部分 amd64 PHP 站点镜像"], + [ + "Docker", + f"{environment.get('docker_name', '—')} · {environment.get('docker_architecture', '—')} · {environment.get('docker_server_version', '—')}", + "承载站点和浏览器容器", + ], + [ + "浏览器到宿主网关", + environment.get("site_access_host", "—"), + "访问站点容器发布端口", + ], + [ + "ARM 兼容模式", + (environment.get("site_runtime_compatibility") or {}).get("mode", "未启用"), + "运行部分 amd64 PHP 站点镜像", + ], ], ) + _table( @@ -2144,12 +2233,12 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: "sites", ) } - | { - "max_task_executions": 2, - "environment_reset": "runner configured for reset before each execution; legacy evidence has no independent per-execution preparation receipt", - "score_uses": "frozen run manifest; report does not override measured score", - "cost_and_latency": "sum of all executions used for the logical result", - }, + | { + "max_task_executions": 2, + "environment_reset": "runner configured for reset before each execution; legacy evidence has no independent per-execution preparation receipt", + "score_uses": "frozen run manifest; report does not override measured score", + "cost_and_latency": "sum of all executions used for the logical result", + }, "environment": { "platform": environment.get("platform"), "docker_name": environment.get("docker_name"), diff --git a/harness/browser_use/report_i18n.py b/harness/browser_use/report_i18n.py index 6906a96f5..8c9f49cae 100644 --- a/harness/browser_use/report_i18n.py +++ b/harness/browser_use/report_i18n.py @@ -3,6 +3,7 @@ from __future__ import annotations import html +import json import posixpath import re import shutil @@ -27,6 +28,34 @@ TEXT = { + "25 比 25 不是同因平局:五个零分包含四类根因": "25 vs. 25 is not a same-cause tie: five zero scores contain four root-cause classes", + "同一 CowAgent 与同一模型替换浏览器后,30 个配对任务的官方分数没有出现单边胜出,但逐条追查证明相同分数并不代表相同执行结果。根因分别位于官方任务合同、Agent 完成策略、本项目响应协议和证据采集器;Task 506 的 Chromium 与 Moli 甚至不是同一种失败。": "With the same CowAgent and model and only the browser changed, none of the 30 paired tasks produced a one-sided official-score win. Case-by-case investigation shows that equal scores do not imply equal execution outcomes. Root causes lie in the official task contract, Agent completion policy, this project's response protocol, and the evidence collector; Chromium and Moli did not even fail Task 506 in the same way.", + "二、五个零分的根因与修复边界": "2. Root causes and repair boundaries for the five zero scores", + "两个浏览器共享 CowAgent、模型、任务合同与 evaluator,但这不意味着共同零分必然来自共同原因。根因审计把“页面上出现了什么”继续追到产生错误的合同、策略或采集实现,并为每项给出必须通过的复测条件。": "The two browsers share CowAgent, the model, task contracts, and evaluator, but shared zero scores do not necessarily share a cause. The root-cause audit traces visible symptoms to the contract, policy, or collection implementation that produced them and defines a required retest gate for each.", + "根因": "Root cause", + "责任层": "Owning layer", + "证据闭环": "Evidence chain", + "根治措施": "Corrective action", + "复测门槛": "Retest gate", + "表 2|历史官方分数不被人工改写;合同或运行协议修复后必须产生新证据。": "Table 2 | Historical official scores are not manually changed; contract or runtime-protocol fixes require new evidence.", + "可执行结论:": "Actionable conclusion:", + "#68 因官方 expected 与 schema 冲突而退出下一轮干净 A/B;#142 因类别验收边界未定义而退出,直至补齐可观察口径;#168 修正本项目的空结果状态协议;#124 增加候选语义核验和提交前自检;#506 修复 HAR 即时采集、末步工具调用 fail-closed,并在预注册的 40 步上限下复测。旧的 25/30 仍保留为历史官方分数,但不能再解释成五个同因的 Agent 失败。": "Task 68 leaves the next clean A/B because the official expected value conflicts with its schema. Task 142 leaves until its category acceptance boundary is observable. Task 168 requires the project's zero-result status protocol to be fixed. Task 124 requires candidate-semantic verification and a pre-submit check. Task 506 requires immediate HAR body capture, fail-closed handling of a final-turn tool call, and a retest under a preregistered 40-step ceiling. The old 25/30 remains the historical official score, but it can no longer be interpreted as five same-cause Agent failures.", + "根治措施:": "Corrective action:", + "共同零分": "Shared zero scores", + "不等于共同根因": "Do not imply a shared cause", + "根因结论:": "Root-cause conclusion:", + "#68 是官方任务合同冲突,#124 是候选语义核验缺失,#142 是类别验收边界未定义,#168 是本项目响应协议错误;#506 的 Chromium 是 HAR 采集误判,Moli 是执行步数与完成确认问题。": "Task 68 is an official task-contract conflict; Task 124 lacks candidate-semantic verification; Task 142 has no defined category acceptance boundary; Task 168 is a response-protocol defect in this project. For Task 506, Chromium was misjudged because of HAR capture, while Moli ran out of decision steps without confirming completion.", + "任务合同问题": "Task-contract issues", + "2 题": "2 tasks", + "#68 schema 冲突;#142 类别口径缺失": "Task 68 has a schema conflict; Task 142 lacks a category criterion.", + "Agent 协议与策略": "Agent protocol and policy", + "#124 未核验候选;#168 空结果状态错误": "Task 124 did not validate candidates; Task 168 used the wrong zero-result status.", + "不同根因的同分": "Equal score, different causes", + "1 题": "1 task", + "#506:Chromium 采集误判;Moli 未确认完成": "Task 506: Chromium collection false negative; Moli completion was not confirmed.", + "处理原则:": "Treatment:", + "合同不稳定的任务退出下一轮样本;代码与协议缺陷修复后只通过新执行验证,不覆盖历史官方分数。": "Contract-unstable tasks leave the next cohort. Code and protocol fixes are validated only by new executions and never overwrite historical official scores.", + "查看五题根因、证据闭环与复测门槛 →": "View root causes, evidence chains, and retest gates for all five tasks →", "首页 · Browser Use 评测": "Home · Browser Use evaluation", "Browser Use 评测": "Browser Use evaluation", "Chromium 与 Moli:Browser Use 能力实测": "Chromium vs. Moli: a Browser Use capability evaluation", @@ -388,6 +417,17 @@ "中文": "中文", } +_FAILURE_ANALYSIS = json.loads( + (Path(__file__).parents[2] / "research/browser-use-failure-analysis.json").read_text( + encoding="utf-8" + ) +) +for _diagnosis in _FAILURE_ANALYSIS["tasks"].values(): + for _field in ("root_cause", "owner", "proof", "corrective_action", "verification"): + _localized = _diagnosis.get(_field) + if isinstance(_localized, dict) and _localized.get("zh") and _localized.get("en"): + TEXT[str(_localized["zh"])] = str(_localized["en"]) + # Browser Use-specific prose is intentionally translated here rather than in # the shared Web Fetch catalog. Task intent and measured evidence are not # listed: unmatched task text stays verbatim, and Localizer preserves pre/code @@ -445,7 +485,6 @@ "查看任务合同": "View task contract", "是": "Yes", "否": "No", - # Result-detail information architecture. "先看任务目标与验收结论,再按需展开技术明细和 CowAgent 的每一步 BrowserTool 调用。": "Start with the task objective and acceptance outcome, then expand technical evidence and each CowAgent BrowserTool call as needed.", "任务与验收": "Task and acceptance", @@ -489,7 +528,6 @@ "共享的最终状态协议错误,不是检索内容错误,也不是浏览器执行错误。 本诊断不修改官方分数。": "Shared final-status protocol error, not a retrieval-content or browser-execution error. This diagnosis does not change the official score.", "共享的长链任务完成错误:30 步上限前没有完成结账。两种浏览器路径细节不同,但都未越过同一验收终点。": "Shared long-horizon completion error: checkout was not completed within the 30-step limit. Browser paths differed in detail, but neither crossed the same acceptance endpoint.", "共享的长链任务完成错误:30 步上限前没有完成结账。两种浏览器路径细节不同,但都未越过同一验收终点。 本诊断不修改官方分数。": "Shared long-horizon completion error: checkout was not completed within the 30-step limit. Browser paths differed in detail, but neither crossed the same acceptance endpoint. This diagnosis does not change the official score.", - # Local environment and expansion guidance. "Mac 上可以继续扩展哪些实例": "Which environments can be expanded on Mac", "环境": "Environment", @@ -555,7 +593,6 @@ "。后者是 VM 视角下的 macOS 宿主网关,不是虚拟机地址,也不是评测网站。Shopping、Shopping Admin、Reddit、GitLab 四个官方站点镜像通过 Docker 端口发布到这个宿主入口,独立的 Chromium/Moli 浏览器容器据此访问本地站点。": ". The latter is the macOS host gateway from the VM's perspective, not the VM address or an evaluation website. The four official Shopping, Shopping Admin, Reddit, and GitLab images publish Docker ports through that host entry point, which the independent Chromium and Moli browser containers use to reach the local sites.", ",只保留该题所需站点;env-ctrl 报告 ready 后,再做连续应用健康检查、清浏览器状态、登录测试账号并打开起始页。当前冻结证据保存了代码提交、运行协议和镜像身份;事件流从环境准备完成后开始,因此逐次 reset、ready 与 health 结果未形成独立绑定的准备回执。新版运行协议已要求保存该回执。": ", retaining only the sites required by the task. After env-ctrl reports ready, the runner performs consecutive application health checks, clears browser state, signs in with test accounts, and opens the start page. The frozen evidence retains the code commit, run protocol, and image identities. Because the event stream begins after preparation, per-run reset, readiness, and health outcomes were not bound into a separate preparation receipt. The new run protocol requires that receipt.", "表 1|30 个任务的逐题最终结果配对。": "Table 1 | Paired final outcomes for 30 tasks.", - # Deployment, timing, browser matrix, and participant framing. "图 1 · 从官方站点镜像、逐题隔离与健康检查,到 CowAgent 观察—决策—操作闭环,再到 Chromium/Moli 对照和官方程序化验收。窄屏可横向滚动查看。": "Figure 1. From official site images, per-task isolation, and health checks through CowAgent's observe-decide-act loop, then Chromium/Moli comparison and official programmatic evaluation. Scroll horizontally on narrow screens.", "站点确实部署在本机,不是公网代站。": "The sites were deployed locally; they were not public-web substitutes.", @@ -630,7 +667,6 @@ "Agent 链路耗时包含会话准备、登录、模型推理、调度、工具执行和收尾,不包含 Docker 站点重置;BrowserTool 耗时是全部 browser 工具调用的墙钟时间之和,不等同于纯内核时间。": "Agent-stack latency includes session preparation, sign-in, model inference, orchestration, tool execution, and cleanup, but excludes Docker site reset. BrowserTool latency is the sum of wall-clock time for all browser tool calls and is not pure browser-core time.", "执行未完成;错误信息见运行与证据。": "Execution did not complete; see runtime and evidence for the error.", "全部官方 evaluator 均通过。": "All official evaluators passed.", - # Result pages and audit boundaries. "Agent 端到端耗时包含会话准备、登录、模型推理、调度、工具执行和收尾,不包含 Docker 站点重置;BrowserTool 执行耗时是全部 browser 工具调用的墙钟时间之和,不等同于纯内核时间。": "Agent end-to-end latency includes session preparation, sign-in, model inference, orchestration, tool execution, and cleanup, but excludes Docker site reset. BrowserTool execution latency is the sum of wall-clock time for all browser tool calls and is not pure browser-core time.", "公开包包含脱敏的回答、evaluator、事件流与轨迹。原始 HAR 和 Playwright trace 可能携带登录态、Cookie、页面快照或请求正文,因此不公开;其字节数和 SHA-256 保留用于证明证据绑定。": "The public bundle contains sanitized answers, evaluator records, event streams, and trajectories. Raw HAR and Playwright traces may contain authenticated state, cookies, page snapshots, or request bodies, so they are withheld; byte counts and SHA-256 hashes retain the evidence binding.", @@ -655,7 +691,6 @@ "争议处理": "Dispute handling", "任何不一致或缺失证据均阻止发布;人工不得改分": "Any inconsistency or missing evidence blocks release; humans may not change scores", "逐结果覆盖": "Per-result coverage", - # Participants, reproduction, and account boundaries. "方法介绍 · Browser Use 评测": "Participants · Browser Use evaluation", "本轮 A/B 的唯一变化项是浏览器实现;CowAgent、模型、任务、资源上限、站点与 evaluator 都是共同实验条件。": "The browser implementation is the only changed variable in this A/B test. CowAgent, the model, tasks, resource limits, sites, and evaluators are shared experimental conditions.", @@ -861,7 +896,6 @@ "找到最新完成订单,并规范化、排序多个行项目": "Find the latest completed order, then normalize and sort multiple line items", "清空购物车,按预算和评分筛选品类,选择商品并结账": "Empty the cart, filter a category by budget and rating, choose a product, and check out", "定位项目文件、修改标题、提交到默认分支并验证": "Locate a project file, change its title, commit to the default branch, and verify", - # Standalone interpretation article. "报告解读 · Browser Use 评测": "Interpretation · Browser Use evaluation", "从 25 比 25 读出下一轮该怎么做": "What the 25-to-25 result implies for the next run", @@ -943,7 +977,9 @@ def translate_text(value: str) -> str: ), ( r"最终状态不符:期望 (.+),实际为 (.+)。", - lambda match: f"Final status mismatch: expected {match.group(1)}, observed {match.group(2)}.", + lambda match: ( + f"Final status mismatch: expected {match.group(1)}, observed {match.group(2)}." + ), ), ( r"在同一 CowAgent、同一 (.+?)(reasoning=low)和同一 WebArena‑Verified 30 题样本下,对比两种浏览器完成真实网页任务的能力。", @@ -1035,7 +1071,7 @@ def _tag(tag: str, attrs, *, closed: bool = False) -> str: value = translate_text(value) rendered.append(f'{key}="{html.escape(value, quote=True)}"') suffix = " /" if closed else "" - return f'<{tag}{(" " + " ".join(rendered)) if rendered else ""}{suffix}>' + return f"<{tag}{(' ' + ' '.join(rendered)) if rendered else ''}{suffix}>" def handle_endtag(self, tag: str) -> None: if tag in self.stack: diff --git a/harness/browser_use/runner.py b/harness/browser_use/runner.py index 389cce7ec..10c8c7efc 100644 --- a/harness/browser_use/runner.py +++ b/harness/browser_use/runner.py @@ -208,7 +208,9 @@ def site_url(site: WebArenaSite, host: str) -> str: # The gateway IP is active. The official images may redirect to # host.docker.internal, so retain that as an equivalent URL for the # package's own derendering and NetworkEventEvaluator logic. - urls=list(dict.fromkeys([site_url(site, hostname), site_url(site, "host.docker.internal")])), + urls=list( + dict.fromkeys([site_url(site, hostname), site_url(site, "host.docker.internal")]) + ), credentials=SITE_CREDENTIALS[site], use_header_login=site == WebArenaSite.SHOPPING_ADMIN, ) @@ -277,9 +279,7 @@ def _verify_arm_php_compatibility(container_name: str) -> None: process_table = processes.output.decode(errors="replace") qemu = container.exec_run(["test", "-x", QEMU_X86_64_CONTAINER_PATH]) if processes.exit_code != 0 or "/mnt/lima-rosetta/rosetta" not in process_table: - raise RuntimeError( - "ARM Docker host must enable Rosetta for official amd64 site images" - ) + raise RuntimeError("ARM Docker host must enable Rosetta for official amd64 site images") if qemu.exit_code != 0: raise RuntimeError("PHP-FPM QEMU compatibility interpreter was not mounted") if f"{QEMU_X86_64_CONTAINER_PATH} /usr/local/sbin/php-fpm" not in process_table: @@ -293,9 +293,7 @@ def _site_runtime_compatibility(docker_arch: str) -> dict[str, Any]: "mode": "rosetta-with-qemu-php-fpm", "sites": sorted(site.value for site in AMD64_PHP_SITES), "qemu_host_path": QEMU_X86_64_HOST_PATH, - "supervisor_config_sha256": hashlib.sha256( - PHP_FPM_COMPAT_CONFIG.read_bytes() - ).hexdigest(), + "supervisor_config_sha256": hashlib.sha256(PHP_FPM_COMPAT_CONFIG.read_bytes()).hexdigest(), } @@ -388,8 +386,7 @@ def login() -> None: context.set_extra_http_headers( { SITE_AUTH_HEADERS[site]: ( - f"{SITE_CREDENTIALS[site]['username']}:" - f"{SITE_CREDENTIALS[site]['password']}" + f"{SITE_CREDENTIALS[site]['username']}:{SITE_CREDENTIALS[site]['password']}" ) for site in sites if site in SITE_AUTH_HEADERS @@ -442,14 +439,44 @@ def _system_prompt() -> str: change state unless the objective requires it. When finished, return only one JSON object with task_type (RETRIEVE, MUTATE, or NAVIGATE), status (SUCCESS, NOT_FOUND_ERROR, ACTION_NOT_ALLOWED_ERROR, PERMISSION_DENIED_ERROR, DATA_VALIDATION_ERROR, or UNKNOWN_ERROR), -retrieved_data, and error_details. For RETRIEVE, retrieved_data is always a list. For other task -types it is null.""" +retrieved_data, and error_details. Treat search and list pages as candidate sets, not proof that +every result satisfies the objective: verify every included item against all stated constraints +using its title, category, breadcrumb, detail page, or other visible site evidence before sorting, +aggregating, or mutating. Before returning, check semantic eligibility, completeness, arithmetic, +and any required saved or submitted state. A successful RETRIEVE returns a list. If an exhaustive +RETRIEVE finds no matching item, return NOT_FOUND_ERROR with retrieved_data null. For MUTATE and +NAVIGATE, retrieved_data is null.""" + + +def _exhausted_agent_steps(events: list[dict[str, Any]], max_steps: int) -> bool: + """Return whether CowAgent used its final decision turn for another tool call. + + CowAgent's max-step fallback asks the model for a prose summary without tools. That + fallback is not a benchmark completion and may claim success even though the last + browser action has not been observed or verified. + """ + + turns = [event for event in events if event.get("type") == "turn_end"] + if not turns: + return False + data = turns[-1].get("data") or {} + return int(data.get("turn") or 0) >= max_steps and bool(data.get("has_tool_calls")) + + +def _max_steps_response(task_type: str, max_steps: int) -> dict[str, Any]: + return { + "task_type": task_type.upper(), + "status": "UNKNOWN_ERROR", + "retrieved_data": None, + "error_details": ( + f"Agent reached the {max_steps}-step decision limit before producing " + "a tool-free, post-action completion." + ), + } def _user_prompt(task: Any, start_urls: list[str]) -> str: - response_eval = next( - item for item in task.eval if item.evaluator == "AgentResponseEvaluator" - ) + response_eval = next(item for item in task.eval if item.evaluator == "AgentResponseEvaluator") return ( "Task objective:\n" + task.intent @@ -499,6 +526,7 @@ def _run_attempt( # noqa: PLR0913 output_dir: Path, model: OpenAIResponsesModel, sample_interval_ms: int, + max_agent_steps: int, ) -> dict[str, Any]: task = benchmark.get_task(selected.task_id) sites = tuple(task.sites) @@ -521,9 +549,7 @@ def _run_attempt( # noqa: PLR0913 prepare_receipt["error"] = str(exc) raise finally: - prepare_receipt["duration_ms"] = round( - (time.perf_counter() - prepare_started) * 1000, 3 - ) + prepare_receipt["duration_ms"] = round((time.perf_counter() - prepare_started) * 1000, 3) prepare_path.write_text( json.dumps(prepare_receipt, ensure_ascii=False, indent=2), encoding="utf-8" ) @@ -586,7 +612,7 @@ def _run_attempt( # noqa: PLR0913 model=model, tools=[tool], output_mode="logger", - max_steps=30, + max_steps=max_agent_steps, enable_skills=False, skip_context_files=True, ) @@ -597,6 +623,13 @@ def _run_attempt( # noqa: PLR0913 raw_response = agent.run_stream( _user_prompt(task, start_urls), on_event=events.append, clear_history=True ) + if _exhausted_agent_steps(events, max_agent_steps): + response_eval = next( + item for item in task.eval if item.evaluator == "AgentResponseEvaluator" + ) + raw_response = json.dumps( + _max_steps_response(str(response_eval.expected.task_type), max_agent_steps) + ) agent_run_ms = (time.perf_counter() - agent_started) * 1000 finalize_started = time.perf_counter() service._submit( @@ -752,9 +785,7 @@ def run(args: argparse.Namespace) -> Path: # noqa: PLR0915 if not api_key: source = env_file or repo_root / ".env" raise RuntimeError(f"OPENAI_API_KEY is required in {source}") - cowagent_repo = Path( - args.cowagent_repo or repo_env.get("COWAGENT_REPO", "") - ).resolve() + cowagent_repo = Path(args.cowagent_repo or repo_env.get("COWAGENT_REPO", "")).resolve() if not (cowagent_repo / "agent/protocol/agent.py").exists(): raise RuntimeError("--cowagent-repo must point to a CowAgent checkout") harness_commit = _require_clean_git_checkout(repo_root) @@ -821,7 +852,9 @@ def run(args: argparse.Namespace) -> Path: # noqa: PLR0915 endpoints[engine] = _wait_cdp(http_endpoint, container=container) browser_metadata[engine] = _cdp_metadata(http_endpoint) - gateway_addresses = {_container_host_gateway(container) for container in containers.values()} + gateway_addresses = { + _container_host_gateway(container) for container in containers.values() + } if len(gateway_addresses) != 1: raise RuntimeError(f"browser containers disagree on host gateway: {gateway_addresses}") site_access_host = gateway_addresses.pop() @@ -838,9 +871,8 @@ def run(args: argparse.Namespace) -> Path: # noqa: PLR0915 "model": args.model, "reasoning_effort": "low", "model_transport_retries": 1, - "provider_base_url": env.get( - "OPENAI_BASE_URL", "https://api.openai.com/v1" - ), + "max_agent_steps": args.max_agent_steps, + "provider_base_url": env.get("OPENAI_BASE_URL", "https://api.openai.com/v1"), "browser_images": image_metadata, "site_images": site_image_metadata, "browser_versions": browser_metadata, @@ -894,6 +926,7 @@ def run(args: argparse.Namespace) -> Path: # noqa: PLR0915 output_dir=output_dir, model=model, sample_interval_ms=args.sample_interval_ms, + max_agent_steps=args.max_agent_steps, ) row["run_id"] = run_id stream.write(json.dumps(row, ensure_ascii=False) + "\n") @@ -948,6 +981,7 @@ def run(args: argparse.Namespace) -> Path: # noqa: PLR0915 "failure_retries": 1, "task_failure_retries": 1, "model_transport_retries": 1, + "max_agent_steps": args.max_agent_steps, "official_score_uses": "final execution for each browser and task", "logical_result_cost": "sum of all executions used for each browser and task", "evaluator": "official webarena-verified package, unmodified", @@ -996,6 +1030,12 @@ def parse_args() -> argparse.Namespace: parser.add_argument("--cpus", type=float, default=2.0) parser.add_argument("--memory", default="2g") parser.add_argument("--sample-interval-ms", type=int, default=100) + parser.add_argument( + "--max-agent-steps", + type=int, + default=40, + help="maximum CowAgent decision turns; exhaustion fails closed instead of summarizing", + ) parser.add_argument( "--max-tasks", type=int, diff --git a/harness/browser_use/selection.py b/harness/browser_use/selection.py index 0f8da4426..7d81041a4 100644 --- a/harness/browser_use/selection.py +++ b/harness/browser_use/selection.py @@ -13,6 +13,7 @@ ALLOWED_SITES = frozenset({"shopping", "shopping_admin", "reddit", "gitlab"}) TIER_COUNTS = {"easy": 5, "medium": 10, "hard": 15} TIER_BOUNDS = {"easy": (1, 4), "medium": (5, 7), "hard": (8, None)} +TASK_EXCLUSIONS_PATH = Path(__file__).parents[2] / "evaluation/browser-use/task-exclusions.json" @dataclass(frozen=True) @@ -36,6 +37,139 @@ def _sha256(path: Path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest() +def _json_equal(left: Any, right: Any) -> bool: + """Compare JSON values without treating booleans as numbers.""" + if type(left) is not type(right): + return False + if isinstance(left, list): + return len(left) == len(right) and all( + _json_equal(left_item, right_item) + for left_item, right_item in zip(left, right, strict=True) + ) + if isinstance(left, dict): + return left.keys() == right.keys() and all( + _json_equal(left[key], right[key]) for key in left + ) + return bool(left == right) + + +def _validate_json_schema(value: Any, schema: Any, *, path: str) -> None: + """Validate the JSON Schema subset used by official response contracts.""" + if not isinstance(schema, dict): + raise ValueError(f"{path}: schema must be an object") + + for keyword in ("anyOf", "oneOf"): + if keyword not in schema: + continue + branches = schema[keyword] + if not isinstance(branches, list) or not branches: + raise ValueError(f"{path}: {keyword} must be a non-empty array") + failures: list[str] = [] + matches = 0 + for branch in branches: + try: + _validate_json_schema(value, branch, path=path) + except ValueError as exc: + failures.append(str(exc)) + else: + matches += 1 + if keyword == "anyOf" and matches == 0: + raise ValueError(f"{path}: does not match anyOf ({'; '.join(failures)})") + if keyword == "oneOf" and matches != 1: + detail = f"; {'; '.join(failures)}" if failures else "" + raise ValueError( + f"{path}: must match exactly one oneOf branch, matched {matches}{detail}" + ) + + if "const" in schema and not _json_equal(value, schema["const"]): + raise ValueError(f"{path}: expected const {schema['const']!r}, got {value!r}") + if "enum" in schema: + options = schema["enum"] + if not isinstance(options, list): + raise ValueError(f"{path}: enum must be an array") + if not any(_json_equal(value, option) for option in options): + raise ValueError(f"{path}: expected one of {options!r}, got {value!r}") + + declared_types = schema.get("type") + if declared_types is not None: + if isinstance(declared_types, str): + type_names = [declared_types] + elif isinstance(declared_types, list) and all( + isinstance(item, str) for item in declared_types + ): + type_names = declared_types + else: + raise ValueError(f"{path}: type must be a string or array of strings") + checks = { + "object": lambda item: isinstance(item, dict), + "array": lambda item: isinstance(item, list), + "string": lambda item: isinstance(item, str), + "number": lambda item: isinstance(item, (int, float)) and not isinstance(item, bool), + "integer": lambda item: isinstance(item, int) and not isinstance(item, bool), + "boolean": lambda item: isinstance(item, bool), + "null": lambda item: item is None, + } + unknown = [name for name in type_names if name not in checks] + if unknown: + raise ValueError(f"{path}: unsupported schema type {unknown[0]!r}") + if not any(checks[name](value) for name in type_names): + actual = "null" if value is None else type(value).__name__ + expected = " or ".join(type_names) + raise ValueError(f"{path}: expected {expected}, got {actual}") + + if isinstance(value, dict): + required = schema.get("required", []) + if not isinstance(required, list) or not all(isinstance(key, str) for key in required): + raise ValueError(f"{path}: required must be an array of strings") + for key in required: + if key not in value: + raise ValueError(f"{path}: missing required property {key!r}") + properties = schema.get("properties", {}) + if not isinstance(properties, dict): + raise ValueError(f"{path}: properties must be an object") + for key, child_schema in properties.items(): + if key in value: + _validate_json_schema(value[key], child_schema, path=f"{path}.{key}") + + if isinstance(value, list) and "items" in schema: + item_schema = schema["items"] + for index, item in enumerate(value): + _validate_json_schema(item, item_schema, path=f"{path}[{index}]") + + +def _validate_agent_response_contract(task_id: int, task: Any) -> None: + evaluators = [item for item in task.eval if item.evaluator == "AgentResponseEvaluator"] + if not evaluators: + raise ValueError(f"task {task_id}: missing AgentResponseEvaluator") + for evaluator in evaluators: + payload = evaluator.model_dump(mode="json") + expected = payload.get("expected") + schema = payload.get("results_schema") + if not isinstance(expected, dict) or "retrieved_data" not in expected: + raise ValueError( + f"task {task_id}: AgentResponseEvaluator is missing expected.retrieved_data" + ) + if schema is None: + raise ValueError(f"task {task_id}: AgentResponseEvaluator is missing results_schema") + try: + _validate_json_schema( + expected["retrieved_data"], schema, path="expected.retrieved_data" + ) + except ValueError as exc: + raise ValueError( + f"task {task_id}: AgentResponseEvaluator expected.retrieved_data " + f"violates results_schema: {exc}" + ) from exc + + +def _task_exclusion(task_id: int) -> dict[str, str] | None: + payload = json.loads(TASK_EXCLUSIONS_PATH.read_text(encoding="utf-8")) + if payload.get("schema") != "lex_browser_use_task_exclusions/1": + raise ValueError("unsupported Browser Use task-exclusion schema") + item = (payload.get("tasks") or {}).get(str(task_id)) + return {str(key): str(value) for key, value in item.items()} if item else None + + def load_pilot(path: str | Path) -> PilotSelection: source = Path(path) payload = json.loads(source.read_text(encoding="utf-8")) @@ -59,15 +193,23 @@ def load_pilot(path: str | Path) -> PilotSelection: raise ValueError(f"pilot tier counts must be {TIER_COUNTS}, got {counts}") for task in tasks: low, high = TIER_BOUNDS[task.tier] - if task.estimated_min_steps < low or ( - high is not None and task.estimated_min_steps > high - ): + if task.estimated_min_steps < low or (high is not None and task.estimated_min_steps > high): raise ValueError(f"task {task.task_id}: step estimate is outside {task.tier}") return PilotSelection(tasks, source, _sha256(source), dict(payload.get("metadata") or {})) -def validate_against_official(selection: PilotSelection, benchmark: Any) -> list[dict[str, Any]]: - """Resolve task metadata through the official package; never copy evaluator rules.""" +def validate_against_official( + selection: PilotSelection, + benchmark: Any, + *, + enforce_contract_integrity: bool = True, +) -> list[dict[str, Any]]: + """Resolve official metadata and, for new runs, reject invalid response contracts. + + Historical report reconstruction may disable the prospective integrity gate because + its immutable cohort can contain the very contract defect being documented. It must + still resolve the exact official package and checksum used by that evidence. + """ hard_payload = json.loads( files("webarena_verified") .joinpath("assets/dataset/subsets/webarena-verified-hard.json") @@ -77,6 +219,14 @@ def validate_against_official(selection: PilotSelection, benchmark: Any) -> list resolved: list[dict[str, Any]] = [] for selected in selection.tasks: task = benchmark.get_task(selected.task_id) + if enforce_contract_integrity: + exclusion = _task_exclusion(selected.task_id) + if exclusion: + raise ValueError( + f"task {selected.task_id}: excluded from new Browser Use runs: " + f"{exclusion['reason']}; {exclusion['disposition']}" + ) + _validate_agent_response_contract(selected.task_id, task) sites = [getattr(site, "value", str(site)) for site in task.sites] if not set(sites).issubset(ALLOWED_SITES): raise ValueError(f"task {selected.task_id}: unsupported pilot sites {sites}") diff --git a/research/browser-use-failure-analysis.json b/research/browser-use-failure-analysis.json index 793b0f3ce..6e3b6f396 100644 --- a/research/browser-use-failure-analysis.json +++ b/research/browser-use-failure-analysis.json @@ -1,42 +1,52 @@ { - "schema": "lex_browser_use_failure_analysis/2", + "schema": "lex_browser_use_failure_analysis/3", "scope": { "benchmark": "WebArena-Verified 1.2.3", "task_ids": [68, 124, 142, 168, 506], "engines": ["chromium", "moli"], - "evidence": "final logical response, official evaluator output, and autonomous BrowserTool trajectory", - "score_effect": "none" + "evidence": "score-bearing final response, official evaluator output, HAR, and autonomous BrowserTool event trajectory", + "score_effect": "historical official scores are unchanged; corrected protocols require new executions" }, "tasks": { "68": { - "mismatch": "两种浏览器都只返回《The Hobbit》,官方合同还要求《A Christmas Carol》(author=null)。", - "confirmed_failure_point": "最终答案少了一条必需记录。", - "direct_evidence": "两条最终轨迹都已读取《A Christmas Carol》帖子;官方 evaluator 报告 Matched (1/2)。", - "root_cause_boundary": "可确认遗漏发生在页面内容已经进入轨迹之后;现有证据不能判定模型为何丢弃该记录。" + "observed_result": "Both results returned only The Hobbit; the official expected payload also requires A Christmas Carol with author=null.", + "root_cause": {"zh": "官方响应合同内部矛盾:expected 中 author 为 null,而公开 results_schema 只允许 string。", "en": "The official response contract is internally inconsistent: the expected payload contains a null author while the published results_schema permits only string for author."}, + "owner": {"zh": "评测集任务合同", "en": "Benchmark task contract"}, + "proof": {"zh": "程序化扫描 812 个官方任务,发现 8 个 expected 不符合自身 schema(2、3、6、21、68、69、96、319);本轮 30 题中只有 #68。两条轨迹都打开过 A Christmas Carol 帖子,页面访问不是阻断点。", "en": "A scan of all 812 official tasks found eight expected payloads that violate their own published schema (2, 3, 6, 21, 68, 69, 96, 319). Task 68 is the only one in this 30-task cohort. Both trajectories opened the A Christmas Carol post, so page access was not the stopping point."}, + "corrective_action": {"zh": "新运行在调用模型前拒绝合同不一致任务。#68 退出干净浏览器 A/B,除非官方修正 schema,或实验预注册明确的 benchmark 补丁。", "en": "Reject contract-inconsistent tasks during preflight. Exclude task 68 from a clean browser A/B unless the benchmark publishes a corrected schema or the experiment preregisters an explicit benchmark patch."}, + "verification": {"zh": "准入检查必须在任何模型或浏览器调用前失败,并指出违反 schema 的 expected 字段路径。", "en": "The preflight must fail before any model or browser call and identify the expected-value path that violates the schema."} }, "124": { - "mismatch": "两种浏览器都返回 min=0.01、max=17774.32;官方合同要求 max=298.00。", - "confirmed_failure_point": "最终价格聚合纳入了不符合 wireless earphone 语义的商品。", - "direct_evidence": "轨迹中的 17,774.32 对应 QFWCJ 4K Short Throw Projector,且与最终错误最大值完全相同。", - "root_cause_boundary": "可确认错误值来自无关投影仪;现有证据不能区分是模型判断、Agent 策略还是任务提示约束不足导致未过滤。" + "observed_result": "Both results used 17774.32 as the maximum instead of the official 298.00.", + "root_cause": {"zh": "Agent 把 Magento 全文搜索命中直接当成合格的无线耳机集合,未逐项验证任务语义便聚合页面极值。", "en": "The Agent treated Magento full-text search hits as an already-qualified wireless-earphone set and aggregated page extrema without validating each candidate against the task noun phrase."}, + "owner": {"zh": "Agent 完成策略", "en": "Agent completion policy"}, + "proof": {"zh": "两条轨迹中的 17,774.32 都属于 QFWCJ 4K Short Throw Projector,并与最终错误最大值完全一致;两条轨迹都没有打开或以其他方式验证该候选是无线耳机。", "en": "The 17774.32 value in both trajectories belongs to QFWCJ 4K Short Throw Projector; it is exactly the incorrect maximum in both final answers. Neither trajectory opened or otherwise validated that candidate as a wireless earphone."}, + "corrective_action": {"zh": "把候选语义核验和提交前的完整性检查写入统一 Agent 协议,不增加针对 #124 标准答案的特例。", "en": "Make candidate validation and a pre-submit semantic/completeness check part of the common Agent protocol; do not add a task-specific expected-answer rule."}, + "verification": {"zh": "按修订协议在两个浏览器重跑 #124,并要求证据证明纳入极值的商品满足全部任务约束。", "en": "Rerun task 124 on both browsers under the revised protocol and require evidence that included extrema satisfy every task constraint."} }, "142": { - "mismatch": "两种浏览器都返回 80.50;官方合同要求 68.51。", - "confirmed_failure_point": "最终求和多计入了 11.99 的 Crystal Headband。", - "direct_evidence": "轨迹列出 50.52 的 conditioner、11.99 的 headband 和 17.99 的 haircolor;80.50=50.52+11.99+17.99,68.51=50.52+17.99。", - "root_cause_boundary": "可确认差额及其来源;现有证据不能判定模型为何把 headband 纳入任务口径。" + "observed_result": "Both results returned 80.50; the official expected value is 68.51.", + "root_cause": {"zh": "任务没有可操作地定义“hair care and hair style”的归类边界。Agent 按商品名归类并纳入 11.99 的 Crystal Headband,官方答案却在未给规则的情况下排除了它。", "en": "The task does not operationally define membership in 'hair care and hair style'. The Agent classified by product name and included a 11.99 Crystal Headband, while the official answer silently excludes it."}, + "owner": {"zh": "评测集验收边界,叠加 Agent 类别核验不足", "en": "Benchmark acceptance boundary, amplified by Agent category verification"}, + "proof": {"zh": "轨迹列出 50.52 的 conditioner、11.99 的 headband 和 17.99 的 haircolor,三者合计 80.50;排除 headband 恰为官方 68.51。任务合同只给最终数字,没有类别 taxonomy 或纳入规则。", "en": "The trajectories list 50.52 conditioner, 11.99 headband, and 17.99 haircolor. Their sum is 80.50; excluding the headband yields the expected 68.51. The task contract contains only the final number and no category taxonomy or inclusion rubric."}, + "corrective_action": {"zh": "#142 退出浏览器 A/B,直至类别归属绑定到站点 taxonomy 等可观察规则;统一 Agent 协议同时要求用站点证据核验类别。", "en": "Exclude task 142 from browser A/B until category membership is bound to an observable rule such as site taxonomy. The generic Agent protocol should also verify category claims against site evidence."}, + "verification": {"zh": "替代任务必须具有无需查看 expected total、两名独立复核者也能一致执行的客观类别谓词。", "en": "A replacement task must have an objective category predicate that two independent reviewers can apply without seeing the expected total."} }, "168": { - "mismatch": "两种浏览器都返回 SUCCESS + [];官方合同要求 NOT_FOUND_ERROR + null。", - "confirmed_failure_point": "空结果被编码成错误的最终状态。", - "direct_evidence": "官方 evaluator 的唯一失败断言是 Expected not_found_error, got success;检索内容同为空。", - "root_cause_boundary": "可确认是最终响应协议不匹配,不是页面导航失败;现有证据不能判定错误映射来自模型还是 Agent 响应封装。" + "observed_result": "Both results returned SUCCESS with an empty list; the official contract requires NOT_FOUND_ERROR with null.", + "root_cause": {"zh": "本项目 Agent 适配协议错误地规定所有 RETRIEVE 都必须返回列表,并遗漏零结果状态映射,直接违反该题官方 null schema。", "en": "The benchmark adapter's system protocol incorrectly stated that every RETRIEVE result must be a list and omitted the required zero-result status mapping, directly contradicting this task's official null schema."}, + "owner": {"zh": "browser-eval Agent 适配层", "en": "browser-eval Agent adapter"}, + "proof": {"zh": "官方 evaluator 唯一失败断言是状态不一致。该题 schema 为 null,而旧 runner 协议写明“For RETRIEVE, retrieved_data is always a list”。两条轨迹均完成搜索且没有匹配项目。", "en": "The official evaluator's only failed assertion is the status mismatch. The task schema is null, while the runner's system protocol said 'For RETRIEVE, retrieved_data is always a list.' Both trajectories completed the search and found no matching project."}, + "corrective_action": {"zh": "只在成功检索时返回 SUCCESS 加列表;穷尽搜索仍无结果时返回 NOT_FOUND_ERROR 加 null。", "en": "Specify SUCCESS plus list only for successful retrieval and NOT_FOUND_ERROR plus null after an exhaustive zero-result search."}, + "verification": {"zh": "单测锁定协议,并在两个浏览器重跑 #168;导航行为可以不变,但最终状态合同必须通过。", "en": "Unit-test the protocol and rerun task 168 on both browsers; navigation behavior need not change, but the final status contract must pass."} }, "506": { - "mismatch": "最终逻辑结果均已添加正确商品,但没有付款提交事件和下单成功页事件。", - "confirmed_failure_point": "结账在付款提交之前停止。", - "direct_evidence": "官方 evaluator 已确认正确商品加入购物车;付款提交与成功页规则失败,最终响应明确记录达到单次运行最大步骤数。", - "root_cause_boundary": "30 步上限是已确认的直接停止条件;现有证据不能进一步量化模型规划、Agent 调度和浏览器工具开销各自消耗了多少不必要步骤。" + "observed_result": "Both official scores are zero, but the score-bearing executions failed for different reasons.", + "root_cause": {"zh": "两端根因不同。Chromium 是测量假阴性:结账已完成,但 HAR 在运行结束才取响应体,CDP 已淘汰付款响应。Moli 是执行与完成确认失败:第 30 个决策回合才点击 Place order,没有后续观察,CowAgent 超限总结仍生成了成功形态的回答。", "en": "Chromium is a measurement false negative: checkout completed, but the HAR collector deferred Network.getResponseBody until run end and CDP had already evicted the payment response body. Moli is an execution/completion failure: its 30th decision turn issued Place order, leaving no post-action observation, and CowAgent's max-step fallback still produced a success-shaped answer."}, + "owner": {"zh": "Chromium:browser-eval HAR 采集器;Moli:运行预算与 CowAgent 完成处理", "en": "Chromium: browser-eval HAR collector; Moli: run budget and CowAgent completion handling"}, + "proof": {"zh": "Chromium 已到成功页并记录订单号;evaluator 的响应、商品、付款和成功页检查均通过,只有购物车总额规则收到 null body,HAR 同时记录 Network.getResponseBody unavailable。Moli 没有付款信息或成功页事件,最后一回合 has_tool_calls=true 并点击 Place order。", "en": "Chromium reached the success page and recorded an order number; its evaluator passed response, product, payment, and success-page checks, while the cart-total rule alone received a null body and HAR logged Network.getResponseBody unavailable. Moli recorded no payment-information or success-page event; its final turn had has_tool_calls=true and clicked Place order."}, + "corrective_action": {"zh": "在 CDP loadingFinished 时立即保存 evaluator 所需 JSON 响应体;最后决策回合仍有工具调用时 fail closed;预注册 40 步上限,为最后动作保留观察与验证空间。", "en": "Capture evaluator-relevant JSON bodies immediately on CDP loadingFinished. Fail closed when the final decision turn still contains a tool call, and preregister a 40-turn ceiling so a last action can be observed and verified."}, + "verification": {"zh": "重跑 Chromium 与 Moli 的 #506。Chromium 必须把付款 JSON 保留到评测;Moli 必须在 40 步内确认成功页,否则返回 UNKNOWN_ERROR,不能报告未核验的成功。", "en": "Rerun Chromium and Moli task 506. Chromium must retain the payment JSON body through evaluation; Moli must either verify the success page within 40 turns or return UNKNOWN_ERROR rather than an unverified success."} } } } diff --git a/research/report-changes.json b/research/report-changes.json index 1fc502d5b..8eb89031c 100644 --- a/research/report-changes.json +++ b/research/report-changes.json @@ -5,12 +5,12 @@ { "id": "review-2026-09-10-shared-failure-and-expansion", "date": "2026-09-10", - "title": "共同失败点与本机扩展核验", + "title": "失败根因、修复与本机扩展核验", "status": "已处理", - "scope": "五个共同失败任务、结果详情信息层级、Colima 网络身份和 Mac 环境扩展范围。", - "findings": "结果相同尚未解释到共享链路;详情页重复展示合同、回答和 evaluator 原始 JSON;192.168.5.2 的角色容易被误读为虚拟机地址;179 题并非官方子集。", - "corrections": "逐题交叉核对最终回答、官方规则和执行轨迹,只发布可直接证明的失败点、证据与归因边界;把任务目标与逐规则验收合并为单一可读区,技术 JSON 默认折叠;明确 192.168.5.2 是 VM 看到的 macOS 宿主网关、VM 地址为 192.168.5.3;列出四个已实跑站点的 678 个候选与 233 个 hard 候选。", - "remaining": "Wikipedia 与 Map 尚未在本机实跑;旧结果仍缺逐执行环境准备回执,下一批必须先做可完成性预检并保存回执。" + "scope": "五个零分任务的根因、修复代码、结果详情信息层级、Colima 网络身份和 Mac 环境扩展范围。", + "findings": "相同官方分数掩盖了不同根因:Task 68 的官方 expected 与 schema 冲突;124 缺少候选语义核验;142 的类别验收边界未定义;168 的本项目响应协议与官方 null 合同冲突;506 中 Chromium 为 HAR 采集误判,Moli 为步数耗尽且未确认完成。", + "corrections": "新增运行前合同一致性校验、候选语义与提交前检查、空结果状态协议、JSON 响应体即时 HAR 采集、末步工具调用 fail-closed 和可配置步数上限;报告按根因、责任层、证据、根治措施和复测门槛解释结果。历史分数不改写。", + "remaining": "修复后的 #124、#168、#506 尚需形成新执行证据;#68 与 #142 退出下一轮干净 A/B,直至合同被修复或验收口径可观察。Wikipedia 与 Map 尚未在本机实跑。" }, { "id": "review-2026-09-10-track-naming-and-release-gates", @@ -67,22 +67,28 @@ { "id": "2026-09-10-shared-failure-diagnosis-and-mac-expansion", "date": "2026-09-10", - "title": "确认共同失败点并明确 Mac 扩展边界", - "summary": "报告将五个共同失败提升为核心解读:逐题列出可证失败点、直接证据与归因边界,并在首页按答案数据、状态协议和任务完成边界汇总;同时明确 Colima 宿主网关、本地站点实跑范围及下一批可在 Mac 上预检的任务规模。官方成绩仍为 Chromium 25/30、Moli 25/30。", + "title": "追到失败根因并落实运行器修复", + "summary": "五个零分已追到可操作根因:两项任务合同问题、两项 Agent 协议或策略问题,以及 Task 506 中 Chromium 与 Moli 各不相同的采集和完成问题。运行器已修复合同准入、响应协议、候选核验、HAR 采集与超限处理;历史官方成绩仍为 Chromium 25/30、Moli 25/30,修复效果等待新执行验证。", "attempts_sha256": "517b613adcc40fc0ebe41af2ab2115d76f49faaba5378b426cc196b5b633e9c7", "selection_sha256": "ba3c344df003dd4eba3a5d1ef97162ee98df2ed728f7c29c4188bc4247982d38", "changes": [ [ "报告解读", - "总分相同不足以解释结果,失败证据不应只作为详情页补充信息。", - "将共同失败提升为首页核心结论和解读文章主论点,并链接到五题逐条证据与归因边界。", + "总分相同不足以解释结果,且相同零分不代表相同失败原因。", + "将根因、责任层、证据闭环、根治措施和复测门槛提升为首页与解读文章的核心结论。", "改变结论层级,不改变官方分数、分母或冻结证据。" ], [ "共同失败", - "相同分数只被描述为无分歧,未定位五题的具体验收失败点。", - "按任务核对最终回答、官方 evaluator 和轨迹,分别锁定遗漏记录、纳入错误商品、错误求和、状态编码和付款前停止。", - "只发布证据可直接证明的事实和归因边界,不推断模型隐藏决策;官方分数不变。" + "旧解读停留在错误现象,没有定位产生错误的合同、策略或采集实现。", + "结合官方 expected/schema、最终回答、evaluator、HAR 与逐步事件,定位四类根因;Task 506 分浏览器分别归因。", + "不推断模型隐藏思维;根因由可复核证据闭环支持,官方历史分数不变。" + ], + [ + "运行器根治", + "错误合同会消耗正式调用;空结果协议、搜索候选聚合、HAR 延迟取体和超限总结会制造可避免的失败或误判。", + "新运行先验证 expected 是否满足公开 schema;统一要求候选语义和提交前核验;JSON 响应体在 loadingFinished 即时保存;末步仍调用工具时强制 UNKNOWN_ERROR;步数上限写入运行配置。", + "代码门禁已通过 61 个 Browser Use 单测、206 条报告路由和 104 个视觉状态;任务成败改善仍必须由新执行证明。" ], [ "结果详情", diff --git a/scripts/check_browser_use_report.py b/scripts/check_browser_use_report.py index 706e027e3..a0ec18ed2 100644 --- a/scripts/check_browser_use_report.py +++ b/scripts/check_browser_use_report.py @@ -229,7 +229,7 @@ def check( standalone = (report_dir / "guide/article.html").read_text(encoding="utf-8") if " None: + class Session: + calls = 0 + + def send(self, _method, _params): + self.calls += 1 + if self.calls > 1: + raise RuntimeError("body already evicted") + return {"body": '{"items_qty":1}', "base64Encoded": False} + + def detach(self): + pass + + session = Session() + recorder = CdpHarRecorder() + recorder._sessions.append(session) + recorder._record_request( + session, + id(session), + { + "requestId": "1", + "request": {"method": "GET", "url": "http://example.test/totals"}, + }, + ) + recorder._record_response( + session, + id(session), + { + "requestId": "1", + "response": {"status": 200, "mimeType": "application/json", "headers": {}}, + }, + ) + + recorder._record_finished(session, id(session), {"requestId": "1"}) + path = tmp_path / "network.har" + recorder.stop(object(), path) + + content = json.loads(path.read_text())["log"]["entries"][0]["response"]["content"] + assert content["text"] == '{"items_qty":1}' + assert session.calls == 1 + assert recorder.errors == [] + + def test_invalid_base64_is_not_written_as_plain_json(tmp_path) -> None: class Session: def send(self, _method, _params): diff --git a/tests/browser_use/test_runner.py b/tests/browser_use/test_runner.py index 11a1d81ef..63f0d79de 100644 --- a/tests/browser_use/test_runner.py +++ b/tests/browser_use/test_runner.py @@ -8,8 +8,10 @@ _allow_official_container_hostname, _configure_benchmark, _container_host_gateway, + _exhausted_agent_steps, _limit_cowagent_transport_retries, _login_context, + _max_steps_response, _scheduled_tasks, _site_container_manager, _site_runtime_compatibility, @@ -44,6 +46,32 @@ def test_user_prompt_exposes_schema_but_not_reference_answer() -> None: assert "hollister" not in prompt +def test_system_contract_fails_empty_retrieve_as_not_found() -> None: + from harness.browser_use.runner import _system_prompt + + prompt = _system_prompt() + assert "NOT_FOUND_ERROR with retrieved_data null" in prompt + assert "search and list pages as candidate sets" in prompt + assert "retrieved_data is always a list" not in prompt + + +def test_agent_step_exhaustion_requires_a_final_tool_call() -> None: + exhausted = [{"type": "turn_end", "data": {"turn": 40, "has_tool_calls": True}}] + completed = [{"type": "turn_end", "data": {"turn": 40, "has_tool_calls": False}}] + + assert _exhausted_agent_steps(exhausted, 40) is True + assert _exhausted_agent_steps(completed, 40) is False + assert _max_steps_response("retrieve", 40) == { + "task_type": "RETRIEVE", + "status": "UNKNOWN_ERROR", + "retrieved_data": None, + "error_details": ( + "Agent reached the 40-step decision limit before producing " + "a tool-free, post-action completion." + ), + } + + def test_max_tasks_limits_only_the_current_invocation() -> None: tasks = load_pilot("evaluation/browser-use/webarena-verified-pilot.json").tasks diff --git a/tests/browser_use/test_selection.py b/tests/browser_use/test_selection.py index 3b0fe736a..f6ba1c686 100644 --- a/tests/browser_use/test_selection.py +++ b/tests/browser_use/test_selection.py @@ -1,6 +1,17 @@ from pathlib import Path +from types import SimpleNamespace -from harness.browser_use.selection import TIER_COUNTS, load_pilot +import pytest + +from harness.browser_use.selection import ( + TIER_COUNTS, + PilotSelection, + PilotTask, + _task_exclusion, + _validate_json_schema, + load_pilot, + validate_against_official, +) def test_pilot_has_pre_registered_tier_counts() -> None: @@ -11,3 +22,141 @@ def test_pilot_has_pre_registered_tier_counts() -> None: } == TIER_COUNTS assert sum(task.official_hard for task in selection.tasks) == 16 assert len(selection.sha256) == 64 + + +def test_unstable_task_exclusions_are_machine_readable() -> None: + assert "results_schema" in _task_exclusion(68)["reason"] + assert "category membership" in _task_exclusion(142)["reason"] + assert _task_exclusion(44) is None + + +@pytest.mark.parametrize( + ("value", "schema"), + [ + ({"value": "ok"}, {"type": "object"}), + (["ok"], {"type": "array"}), + ("ok", {"type": "string"}), + (1.5, {"type": "number"}), + (1, {"type": "integer"}), + (True, {"type": "boolean"}), + (None, {"type": "null"}), + ], +) +def test_json_schema_accepts_supported_types(value: object, schema: dict[str, object]) -> None: + _validate_json_schema(value, schema, path="expected.retrieved_data") + + +@pytest.mark.parametrize( + ("value", "schema"), + [ + (None, {"type": "string"}), + (True, {"type": "number"}), + (1.5, {"type": "integer"}), + (0, {"type": "boolean"}), + ], +) +def test_json_schema_rejects_wrong_types(value: object, schema: dict[str, object]) -> None: + with pytest.raises(ValueError, match="expected.retrieved_data: expected"): + _validate_json_schema(value, schema, path="expected.retrieved_data") + + +def test_json_schema_validates_nested_contract_keywords() -> None: + schema = { + "type": "array", + "items": { + "type": "object", + "required": ["kind", "value"], + "properties": { + "kind": {"enum": ["count", "label"]}, + "value": { + "oneOf": [ + {"type": "integer", "const": 3}, + {"type": "string"}, + ] + }, + "optional": {"anyOf": [{"type": "boolean"}, {"type": "null"}]}, + }, + }, + } + _validate_json_schema( + [{"kind": "count", "value": 3, "optional": None}], + schema, + path="expected.retrieved_data", + ) + + with pytest.raises(ValueError, match="missing required property 'value'"): + _validate_json_schema([{"kind": "count"}], schema, path="expected.retrieved_data") + with pytest.raises(ValueError, match="expected one of"): + _validate_json_schema( + [{"kind": "other", "value": "ok"}], + schema, + path="expected.retrieved_data", + ) + with pytest.raises(ValueError, match="does not match anyOf"): + _validate_json_schema( + [{"kind": "label", "value": "ok", "optional": 1}], + schema, + path="expected.retrieved_data", + ) + + +def test_official_contract_validation_rejects_task_68_null_author() -> None: + evaluator = SimpleNamespace( + evaluator="AgentResponseEvaluator", + model_dump=lambda **_: { + "expected": { + "retrieved_data": [ + {"book": "The Hobbit", "author": "Tolkien"}, + {"book": "A Christmas Carol", "author": None}, + ] + }, + "results_schema": { + "type": "array", + "items": { + "type": "object", + "properties": { + "book": {"type": "string"}, + "author": {"type": "string"}, + }, + }, + }, + }, + ) + task = SimpleNamespace( + eval=[evaluator], + sites=[SimpleNamespace(value="reddit")], + intent="intent", + intent_template_id=17, + revision=0, + start_urls=["__REDDIT__"], + ) + selection = PilotSelection( + tasks=(PilotTask(68, "hard", 12, "rationale", True),), + source_path=Path("selection.json"), + sha256="0" * 64, + metadata={}, + ) + + with pytest.raises( + ValueError, + match=r"task 68: excluded from new Browser Use runs: .*results_schema", + ): + validate_against_official(selection, SimpleNamespace(get_task=lambda _: task)) + + with pytest.raises( + ValueError, + match=( + r"task 68: AgentResponseEvaluator expected\.retrieved_data violates " + r"results_schema: expected\.retrieved_data\[1\]\.author: expected string, got null" + ), + ): + from harness.browser_use.selection import _validate_agent_response_contract + + _validate_agent_response_contract(68, task) + + resolved = validate_against_official( + selection, + SimpleNamespace(get_task=lambda _: task), + enforce_contract_integrity=False, + ) + assert resolved[0]["task_id"] == 68 From 6d2100cbec8a96d5369f4072d43df20bd5057a40 Mon Sep 17 00:00:00 2001 From: kevin Date: Thu, 10 Sep 2026 22:40:28 +0800 Subject: [PATCH 044/127] gate browser use root causes on review --- AGENTS.md | 1 + evaluation/browser-use/current-report.json | 4 + harness/browser_use/failure_cause_review.py | 79 +++++++++++ harness/browser_use/report.py | 130 +++++++++++++----- harness/browser_use/report_i18n.py | 17 +++ .../browser-use-failure-cause-review.json | 64 +++++++++ research/report-changes.json | 12 +- scripts/check_browser_use_report.py | 40 ++++++ .../browser_use/test_failure_cause_review.py | 102 ++++++++++++++ 9 files changed, 414 insertions(+), 35 deletions(-) create mode 100644 harness/browser_use/failure_cause_review.py create mode 100644 research/browser-use-failure-cause-review.json create mode 100644 tests/browser_use/test_failure_cause_review.py diff --git a/AGENTS.md b/AGENTS.md index 1763fedc3..e90c981e3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,6 +15,7 @@ - Maintain `research/report-changes.json` for substantive releases and `research/provider-pricing.json` for public pricing. Release notes explain reader-visible or result-visible effects without narrating assistant workflow; prices retain verification dates, commitments, billing units and unknowns. - Use `Browser Use` for the autonomous track and `Browser Trajectory Replay` (Chinese: `轨迹重放`) for the fixed BrowserTool-action track. Their implementation roots are `harness/browser_use/` and `harness/trajectory_replay/`; do not add aliases or compatibility paths using the retired names. - Browser Use reports expose one manifest-authoritative logical result per browser and task. Dataset routes contain only frozen task definitions and acceptance contracts; result routes own verdicts, timings and trajectories. Keep one filterable result inventory instead of adding duplicate success/failure tables. +- Keep automatic evaluator failure location separate from reviewed root cause. A root-cause subtype and reason must come from a per-browser, per-task review bound to the current attempts hash; render both fields blank while that result is unverified, and require complete verified coverage before a formal release when the current profile enables the gate. - Generate browser rows and comparisons from the participant registry. The same-task explorer uses one baseline browser, multi-select comparison browsers and explicit outcome relations, with browser labels linking to full results; never add one fixed table column per future browser. List Agent, model, prompt/tool contract, resources, environment and evaluator as shared controls. Standard summaries use mean, P50 and P95; mark only unique winners on directionally comparable metrics. For action latency, compare per-call latency and keep unequal-workload totals and call counts neutral. - Result-detail summaries contain only the verdict, concise failed-rule labels and short scalar facts. Full evaluator messages, expected/actual payloads, traces and logs belong in dedicated evidence sections, never narrow KPI cards. - Split environment preparation, browser-session setup, Agent run, BrowserTool wall time, evidence finalization and evaluator time at collection. Publish the self-hosted deployment topology and image identity; claim per-execution reset/readiness only when a preparation receipt binds reset, readiness and application health. diff --git a/evaluation/browser-use/current-report.json b/evaluation/browser-use/current-report.json index 888c2d0aa..6b822a4b5 100644 --- a/evaluation/browser-use/current-report.json +++ b/evaluation/browser-use/current-report.json @@ -21,6 +21,10 @@ "scoring_authority": "unmodified official programmatic evaluator", "review_kind": "deterministic evidence integrity audit", "independent_semantic_review": false, + "failure_cause_review": { + "required_for_publication": true, + "source_sha256": "91125db4f0fc5bed9424267505fa372b7cf44bb2fec3b742cd5f1ee9f2aefdd2" + }, "tasks_without_measured_success_path": [ 68, 124, diff --git a/harness/browser_use/failure_cause_review.py b/harness/browser_use/failure_cause_review.py new file mode 100644 index 000000000..84e355c78 --- /dev/null +++ b/harness/browser_use/failure_cause_review.py @@ -0,0 +1,79 @@ +"""Load hash-bound, per-result Browser Use failure-cause reviews.""" + +from __future__ import annotations + +import hashlib +import json +from pathlib import Path +from typing import Any + + +DEFAULT_REVIEW_PATH = ( + Path(__file__).parents[2] / "research/browser-use-failure-cause-review.json" +) + + +def sha256(path: Path) -> str | None: + return hashlib.sha256(path.read_bytes()).hexdigest() if path.is_file() else None + + +def load_failure_cause_review(path: Path = DEFAULT_REVIEW_PATH) -> dict[str, Any]: + return json.loads(path.read_text(encoding="utf-8")) if path.is_file() else {} + + +def validate_failure_cause_review( + review: dict[str, Any], + *, + attempts_sha256: str | None, + failed_result_ids: set[str], + require_complete: bool, +) -> dict[str, dict[str, Any]]: + """Return only verified causes; never promote draft or stale diagnoses.""" + + if not review: + if require_complete and failed_result_ids: + raise ValueError("complete publication requires a failure-cause review") + return {} + if review.get("schema") != "lex_browser_use_failure_cause_review/1": + raise ValueError("unsupported Browser Use failure-cause review schema") + bound_hash = (review.get("bindings") or {}).get("attempts_sha256") + if not attempts_sha256 or bound_hash != attempts_sha256: + if require_complete: + raise ValueError("failure-cause review is not bound to the current attempts") + return {} + + results = review.get("results") or {} + verified: dict[str, dict[str, Any]] = {} + for result_id, item in results.items(): + if result_id not in failed_result_ids: + raise ValueError(f"failure-cause review includes a non-failed result: {result_id}") + status = str(item.get("status") or "") + category = item.get("subcategory") or {} + reason = item.get("reason") or {} + if status != "verified": + if category or reason: + raise ValueError( + f"unverified failure cause must not contain a subcategory or reason: {result_id}" + ) + continue + if not all(category.get(language) for language in ("code", "zh", "en")): + raise ValueError(f"verified failure cause has an incomplete subcategory: {result_id}") + if not all(reason.get(language) for language in ("zh", "en")): + raise ValueError(f"verified failure cause has an incomplete reason: {result_id}") + verified[result_id] = item + + if require_complete and set(verified) != failed_result_ids: + missing = sorted(failed_result_ids - set(verified)) + raise ValueError(f"failure-cause review is incomplete: {missing}") + return verified + + +def localized_cause( + verified: dict[str, dict[str, Any]], result_id: str, language: str = "zh" +) -> tuple[str, str]: + """Return blank fields until a result has a verified, current review.""" + + item = verified.get(result_id) or {} + category = item.get("subcategory") or {} + reason = item.get("reason") or {} + return str(category.get(language) or ""), str(reason.get(language) or "") diff --git a/harness/browser_use/report.py b/harness/browser_use/report.py index 4bc587176..27ba82d8f 100644 --- a/harness/browser_use/report.py +++ b/harness/browser_use/report.py @@ -18,6 +18,13 @@ from harness.web_fetch.report_style import STYLE as SHARED_STYLE +from .failure_cause_review import ( + DEFAULT_REVIEW_PATH as FAILURE_CAUSE_REVIEW_PATH, + load_failure_cause_review, + localized_cause, + sha256 as file_sha256, + validate_failure_cause_review, +) from .publication import ( evaluator_summary, evidence_metadata, @@ -992,6 +999,23 @@ def _failed_rule_labels(result: dict[str, Any] | None) -> list[str]: ] +def _automatic_failure_type(result: dict[str, Any] | None) -> tuple[str, str]: + """Classify the observable evaluator failure without claiming root cause.""" + + names = { + item["name"] + for item in evaluator_summary(result) + if float(item.get("score") or 0) < 1 + } + if not names: + return "", "" + if names == {"AgentResponseEvaluator"}: + return "response", "最终回答验收失败" + if names == {"NetworkEventEvaluator"}: + return "network", "网络副作用验收失败" + return "mixed", "多项验收失败" + + def _failure_brief(result: dict[str, Any] | None, row: dict[str, Any]) -> str: labels = _failed_rule_labels(result) if labels: @@ -1260,27 +1284,20 @@ def _task_table(tasks: list[dict[str, Any]]) -> str: ) -def _attempt_table(attempts: list[dict[str, Any]], tasks: list[dict[str, Any]]) -> str: +def _attempt_table( + attempts: list[dict[str, Any]], + tasks: list[dict[str, Any]], + verified_causes: dict[str, dict[str, Any]] | None = None, +) -> str: + verified_causes = verified_causes or {} task_meta = {int(task["task_id"]): task for task in tasks} rows = [] for row in attempts: p = _public_attempt(row) result = "pass" if p["score"] == 1 else "fail" result_record = read_public_json(Path((row.get("paths") or {}).get("evaluation", ""))) - failed_names = { - item["name"] - for item in evaluator_summary(result_record) - if float(item.get("score") or 0) < 1 - } - failure = ( - "none" - if not failed_names - else "mixed" - if len(failed_names) > 1 - else "network" - if "NetworkEventEvaluator" in failed_names - else "response" - ) + failure, automatic_failure = _automatic_failure_type(result_record) + subcategory, reason = localized_cause(verified_causes, p["id"]) task = task_meta.get(p["task_id"], {}) values = [ Safe(f'{p["id"]}'), @@ -1292,13 +1309,16 @@ def _attempt_table(attempts: list[dict[str, Any]], tasks: list[dict[str, Any]]) _seconds(p["browser_tool_duration_ms"]), p["action_count"], _number(p["model_usage"]["total_tokens"]), + automatic_failure, + subcategory, + reason, ] cells = "".join(f"{x if isinstance(x, Safe) else _e(x)}" for x in values) rows.append( f'{cells}' + f'data-search="{_e(" ".join([p["id"], str(task.get("intent") or ""), automatic_failure, subcategory, reason, _failure_diagnosis(result_record, row)]))}">{cells}' ) headers = "".join( f"{_e(x)}" @@ -1312,6 +1332,9 @@ def _attempt_table(attempts: list[dict[str, Any]], tasks: list[dict[str, Any]]) "BrowserTool 累计耗时", "动作数", "Tokens", + "程序自动定位", + "核验根因子类", + "核验原因", ] ) return ( @@ -1321,8 +1344,12 @@ def _attempt_table(attempts: list[dict[str, Any]], tasks: list[dict[str, Any]]) def _engine_attempts( - engine: str, attempts: list[dict[str, Any]], tasks: list[dict[str, Any]] + engine: str, + attempts: list[dict[str, Any]], + tasks: list[dict[str, Any]], + verified_causes: dict[str, dict[str, Any]] | None = None, ) -> str: + verified_causes = verified_causes or {} meta = {int(task["task_id"]): task for task in tasks} rows = [] for row in attempts: @@ -1330,6 +1357,8 @@ def _engine_attempts( continue task = meta[int(row["task_id"])] result = read_public_json(Path((row.get("paths") or {}).get("evaluation", ""))) + automatic_failure = _automatic_failure_type(result)[1] + subcategory, reason = localized_cause(verified_causes, _attempt_id(row)) rows.append( [ Safe(f'{_attempt_id(row)}'), @@ -1339,7 +1368,9 @@ def _engine_attempts( _seconds(float(row.get("duration_ms") or 0)), _seconds(float(row.get("browser_tool_duration_ms") or 0)), int(row.get("action_count") or 0), - _failure_diagnosis(result, row) if float(row["score"]) < 1 else "—", + automatic_failure, + subcategory, + reason, ] ) return _table( @@ -1351,7 +1382,9 @@ def _engine_attempts( "Agent 链路耗时", "BrowserTool 耗时", "动作数", - "失败原因", + "程序自动定位", + "核验根因子类", + "核验原因", ], rows, table_id="engine-result-table", @@ -1447,6 +1480,23 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: protocol, environment = manifest.get("protocol") or {}, manifest.get("environment") or {} attempts = _logical_results(source_attempts, protocol) public_attempts = [_public_attempt(r) for r in attempts] + attempts_sha256 = _sha256(evidence / "attempts.jsonl") + failed_result_ids = { + row["id"] for row in public_attempts if float(row.get("score") or 0) < 1 + } + failure_cause_review = load_failure_cause_review() + verified_causes = validate_failure_cause_review( + failure_cause_review, + attempts_sha256=attempts_sha256, + failed_result_ids=failed_result_ids, + require_complete=False, + ) + for source, public in zip(attempts, public_attempts, strict=True): + evaluation = read_public_json(Path((source.get("paths") or {}).get("evaluation", ""))) + public["automatic_failure_type"] = _automatic_failure_type(evaluation)[1] + subcategory, reason = localized_cause(verified_causes, public["id"]) + public["verified_failure_subcategory"] = subcategory + public["verified_failure_reason"] = reason official = (manifest.get("environment") or {}).get("official_evaluator") or {} tasks = _resolve_tasks( selection, expected_data_checksum=str(official.get("data_checksum") or "") or None @@ -1816,7 +1866,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: analysis += _section( "60 个逻辑结果", "

    默认只显示失败;可按浏览器、层级、站点、任务类型、失败环节和全文关键词筛选。

    " - + _attempt_table(attempts, tasks), + + _attempt_table(attempts, tasks, verified_causes), ) (stage / "analysis/index.html").write_text( _page(title="结果分析", active="analysis", body=analysis, depth=1), encoding="utf-8" @@ -1881,16 +1931,14 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: "任务要求与验收结果", _verification_html(task, evaluation_record, response_record) + ( - '
    根因:' - + _e(_finding_text(SHARED_FAILURE_DIAGNOSES[public["task_id"]], "root_cause")) - + " 根治措施:" - + _e( - _finding_text( - SHARED_FAILURE_DIAGNOSES[public["task_id"]], "corrective_action" - ) - ) + '
    程序自动定位:' + + _e(public["automatic_failure_type"]) + + " 核验根因子类:" + + _e(public["verified_failure_subcategory"]) + + "
    核验原因:" + + _e(public["verified_failure_reason"]) + "
    " - if public["task_id"] in SHARED_FAILURE_DIAGNOSES + if float(public["score"]) < 1 else "" ) + '

    所有规则均为官方程序化验收,必须全部得分 1.0。页面先给出可读结论;官方期望、实际观察和断言保留在折叠技术明细及下载文件中。

    ', @@ -2044,7 +2092,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: body += _section( "全部逻辑结果", "

    每题只展示一个最终逻辑结果;端到端耗时与模型用量包含获得该结果所需的全部执行。

    " - + _engine_attempts(engine, attempts, tasks), + + _engine_attempts(engine, attempts, tasks, verified_causes), ) (stage / f"engines/{engine}.html").write_text( _page(title=ENGINE_LABELS[engine], active="adoption", body=body, depth=1), @@ -2120,7 +2168,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) reproduce += _section( "公开复现入口", - '

    lexmount/browser-eval 是代码、题集和锁文件的唯一来源。

    ./examples/browser_use/run.sh\nmake browser-use-review EVIDENCE="$BROWSER_USE_EVIDENCE"\nmake browser-use-report EVIDENCE="$BROWSER_USE_EVIDENCE"\nmake browser-use-report-check EVIDENCE="$BROWSER_USE_EVIDENCE"

    每个逻辑结果的脱敏回答、evaluator、事件和轨迹位于 artifacts/<result-id>/;evidence-index.json 给出全部 60 个逻辑结果的页面与下载路径。

    ', + '

    lexmount/browser-eval 是代码、题集和锁文件的唯一来源。

    ./examples/browser_use/run.sh\nmake browser-use-review EVIDENCE="$BROWSER_USE_EVIDENCE"\nmake browser-use-report EVIDENCE="$BROWSER_USE_EVIDENCE"\nmake browser-use-report-check EVIDENCE="$BROWSER_USE_EVIDENCE"

    每个逻辑结果的脱敏回答、evaluator、事件和轨迹位于 artifacts/<result-id>/;evidence-index.json 给出全部 60 个逻辑结果的页面与下载路径。

    ', ) (stage / "reproduce/index.html").write_text( _page(title="环境与复现", active="reproduce", body=reproduce, depth=1), encoding="utf-8" @@ -2171,6 +2219,15 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: "".join(json.dumps(row, ensure_ascii=False) + "\n" for row in public_attempts), encoding="utf-8", ) + _write_json( + stage / "data/failure-cause-review.json", + { + "schema": failure_cause_review.get("schema"), + "bindings": failure_cause_review.get("bindings") or {}, + "review_protocol": failure_cause_review.get("review_protocol") or {}, + "results": verified_causes, + }, + ) _write_json( stage / "data/evidence-index.json", { @@ -2204,9 +2261,18 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: "paired_summary": paired, "selection_sha256": selection.sha256, "evidence_bindings": { - "attempts_sha256": _sha256(evidence / "attempts.jsonl"), + "attempts_sha256": attempts_sha256, "manifest_sha256": _sha256(evidence / "manifest.json"), }, + "failure_cause_review": { + "status": "complete" + if set(verified_causes) == failed_result_ids + else "incomplete", + "reviewed_failures": len(verified_causes), + "failed_results": len(failed_result_ids), + "bindings": failure_cause_review.get("bindings") or {}, + "source_sha256": file_sha256(FAILURE_CAUSE_REVIEW_PATH), + }, "review": { "coverage": review.get("coverage"), "disagreement_count": len(review.get("disagreements") or []), diff --git a/harness/browser_use/report_i18n.py b/harness/browser_use/report_i18n.py index 8c9f49cae..651516d05 100644 --- a/harness/browser_use/report_i18n.py +++ b/harness/browser_use/report_i18n.py @@ -428,6 +428,17 @@ if isinstance(_localized, dict) and _localized.get("zh") and _localized.get("en"): TEXT[str(_localized["zh"])] = str(_localized["en"]) +_FAILURE_CAUSE_REVIEW = json.loads( + (Path(__file__).parents[2] / "research/browser-use-failure-cause-review.json").read_text( + encoding="utf-8" + ) +) +for _cause in _FAILURE_CAUSE_REVIEW["results"].values(): + for _field in ("subcategory", "reason"): + _localized = _cause.get(_field) + if isinstance(_localized, dict) and _localized.get("zh") and _localized.get("en"): + TEXT[str(_localized["zh"])] = str(_localized["en"]) + # Browser Use-specific prose is intentionally translated here rather than in # the shared Web Fetch catalog. Task intent and measured evidence are not # listed: unmatched task text stays verbatim, and Localizer preserves pre/code @@ -766,6 +777,12 @@ "Agent 耗时": "Agent latency", "BrowserTool 耗时": "BrowserTool latency", "失败原因": "Failure reason", + "程序自动定位": "Automatic failure locator", + "核验根因子类": "Verified root-cause subtype", + "核验原因": "Verified cause", + "最终回答验收失败": "Final-answer acceptance failure", + "网络副作用验收失败": "Network-side-effect acceptance failure", + "多项验收失败": "Multiple acceptance failures", "60 个任务×浏览器结果均保留最终回答、官方 evaluator 结果、Agent 事件和证据哈希。": "All 60 task-browser results retain the final answer, official evaluator result, agent events, and evidence hashes.", "60/60 个逻辑结果已覆盖,0 个未决。它不重新判题,也不是独立语义复核。": "Evidence-integrity checks cover all 60 logical results with no unresolved integrity issue. They do not rescore tasks or constitute independent semantic review.", "60/60 个逻辑结果完成证据完整性审计": "Evidence integrity checked for all 60 logical results", diff --git a/research/browser-use-failure-cause-review.json b/research/browser-use-failure-cause-review.json new file mode 100644 index 000000000..aad993fc0 --- /dev/null +++ b/research/browser-use-failure-cause-review.json @@ -0,0 +1,64 @@ +{ + "schema": "lex_browser_use_failure_cause_review/1", + "benchmark": "WebArena-Verified 1.2.3", + "bindings": { + "attempts_sha256": "517b613adcc40fc0ebe41af2ab2115d76f49faaba5378b426cc196b5b633e9c7" + }, + "review_protocol": { + "unit": "one score-bearing logical result per browser and task", + "method": "manual root-cause verification against the official task contract, score-bearing final response, evaluator assertions, network evidence, and autonomous Agent event trajectory", + "rule": "automatic evaluator classification remains available; root-cause subcategory and reason remain empty until status is verified" + }, + "results": { + "chromium-68": { + "status": "verified", + "subcategory": {"code": "benchmark_contract", "zh": "评测合同缺陷", "en": "Benchmark contract defect"}, + "reason": {"zh": "官方 expected 要求第二条图书记录的 author 为 null,但同题公开 results_schema 规定 author 只能是 string;官方答案与自身输出合同冲突。", "en": "The official expected value requires a null author for the second book record, while the task's published results_schema permits only a string; the official answer conflicts with its own output contract."} + }, + "moli-68": { + "status": "verified", + "subcategory": {"code": "benchmark_contract", "zh": "评测合同缺陷", "en": "Benchmark contract defect"}, + "reason": {"zh": "官方 expected 要求第二条图书记录的 author 为 null,但同题公开 results_schema 规定 author 只能是 string;官方答案与自身输出合同冲突。", "en": "The official expected value requires a null author for the second book record, while the task's published results_schema permits only a string; the official answer conflicts with its own output contract."} + }, + "chromium-124": { + "status": "verified", + "subcategory": {"code": "agent_strategy", "zh": "Agent 策略缺陷", "en": "Agent strategy defect"}, + "reason": {"zh": "Agent 直接对 Magento 全文搜索结果取价格极值,没有逐项确认商品是否属于“无线耳机”;最终把投影仪 17774.32 当成目标品类最高价。", "en": "The Agent took a price extreme directly from Magento full-text search results without verifying that each item was a wireless earphone; it returned a 17774.32 projector price as the category maximum."} + }, + "moli-124": { + "status": "verified", + "subcategory": {"code": "agent_strategy", "zh": "Agent 策略缺陷", "en": "Agent strategy defect"}, + "reason": {"zh": "Agent 直接对 Magento 全文搜索结果取价格极值,没有逐项确认商品是否属于“无线耳机”;最终把投影仪 17774.32 当成目标品类最高价。", "en": "The Agent took a price extreme directly from Magento full-text search results without verifying that each item was a wireless earphone; it returned a 17774.32 projector price as the category maximum."} + }, + "chromium-142": { + "status": "verified", + "subcategory": {"code": "benchmark_acceptance", "zh": "验收边界缺失", "en": "Undefined acceptance boundary"}, + "reason": {"zh": "任务未给出“hair care and hair style”的可观察归类规则。Agent 按商品名纳入 11.99 的 Crystal Headband,官方答案却排除该商品,现有页面和合同无法推出这一边界。", "en": "The task gives no observable membership rule for 'hair care and hair style'. The Agent included the 11.99 Crystal Headband by product name, while the official answer excludes it; the boundary cannot be derived from the page or contract."} + }, + "moli-142": { + "status": "verified", + "subcategory": {"code": "benchmark_acceptance", "zh": "验收边界缺失", "en": "Undefined acceptance boundary"}, + "reason": {"zh": "任务未给出“hair care and hair style”的可观察归类规则。Agent 按商品名纳入 11.99 的 Crystal Headband,官方答案却排除该商品,现有页面和合同无法推出这一边界。", "en": "The task gives no observable membership rule for 'hair care and hair style'. The Agent included the 11.99 Crystal Headband by product name, while the official answer excludes it; the boundary cannot be derived from the page or contract."} + }, + "chromium-168": { + "status": "verified", + "subcategory": {"code": "harness_protocol", "zh": "Agent 适配协议缺陷", "en": "Agent adapter protocol defect"}, + "reason": {"zh": "站点中没有符合条件的项目,但本项目系统协议错误地要求所有 RETRIEVE 返回列表,且未规定零结果必须映射为 NOT_FOUND_ERROR;Agent 因而输出 SUCCESS 与空列表。", "en": "No matching project existed, but this project's system protocol incorrectly required every RETRIEVE result to be a list and did not map zero results to NOT_FOUND_ERROR; the Agent therefore emitted SUCCESS with an empty list."} + }, + "moli-168": { + "status": "verified", + "subcategory": {"code": "harness_protocol", "zh": "Agent 适配协议缺陷", "en": "Agent adapter protocol defect"}, + "reason": {"zh": "站点中没有符合条件的项目,但本项目系统协议错误地要求所有 RETRIEVE 返回列表,且未规定零结果必须映射为 NOT_FOUND_ERROR;Agent 因而输出 SUCCESS 与空列表。", "en": "No matching project existed, but this project's system protocol incorrectly required every RETRIEVE result to be a list and did not map zero results to NOT_FOUND_ERROR; the Agent therefore emitted SUCCESS with an empty list."} + }, + "chromium-506": { + "status": "verified", + "subcategory": {"code": "evidence_capture", "zh": "证据采集缺陷", "en": "Evidence-capture defect"}, + "reason": {"zh": "结账实际完成,成功页和订单号已出现,response、商品、付款及成功页规则均通过;只有购物车总额规则失败,因为 HAR 到运行结束才读取付款响应体,此时 CDP 已淘汰该 body。该零分是采集假阴性。", "en": "Checkout completed and the success page and order number appeared; response, product, payment, and success-page checks passed. Only the cart-total rule failed because the HAR collector fetched the payment response body at run end after CDP had evicted it. This zero score is a collection false negative."} + }, + "moli-506": { + "status": "verified", + "subcategory": {"code": "completion_budget", "zh": "完成确认失败", "en": "Completion-confirmation failure"}, + "reason": {"zh": "第 30 个决策回合才发出 Place order 点击,之后没有剩余回合观察付款结果;证据中没有付款或成功事件,但超限收尾仍生成成功形态回答,导致任务未被确认完成。", "en": "The Agent issued the Place order click only on decision turn 30, leaving no turn to observe the payment result. The evidence contains no payment or success event, yet max-step finalization emitted a success-shaped answer, so completion was never confirmed."} + } + } +} diff --git a/research/report-changes.json b/research/report-changes.json index 8eb89031c..631889531 100644 --- a/research/report-changes.json +++ b/research/report-changes.json @@ -7,9 +7,9 @@ "date": "2026-09-10", "title": "失败根因、修复与本机扩展核验", "status": "已处理", - "scope": "五个零分任务的根因、修复代码、结果详情信息层级、Colima 网络身份和 Mac 环境扩展范围。", + "scope": "十个失败逻辑结果的自动定位、逐结果根因核验、修复代码、结果详情信息层级、Colima 网络身份和 Mac 环境扩展范围。", "findings": "相同官方分数掩盖了不同根因:Task 68 的官方 expected 与 schema 冲突;124 缺少候选语义核验;142 的类别验收边界未定义;168 的本项目响应协议与官方 null 合同冲突;506 中 Chromium 为 HAR 采集误判,Moli 为步数耗尽且未确认完成。", - "corrections": "新增运行前合同一致性校验、候选语义与提交前检查、空结果状态协议、JSON 响应体即时 HAR 采集、末步工具调用 fail-closed 和可配置步数上限;报告按根因、责任层、证据、根治措施和复测门槛解释结果。历史分数不改写。", + "corrections": "保留 evaluator 的程序自动定位;新增按浏览器与任务记录、绑定 attempts 哈希的根因子类和原因核验,未核验字段保持为空,正式发布要求全部失败结果完成核验。另新增运行前合同一致性校验、候选语义与提交前检查、空结果状态协议、JSON 响应体即时 HAR 采集、末步工具调用 fail-closed 和可配置步数上限。历史分数不改写。", "remaining": "修复后的 #124、#168、#506 尚需形成新执行证据;#68 与 #142 退出下一轮干净 A/B,直至合同被修复或验收口径可观察。Wikipedia 与 Map 尚未在本机实跑。" }, { @@ -68,7 +68,7 @@ "id": "2026-09-10-shared-failure-diagnosis-and-mac-expansion", "date": "2026-09-10", "title": "追到失败根因并落实运行器修复", - "summary": "五个零分已追到可操作根因:两项任务合同问题、两项 Agent 协议或策略问题,以及 Task 506 中 Chromium 与 Moli 各不相同的采集和完成问题。运行器已修复合同准入、响应协议、候选核验、HAR 采集与超限处理;历史官方成绩仍为 Chromium 25/30、Moli 25/30,修复效果等待新执行验证。", + "summary": "十个失败逻辑结果已完成逐项根因核验:两项任务合同问题、两项 Agent 协议或策略问题,以及 Task 506 中 Chromium 与 Moli 各不相同的采集和完成问题。自动 evaluator 定位与核验根因分层展示;核验记录绑定冻结证据哈希。运行器已修复合同准入、响应协议、候选核验、HAR 采集与超限处理;历史官方成绩仍为 Chromium 25/30、Moli 25/30,修复效果等待新执行验证。", "attempts_sha256": "517b613adcc40fc0ebe41af2ab2115d76f49faaba5378b426cc196b5b633e9c7", "selection_sha256": "ba3c344df003dd4eba3a5d1ef97162ee98df2ed728f7c29c4188bc4247982d38", "changes": [ @@ -84,6 +84,12 @@ "结合官方 expected/schema、最终回答、evaluator、HAR 与逐步事件,定位四类根因;Task 506 分浏览器分别归因。", "不推断模型隐藏思维;根因由可复核证据闭环支持,官方历史分数不变。" ], + [ + "失败原因核验", + "程序化 evaluator 能定位未通过的验收类型,但不能单独证明产生失败的根因。", + "按浏览器与任务保存根因子类和具体原因,并绑定当前 attempts 哈希;未核验时两个字段为空,完整发布要求覆盖全部失败逻辑结果。", + "本版 10/10 个失败逻辑结果已核验;Task 506 分浏览器记录不同根因,不改变官方分数。" + ], [ "运行器根治", "错误合同会消耗正式调用;空结果协议、搜索候选聚合、HAR 延迟取体和超限总结会制造可避免的失败或误判。", diff --git a/scripts/check_browser_use_report.py b/scripts/check_browser_use_report.py index a0ec18ed2..5b3a177e2 100644 --- a/scripts/check_browser_use_report.py +++ b/scripts/check_browser_use_report.py @@ -9,6 +9,12 @@ import re from pathlib import Path +from harness.browser_use.failure_cause_review import ( + DEFAULT_REVIEW_PATH as FAILURE_CAUSE_REVIEW_PATH, + load_failure_cause_review, + sha256 as file_sha256, + validate_failure_cause_review, +) from harness.browser_use.report import _logical_results from harness.browser_use.selection import load_pilot @@ -88,6 +94,7 @@ def check( "review.json", "publication-privacy.json", "evidence-index.json", + "failure-cause-review.json", ) ) artifact_files = tuple( @@ -382,6 +389,39 @@ def check( } if reviewed_keys != set(by_key): raise ValueError("result review attempt inventory differs from measured evidence") + failed_result_ids = { + f"{row['engine']}-{int(row['task_id'])}" + for row in logical_rows + if float(row.get("score") or 0) < 1 + } + cause_review = load_failure_cause_review() + verified_causes = validate_failure_cause_review( + cause_review, + attempts_sha256=_sha256(attempts_path), + failed_result_ids=failed_result_ids, + require_complete=bool( + (profile.get("failure_cause_review") or {}).get( + "required_for_publication" + ) + ), + ) + cause_profile = profile.get("failure_cause_review") or {} + if cause_profile.get("source_sha256") != file_sha256(FAILURE_CAUSE_REVIEW_PATH): + raise ValueError("current report profile is not bound to the failure-cause review") + if set(verified_causes) != failed_result_ids: + raise ValueError("every failed logical result must have a verified root cause") + published_cause_review = json.loads( + (report_dir / "data/failure-cause-review.json").read_text(encoding="utf-8") + ) + if published_cause_review.get("results") != verified_causes: + raise ValueError("published failure causes differ from the verified review") + public_by_id = {str(item["id"]): item for item in public_results} + for result_id, item in public_by_id.items(): + has_verified_cause = result_id in verified_causes + if bool(item.get("verified_failure_subcategory")) != has_verified_cause: + raise ValueError(f"failure subcategory review state mismatch: {result_id}") + if bool(item.get("verified_failure_reason")) != has_verified_cause: + raise ValueError(f"failure reason review state mismatch: {result_id}") report_review = audit.get("review") or {} if report_review.get("bindings") != bindings: raise ValueError("rendered report is not bound to the current result review") diff --git a/tests/browser_use/test_failure_cause_review.py b/tests/browser_use/test_failure_cause_review.py new file mode 100644 index 000000000..27535823f --- /dev/null +++ b/tests/browser_use/test_failure_cause_review.py @@ -0,0 +1,102 @@ +import pytest + +from harness.browser_use.failure_cause_review import ( + localized_cause, + validate_failure_cause_review, +) + + +def _review(status: str = "verified") -> dict: + cause = { + "status": status, + "subcategory": { + "code": "agent_strategy", + "zh": "Agent 策略缺陷", + "en": "Agent strategy defect", + }, + "reason": {"zh": "候选未核验。", "en": "Candidates were not verified."}, + } + if status != "verified": + cause = {"status": status} + return { + "schema": "lex_browser_use_failure_cause_review/1", + "bindings": {"attempts_sha256": "attempts-hash"}, + "results": {"chromium-124": cause}, + } + + +def test_unverified_cause_fields_remain_blank() -> None: + verified = validate_failure_cause_review( + _review("pending"), + attempts_sha256="attempts-hash", + failed_result_ids={"chromium-124"}, + require_complete=False, + ) + + assert localized_cause(verified, "chromium-124") == ("", "") + + +def test_verified_cause_is_available_per_browser_and_task() -> None: + verified = validate_failure_cause_review( + _review(), + attempts_sha256="attempts-hash", + failed_result_ids={"chromium-124"}, + require_complete=True, + ) + + assert localized_cause(verified, "chromium-124") == ( + "Agent 策略缺陷", + "候选未核验。", + ) + assert localized_cause(verified, "moli-124") == ("", "") + + +def test_stale_review_is_blank_for_draft_and_blocks_release() -> None: + assert ( + validate_failure_cause_review( + _review(), + attempts_sha256="different-hash", + failed_result_ids={"chromium-124"}, + require_complete=False, + ) + == {} + ) + with pytest.raises(ValueError, match="not bound"): + validate_failure_cause_review( + _review(), + attempts_sha256="different-hash", + failed_result_ids={"chromium-124"}, + require_complete=True, + ) + + +def test_release_requires_every_failed_result_and_rejects_passes() -> None: + with pytest.raises(ValueError, match="incomplete"): + validate_failure_cause_review( + _review(), + attempts_sha256="attempts-hash", + failed_result_ids={"chromium-124", "moli-124"}, + require_complete=True, + ) + with pytest.raises(ValueError, match="non-failed"): + validate_failure_cause_review( + _review(), + attempts_sha256="attempts-hash", + failed_result_ids=set(), + require_complete=False, + ) + + +def test_unverified_record_cannot_carry_diagnosis_text() -> None: + review = _review("pending") + review["results"]["chromium-124"]["reason"] = { + "zh": "尚未证明。", + "en": "Unproven.", + } + with pytest.raises(ValueError, match="must not contain"): + validate_failure_cause_review( + review, + attempts_sha256="attempts-hash", + failed_result_ids={"chromium-124"}, + require_complete=False, + ) From 98f18ed290a1b02fe3c98c7903305a19c7351934 Mon Sep 17 00:00:00 2001 From: kevin Date: Thu, 10 Sep 2026 22:42:48 +0800 Subject: [PATCH 045/127] bind verified browser use release --- evaluation/browser-use/current-report.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evaluation/browser-use/current-report.json b/evaluation/browser-use/current-report.json index 6b822a4b5..819eee4f7 100644 --- a/evaluation/browser-use/current-report.json +++ b/evaluation/browser-use/current-report.json @@ -36,7 +36,7 @@ "base_url": "https://artifact-hub.dev.lexmount.net", "site_slug": "28OH4YZkUYF1", "share_url": "https://artifact-hub.dev.lexmount.net/v/5Vhnhdc_f8FEM2RzctQm-ChRlXUf174Y", - "expected_version": "ver_4e371a7693c142859cbd28b4354cbfb8", + "expected_version": "ver_280ef9af92804d759ab35b24b521fd6f", "state": "verified" } } From c346b7f955b9776e78121bd2b92e4c83977e135d Mon Sep 17 00:00:00 2001 From: kevin Date: Thu, 10 Sep 2026 22:47:22 +0800 Subject: [PATCH 046/127] bind failure review provenance --- evaluation/browser-use/current-report.json | 2 +- harness/browser_use/failure_cause_review.py | 11 +++++++++++ harness/browser_use/report.py | 4 ++++ research/browser-use-failure-cause-review.json | 8 +++++++- scripts/check_browser_use_report.py | 5 +++++ tests/browser_use/test_failure_cause_review.py | 7 +++++++ 6 files changed, 35 insertions(+), 2 deletions(-) diff --git a/evaluation/browser-use/current-report.json b/evaluation/browser-use/current-report.json index 819eee4f7..49da9a16f 100644 --- a/evaluation/browser-use/current-report.json +++ b/evaluation/browser-use/current-report.json @@ -23,7 +23,7 @@ "independent_semantic_review": false, "failure_cause_review": { "required_for_publication": true, - "source_sha256": "91125db4f0fc5bed9424267505fa372b7cf44bb2fec3b742cd5f1ee9f2aefdd2" + "source_sha256": "fd955376a105e0d99a25452b7f45ed412dbaf2735a8d1975e6146915dbe0243c" }, "tasks_without_measured_success_path": [ 68, diff --git a/harness/browser_use/failure_cause_review.py b/harness/browser_use/failure_cause_review.py index 84e355c78..f37bc8eb3 100644 --- a/harness/browser_use/failure_cause_review.py +++ b/harness/browser_use/failure_cause_review.py @@ -65,6 +65,17 @@ def validate_failure_cause_review( if require_complete and set(verified) != failed_result_ids: missing = sorted(failed_result_ids - set(verified)) raise ValueError(f"failure-cause review is incomplete: {missing}") + if require_complete: + expected_coverage = { + "failed_results": len(failed_result_ids), + "verified_results": len(verified), + } + if review.get("coverage") != expected_coverage: + raise ValueError("failure-cause review coverage is inconsistent") + if review.get("unresolved"): + raise ValueError("failure-cause review contains unresolved items") + if not review.get("reviewer_class") or not review.get("reviewed_at"): + raise ValueError("failure-cause review must identify its reviewer class and date") return verified diff --git a/harness/browser_use/report.py b/harness/browser_use/report.py index 27ba82d8f..82d29a4da 100644 --- a/harness/browser_use/report.py +++ b/harness/browser_use/report.py @@ -2224,6 +2224,10 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: { "schema": failure_cause_review.get("schema"), "bindings": failure_cause_review.get("bindings") or {}, + "coverage": failure_cause_review.get("coverage") or {}, + "unresolved": failure_cause_review.get("unresolved") or [], + "reviewed_at": failure_cause_review.get("reviewed_at"), + "reviewer_class": failure_cause_review.get("reviewer_class"), "review_protocol": failure_cause_review.get("review_protocol") or {}, "results": verified_causes, }, diff --git a/research/browser-use-failure-cause-review.json b/research/browser-use-failure-cause-review.json index aad993fc0..910103331 100644 --- a/research/browser-use-failure-cause-review.json +++ b/research/browser-use-failure-cause-review.json @@ -1,14 +1,20 @@ { "schema": "lex_browser_use_failure_cause_review/1", "benchmark": "WebArena-Verified 1.2.3", + "reviewed_at": "2026-09-10", + "reviewer_class": "evidence analyst", "bindings": { - "attempts_sha256": "517b613adcc40fc0ebe41af2ab2115d76f49faaba5378b426cc196b5b633e9c7" + "attempts_sha256": "517b613adcc40fc0ebe41af2ab2115d76f49faaba5378b426cc196b5b633e9c7", + "manifest_sha256": "23a747e6286e7b8c28c07abae83b343892f6376e9aae1dcc0f3ab4da98271cae", + "selection_sha256": "ba3c344df003dd4eba3a5d1ef97162ee98df2ed728f7c29c4188bc4247982d38" }, "review_protocol": { "unit": "one score-bearing logical result per browser and task", "method": "manual root-cause verification against the official task contract, score-bearing final response, evaluator assertions, network evidence, and autonomous Agent event trajectory", "rule": "automatic evaluator classification remains available; root-cause subcategory and reason remain empty until status is verified" }, + "coverage": {"failed_results": 10, "verified_results": 10}, + "unresolved": [], "results": { "chromium-68": { "status": "verified", diff --git a/scripts/check_browser_use_report.py b/scripts/check_browser_use_report.py index 5b3a177e2..1fdf825a3 100644 --- a/scripts/check_browser_use_report.py +++ b/scripts/check_browser_use_report.py @@ -410,6 +410,11 @@ def check( raise ValueError("current report profile is not bound to the failure-cause review") if set(verified_causes) != failed_result_ids: raise ValueError("every failed logical result must have a verified root cause") + cause_bindings = cause_review.get("bindings") or {} + if cause_bindings.get("manifest_sha256") != _sha256(manifest_path): + raise ValueError("failure-cause review is not bound to the current manifest") + if cause_bindings.get("selection_sha256") != selection.sha256: + raise ValueError("failure-cause review is not bound to the current selection") published_cause_review = json.loads( (report_dir / "data/failure-cause-review.json").read_text(encoding="utf-8") ) diff --git a/tests/browser_use/test_failure_cause_review.py b/tests/browser_use/test_failure_cause_review.py index 27535823f..8d886fd06 100644 --- a/tests/browser_use/test_failure_cause_review.py +++ b/tests/browser_use/test_failure_cause_review.py @@ -20,7 +20,14 @@ def _review(status: str = "verified") -> dict: cause = {"status": status} return { "schema": "lex_browser_use_failure_cause_review/1", + "reviewed_at": "2026-09-10", + "reviewer_class": "evidence analyst", "bindings": {"attempts_sha256": "attempts-hash"}, + "coverage": { + "failed_results": 1, + "verified_results": 1 if status == "verified" else 0, + }, + "unresolved": [], "results": {"chromium-124": cause}, } From 412633989659a9a3eafd2475625290c696bb1b43 Mon Sep 17 00:00:00 2001 From: kevin Date: Thu, 10 Sep 2026 22:48:07 +0800 Subject: [PATCH 047/127] bind final reviewed report version --- evaluation/browser-use/current-report.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evaluation/browser-use/current-report.json b/evaluation/browser-use/current-report.json index 49da9a16f..c4a7d20c5 100644 --- a/evaluation/browser-use/current-report.json +++ b/evaluation/browser-use/current-report.json @@ -36,7 +36,7 @@ "base_url": "https://artifact-hub.dev.lexmount.net", "site_slug": "28OH4YZkUYF1", "share_url": "https://artifact-hub.dev.lexmount.net/v/5Vhnhdc_f8FEM2RzctQm-ChRlXUf174Y", - "expected_version": "ver_280ef9af92804d759ab35b24b521fd6f", + "expected_version": "ver_0bb766584e944330995bb009c4562dc4", "state": "verified" } } From 7eb7fc006f21e5f4624a3d764b3d0689178ee608 Mon Sep 17 00:00:00 2001 From: kevin Date: Thu, 10 Sep 2026 22:55:12 +0800 Subject: [PATCH 048/127] honor draft failure review policy --- scripts/check_browser_use_report.py | 32 +++++++++++++++-------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/scripts/check_browser_use_report.py b/scripts/check_browser_use_report.py index 1fdf825a3..79000d426 100644 --- a/scripts/check_browser_use_report.py +++ b/scripts/check_browser_use_report.py @@ -395,26 +395,28 @@ def check( if float(row.get("score") or 0) < 1 } cause_review = load_failure_cause_review() + cause_profile = profile.get("failure_cause_review") or {} + cause_required = bool(cause_profile.get("required_for_publication")) verified_causes = validate_failure_cause_review( cause_review, attempts_sha256=_sha256(attempts_path), failed_result_ids=failed_result_ids, - require_complete=bool( - (profile.get("failure_cause_review") or {}).get( - "required_for_publication" - ) - ), + require_complete=cause_required, ) - cause_profile = profile.get("failure_cause_review") or {} - if cause_profile.get("source_sha256") != file_sha256(FAILURE_CAUSE_REVIEW_PATH): - raise ValueError("current report profile is not bound to the failure-cause review") - if set(verified_causes) != failed_result_ids: - raise ValueError("every failed logical result must have a verified root cause") - cause_bindings = cause_review.get("bindings") or {} - if cause_bindings.get("manifest_sha256") != _sha256(manifest_path): - raise ValueError("failure-cause review is not bound to the current manifest") - if cause_bindings.get("selection_sha256") != selection.sha256: - raise ValueError("failure-cause review is not bound to the current selection") + if cause_required: + if cause_profile.get("source_sha256") != file_sha256( + FAILURE_CAUSE_REVIEW_PATH + ): + raise ValueError( + "current report profile is not bound to the failure-cause review" + ) + if set(verified_causes) != failed_result_ids: + raise ValueError("every failed logical result must have a verified root cause") + cause_bindings = cause_review.get("bindings") or {} + if cause_bindings.get("manifest_sha256") != _sha256(manifest_path): + raise ValueError("failure-cause review is not bound to the current manifest") + if cause_bindings.get("selection_sha256") != selection.sha256: + raise ValueError("failure-cause review is not bound to the current selection") published_cause_review = json.loads( (report_dir / "data/failure-cause-review.json").read_text(encoding="utf-8") ) From 31780a9f1df0b242d4581598142b61779ff531db Mon Sep 17 00:00:00 2001 From: kevin Date: Thu, 10 Sep 2026 23:06:28 +0800 Subject: [PATCH 049/127] resolve remaining browser use review findings --- harness/browser_use/report.py | 2 +- harness/browser_use/runner.py | 2 +- harness/browser_use/selection.py | 40 ++++++++++++++++++++++++++--- tests/browser_use/test_runner.py | 18 +++++++++++-- tests/browser_use/test_selection.py | 15 +++++++++++ 5 files changed, 69 insertions(+), 8 deletions(-) diff --git a/harness/browser_use/report.py b/harness/browser_use/report.py index 82d29a4da..dff5bd94a 100644 --- a/harness/browser_use/report.py +++ b/harness/browser_use/report.py @@ -63,7 +63,7 @@ def _finding_text(diagnosis: dict[str, Any], field: str) -> str: - value = diagnosis[field] + value = diagnosis.get(field, {}) return str(value.get("zh") or value.get("en") or "") if isinstance(value, dict) else str(value) diff --git a/harness/browser_use/runner.py b/harness/browser_use/runner.py index 10c8c7efc..44bd9f053 100644 --- a/harness/browser_use/runner.py +++ b/harness/browser_use/runner.py @@ -460,7 +460,7 @@ def _exhausted_agent_steps(events: list[dict[str, Any]], max_steps: int) -> bool if not turns: return False data = turns[-1].get("data") or {} - return int(data.get("turn") or 0) >= max_steps and bool(data.get("has_tool_calls")) + return len(turns) >= max_steps and bool(data.get("has_tool_calls")) def _max_steps_response(task_type: str, max_steps: int) -> dict[str, Any]: diff --git a/harness/browser_use/selection.py b/harness/browser_use/selection.py index 7d81041a4..5ec004458 100644 --- a/harness/browser_use/selection.py +++ b/harness/browser_use/selection.py @@ -5,6 +5,7 @@ import hashlib import json from dataclasses import dataclass +from functools import lru_cache from importlib.resources import files from pathlib import Path from typing import Any @@ -54,9 +55,33 @@ def _json_equal(left: Any, right: Any) -> bool: def _validate_json_schema(value: Any, schema: Any, *, path: str) -> None: - """Validate the JSON Schema subset used by official response contracts.""" + """Validate the JSON Schema subset used by official response contracts. + + The current official dataset uses structural keywords plus ``format`` as a + normalization hint. Unknown validation keywords fail closed so a future + contract cannot silently bypass this preflight. + """ if not isinstance(schema, dict): raise ValueError(f"{path}: schema must be an object") + supported = { + "anyOf", + "oneOf", + "const", + "enum", + "type", + "required", + "properties", + "items", + "format", + "title", + } + unsupported = sorted(set(schema) - supported) + if unsupported: + raise ValueError(f"{path}: unsupported schema keyword {unsupported[0]!r}") + if "format" in schema and not isinstance(schema["format"], str): + raise ValueError(f"{path}: format must be a string") + if "title" in schema and not isinstance(schema["title"], str): + raise ValueError(f"{path}: title must be a string") for keyword in ("anyOf", "oneOf"): if keyword not in schema: @@ -162,12 +187,19 @@ def _validate_agent_response_contract(task_id: int, task: Any) -> None: ) from exc -def _task_exclusion(task_id: int) -> dict[str, str] | None: +@lru_cache(maxsize=1) +def _task_exclusions() -> dict[str, dict[str, str]]: payload = json.loads(TASK_EXCLUSIONS_PATH.read_text(encoding="utf-8")) if payload.get("schema") != "lex_browser_use_task_exclusions/1": raise ValueError("unsupported Browser Use task-exclusion schema") - item = (payload.get("tasks") or {}).get(str(task_id)) - return {str(key): str(value) for key, value in item.items()} if item else None + return { + str(task_id): {str(key): str(value) for key, value in item.items()} + for task_id, item in (payload.get("tasks") or {}).items() + } + + +def _task_exclusion(task_id: int) -> dict[str, str] | None: + return _task_exclusions().get(str(task_id)) def load_pilot(path: str | Path) -> PilotSelection: diff --git a/tests/browser_use/test_runner.py b/tests/browser_use/test_runner.py index 63f0d79de..eaad1786a 100644 --- a/tests/browser_use/test_runner.py +++ b/tests/browser_use/test_runner.py @@ -56,10 +56,24 @@ def test_system_contract_fails_empty_retrieve_as_not_found() -> None: def test_agent_step_exhaustion_requires_a_final_tool_call() -> None: - exhausted = [{"type": "turn_end", "data": {"turn": 40, "has_tool_calls": True}}] - completed = [{"type": "turn_end", "data": {"turn": 40, "has_tool_calls": False}}] + exhausted = [ + {"type": "turn_end", "data": {"turn": turn, "has_tool_calls": True}} + for turn in range(1, 41) + ] + zero_indexed = [ + {"type": "turn_end", "data": {"turn": turn, "has_tool_calls": True}} + for turn in range(40) + ] + completed = [ + { + "type": "turn_end", + "data": {"turn": turn, "has_tool_calls": turn != 40}, + } + for turn in range(1, 41) + ] assert _exhausted_agent_steps(exhausted, 40) is True + assert _exhausted_agent_steps(zero_indexed, 40) is True assert _exhausted_agent_steps(completed, 40) is False assert _max_steps_response("retrieve", 40) == { "task_type": "RETRIEVE", diff --git a/tests/browser_use/test_selection.py b/tests/browser_use/test_selection.py index f6ba1c686..902ab5f4e 100644 --- a/tests/browser_use/test_selection.py +++ b/tests/browser_use/test_selection.py @@ -100,6 +100,21 @@ def test_json_schema_validates_nested_contract_keywords() -> None: ) +def test_json_schema_treats_official_format_as_hint_and_rejects_unknown_keywords() -> None: + _validate_json_schema( + "2026-09-10", + {"type": "string", "format": "date", "title": "Arrival date"}, + path="expected.retrieved_data", + ) + for keyword in ("allOf", "$ref", "pattern", "minimum"): + with pytest.raises(ValueError, match="unsupported schema keyword"): + _validate_json_schema( + "value", + {"type": "string", keyword: []}, + path="expected.retrieved_data", + ) + + def test_official_contract_validation_rejects_task_68_null_author() -> None: evaluator = SimpleNamespace( evaluator="AgentResponseEvaluator", From 51a37526a2819655cd43b45a96c1a1c94ca77cc5 Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 11 Sep 2026 00:41:04 +0800 Subject: [PATCH 050/127] expand browser use cohort to 90 tasks --- evaluation/browser-use/current-report.json | 17 ++-- .../browser-use/webarena-verified-pilot.json | 78 +++++++++++++++++-- examples/browser_use/README.md | 12 +-- harness/browser_use/selection.py | 29 ++++++- tests/browser_use/test_runner.py | 2 +- tests/browser_use/test_selection.py | 4 +- 6 files changed, 113 insertions(+), 29 deletions(-) diff --git a/evaluation/browser-use/current-report.json b/evaluation/browser-use/current-report.json index c4a7d20c5..c9f355bbf 100644 --- a/evaluation/browser-use/current-report.json +++ b/evaluation/browser-use/current-report.json @@ -2,8 +2,8 @@ "schema": "lex_browser_use_current_report/1", "benchmark": "WebArena-Verified", "benchmark_version": "1.2.3", - "task_count": 30, - "selection_sha256": "ba3c344df003dd4eba3a5d1ef97162ee98df2ed728f7c29c4188bc4247982d38", + "task_count": 90, + "selection_sha256": "2cb716552e36df7535e83ed6d2cf3f84283e3aca86072817cd6e039a38547c7b", "official_data_checksum": "d65275660814663375028e9017e1f929e3c38321041b125795e2713b52243d30", "engines": [ "chromium", @@ -15,6 +15,7 @@ }, "model": "gpt-5.6-sol", "reasoning_effort": "low", + "max_agent_steps": 40, "max_task_executions": 2, "logical_result": "score authority follows the frozen run manifest; aggregate all execution latency and cost", "model_transport_retries": 1, @@ -23,20 +24,14 @@ "independent_semantic_review": false, "failure_cause_review": { "required_for_publication": true, - "source_sha256": "fd955376a105e0d99a25452b7f45ed412dbaf2735a8d1975e6146915dbe0243c" + "source_sha256": null }, - "tasks_without_measured_success_path": [ - 68, - 124, - 142, - 168, - 506 - ], + "tasks_without_measured_success_path": [], "publication": { "base_url": "https://artifact-hub.dev.lexmount.net", "site_slug": "28OH4YZkUYF1", "share_url": "https://artifact-hub.dev.lexmount.net/v/5Vhnhdc_f8FEM2RzctQm-ChRlXUf174Y", "expected_version": "ver_0bb766584e944330995bb009c4562dc4", - "state": "verified" + "state": "pending-90-task-evidence" } } diff --git a/evaluation/browser-use/webarena-verified-pilot.json b/evaluation/browser-use/webarena-verified-pilot.json index b3f14dd93..c8cc361fb 100644 --- a/evaluation/browser-use/webarena-verified-pilot.json +++ b/evaluation/browser-use/webarena-verified-pilot.json @@ -4,10 +4,14 @@ "benchmark": "WebArena-Verified", "benchmark_version": "1.2.3", "difficulty_definition": "estimated minimum visible UI operations after authentication: easy 1-4, medium 5-7, hard 8+", - "selection_policy": "pre-registered before model execution; four self-hosted sites only; covers retrieve, navigate, and mutate", + "selection_policy": "pre-registered before the 90-task run; provider- and result-blind; single-site tasks from the four locally verified self-hosted sites; known invalid or unobservable acceptance contracts excluded; broad intent-template and retrieve, navigate, mutate coverage", + "tier_counts": {"easy": 15, "medium": 30, "hard": 45}, + "site_counts": {"shopping": 23, "shopping_admin": 23, "reddit": 22, "gitlab": 22}, + "task_type_counts": {"RETRIEVE": 37, "NAVIGATE": 13, "MUTATE": 40}, + "continuity": "retains 28 contract-valid tasks from the prior 30-task cohort, removes tasks 68 and 142 under the prospective exclusion registry, and adds 62 tasks without using prior browser outcomes", "excluded_sites": { - "wikipedia": "requires a large external ZIM data mount", - "map": "requires multiple external map datasets and persistent volumes" + "wikipedia": "requires a large external ZIM data mount and has not completed the local execution qualification gate", + "map": "requires multiple external map datasets and persistent volumes and has not completed the local execution qualification gate" } }, "tasks": [ @@ -16,6 +20,16 @@ {"task_id": 77, "tier": "easy", "estimated_min_steps": 3, "official_hard": false, "rationale": "打开评论列表并读取一种状态的数量"}, {"task_id": 94, "tier": "easy", "estimated_min_steps": 4, "official_hard": false, "rationale": "搜索一张发票并读取总金额"}, {"task_id": 404, "tier": "easy", "estimated_min_steps": 4, "official_hard": false, "rationale": "进入一个论坛、选择最新帖子并点赞"}, + {"task_id": 313, "tier": "easy", "estimated_min_steps": 2, "official_hard": false, "rationale": "从商城页面直接读取客服号码"}, + {"task_id": 274, "tier": "easy", "estimated_min_steps": 3, "official_hard": false, "rationale": "提交一个商品关键词并停留在搜索结果页"}, + {"task_id": 260, "tier": "easy", "estimated_min_steps": 2, "official_hard": false, "rationale": "从商城导航进入指定品类页"}, + {"task_id": 386, "tier": "easy", "estimated_min_steps": 4, "official_hard": false, "rationale": "搜索一个明确商品并读取评分"}, + {"task_id": 374, "tier": "easy", "estimated_min_steps": 3, "official_hard": false, "rationale": "在后台导航到指定主题设置页"}, + {"task_id": 157, "tier": "easy", "estimated_min_steps": 3, "official_hard": true, "rationale": "在后台打开全部客户详情视图"}, + {"task_id": 595, "tier": "easy", "estimated_min_steps": 4, "official_hard": false, "rationale": "从指定论坛热门帖子页完成订阅"}, + {"task_id": 645, "tier": "easy", "estimated_min_steps": 4, "official_hard": true, "rationale": "在已知论坛发布只有标题的新帖子"}, + {"task_id": 156, "tier": "easy", "estimated_min_steps": 2, "official_hard": true, "rationale": "打开分配给当前用户的合并请求页"}, + {"task_id": 258, "tier": "easy", "estimated_min_steps": 2, "official_hard": false, "rationale": "打开 GitLab 公共项目列表"}, {"task_id": 45, "tier": "medium", "estimated_min_steps": 5, "official_hard": false, "rationale": "进入项目议题列表并应用排序和状态筛选"}, {"task_id": 118, "tier": "medium", "estimated_min_steps": 5, "official_hard": false, "rationale": "把需求转成商品查询并打开匹配详情页"}, @@ -26,22 +40,72 @@ {"task_id": 183, "tier": "medium", "estimated_min_steps": 6, "official_hard": false, "rationale": "按库存量筛选后台表格并收集 SKU"}, {"task_id": 188, "tier": "medium", "estimated_min_steps": 6, "official_hard": false, "rationale": "按状态筛选订单,打开最新一单并读取总额"}, {"task_id": 198, "tier": "medium", "estimated_min_steps": 6, "official_hard": false, "rationale": "筛选并排序订单,再打开最新的匹配客户"}, - {"task_id": 399, "tier": "medium", "estimated_min_steps": 6, "official_hard": true, "rationale": "打开个人资料设置,修改简介并保存"}, + {"task_id": 399, "tier": "medium", "estimated_min_steps": 5, "official_hard": true, "rationale": "打开个人资料设置,修改简介并保存"}, + {"task_id": 225, "tier": "medium", "estimated_min_steps": 5, "official_hard": false, "rationale": "定位商品评论并提取低评分评论标题"}, + {"task_id": 231, "tier": "medium", "estimated_min_steps": 5, "official_hard": false, "rationale": "筛选订单状态并读取最近订单号"}, + {"task_id": 298, "tier": "medium", "estimated_min_steps": 6, "official_hard": false, "rationale": "筛选最近已完成订单并打开详情页"}, + {"task_id": 465, "tier": "medium", "estimated_min_steps": 6, "official_hard": false, "rationale": "搜索明确商品并加入愿望清单"}, + {"task_id": 279, "tier": "medium", "estimated_min_steps": 7, "official_hard": false, "rationale": "筛选品牌商品并汇总名称与价格区间"}, + {"task_id": 284, "tier": "medium", "estimated_min_steps": 7, "official_hard": true, "rationale": "按容量和价格比较商品并打开目标详情页"}, + {"task_id": 288, "tier": "medium", "estimated_min_steps": 7, "official_hard": false, "rationale": "从取消订单历史中定位客户并读取邮箱"}, + {"task_id": 243, "tier": "medium", "estimated_min_steps": 6, "official_hard": false, "rationale": "定位指定商品最低评分客户并读取邮箱"}, + {"task_id": 676, "tier": "medium", "estimated_min_steps": 5, "official_hard": false, "rationale": "按欺诈状态筛选后台订单列表"}, + {"task_id": 470, "tier": "medium", "estimated_min_steps": 6, "official_hard": false, "rationale": "搜索指定订单并执行取消"}, + {"task_id": 605, "tier": "medium", "estimated_min_steps": 7, "official_hard": false, "rationale": "选择相关论坛并发布一个问题"}, + {"task_id": 620, "tier": "medium", "estimated_min_steps": 6, "official_hard": false, "rationale": "在关系论坛填写标题和正文并发帖"}, + {"task_id": 625, "tier": "medium", "estimated_min_steps": 7, "official_hard": true, "rationale": "为讨论主题选择相关论坛并发帖"}, + {"task_id": 630, "tier": "medium", "estimated_min_steps": 6, "official_hard": false, "rationale": "在已知论坛按给定标题和正文发帖"}, + {"task_id": 714, "tier": "medium", "estimated_min_steps": 5, "official_hard": false, "rationale": "进入论坛历史榜并踩排名第一的帖子"}, + {"task_id": 475, "tier": "medium", "estimated_min_steps": 6, "official_hard": false, "rationale": "创建一个命名明确的空仓库"}, + {"task_id": 339, "tier": "medium", "estimated_min_steps": 6, "official_hard": false, "rationale": "在当前项目打开并筛选带缺陷标签的议题"}, + {"task_id": 418, "tier": "medium", "estimated_min_steps": 5, "official_hard": true, "rationale": "打开个人设置并更新 GitLab 状态"}, + {"task_id": 389, "tier": "medium", "estimated_min_steps": 6, "official_hard": false, "rationale": "定位指定合并请求并发表评论"}, + {"task_id": 448, "tier": "medium", "estimated_min_steps": 5, "official_hard": false, "rationale": "打开个人资料并修改主页网址"}, {"task_id": 28, "tier": "hard", "estimated_min_steps": 10, "official_hard": true, "rationale": "找到最新论坛帖子,并按作者和票数条件检查多条评论"}, {"task_id": 50, "tier": "hard", "estimated_min_steps": 9, "official_hard": true, "rationale": "检查一整年的分页订单并汇总数量和支出"}, {"task_id": 66, "tier": "hard", "estimated_min_steps": 12, "official_hard": true, "rationale": "检查十个排序帖子,并根据描述归类推荐内容"}, - {"task_id": 68, "tier": "hard", "estimated_min_steps": 12, "official_hard": true, "rationale": "检查十个排序帖子,并从合格描述中提取两个字段"}, {"task_id": 108, "tier": "hard", "estimated_min_steps": 10, "official_hard": true, "rationale": "应用日期和状态条件,并汇总五个月度数量"}, {"task_id": 110, "tier": "hard", "estimated_min_steps": 12, "official_hard": true, "rationale": "应用日期和状态条件,并汇总十一个月度数量"}, {"task_id": 124, "tier": "hard", "estimated_min_steps": 9, "official_hard": true, "rationale": "搜索商品、检查结果页并计算价格区间"}, {"task_id": 127, "tier": "hard", "estimated_min_steps": 9, "official_hard": true, "rationale": "结合搜索词排名与独立的商品可用性检查"}, - {"task_id": 142, "tier": "hard", "estimated_min_steps": 10, "official_hard": true, "rationale": "检查日期区间内的订单、归类商品并汇总行项目金额"}, {"task_id": 184, "tier": "hard", "estimated_min_steps": 9, "official_hard": true, "rationale": "筛选库存、检查变体并返回成对商品字段"}, {"task_id": 193, "tier": "hard", "estimated_min_steps": 8, "official_hard": true, "rationale": "排序已完成订单、检查两份详情并合计付款"}, {"task_id": 196, "tier": "hard", "estimated_min_steps": 12, "official_hard": true, "rationale": "检查两组各四笔订单,并计算付款差额"}, {"task_id": 204, "tier": "hard", "estimated_min_steps": 9, "official_hard": true, "rationale": "找到最新完成订单,并规范化、排序多个行项目"}, {"task_id": 506, "tier": "hard", "estimated_min_steps": 12, "official_hard": false, "rationale": "清空购物车,按预算和评分筛选品类,选择商品并结账"}, - {"task_id": 441, "tier": "hard", "estimated_min_steps": 11, "official_hard": true, "rationale": "定位项目文件、修改标题、提交到默认分支并验证"} + {"task_id": 441, "tier": "hard", "estimated_min_steps": 11, "official_hard": true, "rationale": "定位项目文件、修改标题、提交到默认分支并验证"}, + {"task_id": 163, "tier": "hard", "estimated_min_steps": 8, "official_hard": true, "rationale": "遍历商品评论并收集全部低评分标题"}, + {"task_id": 320, "tier": "hard", "estimated_min_steps": 10, "official_hard": true, "rationale": "筛选指定月份的取消订单并汇总含运费退款"}, + {"task_id": 334, "tier": "hard", "estimated_min_steps": 8, "official_hard": false, "rationale": "在历史订单中跨条目定位描述匹配商品的最近日期"}, + {"task_id": 528, "tier": "hard", "estimated_min_steps": 12, "official_hard": true, "rationale": "从订单历史提取订单号和金额,再填写但不提交退款表单"}, + {"task_id": 571, "tier": "hard", "estimated_min_steps": 8, "official_hard": true, "rationale": "进入账户地址簿并完整更新邮寄地址"}, + {"task_id": 585, "tier": "hard", "estimated_min_steps": 10, "official_hard": true, "rationale": "从购买历史定位商品并提交指定评分和评论"}, + {"task_id": 510, "tier": "hard", "estimated_min_steps": 12, "official_hard": false, "rationale": "按品类、评论数、评分和价格比较商品并完成结账"}, + {"task_id": 464, "tier": "hard", "estimated_min_steps": 10, "official_hard": false, "rationale": "统计商品高评分评论后更新商品描述"}, + {"task_id": 547, "tier": "hard", "estimated_min_steps": 10, "official_hard": false, "rationale": "编辑可配置商品并为指定尺码增加颜色选项"}, + {"task_id": 694, "tier": "hard", "estimated_min_steps": 12, "official_hard": true, "rationale": "按属性集、库存、尺码、颜色和价格创建商品"}, + {"task_id": 699, "tier": "hard", "estimated_min_steps": 10, "official_hard": false, "rationale": "创建面向注册客户的全站折扣价格规则"}, + {"task_id": 771, "tier": "hard", "estimated_min_steps": 10, "official_hard": true, "rationale": "筛选全部高评分待审评论并逐项批准"}, + {"task_id": 67, "tier": "hard", "estimated_min_steps": 12, "official_hard": true, "rationale": "检查十个热门帖子并精确提取单书推荐标题"}, + {"task_id": 580, "tier": "hard", "estimated_min_steps": 12, "official_hard": true, "rationale": "创建论坛并配置名称、标题、描述和侧栏"}, + {"task_id": 610, "tier": "hard", "estimated_min_steps": 10, "official_hard": true, "rationale": "发布书评后继续定位新帖子并追加评论"}, + {"task_id": 615, "tier": "hard", "estimated_min_steps": 10, "official_hard": true, "rationale": "从来源帖子提取图片地址并转载到另一个论坛"}, + {"task_id": 640, "tier": "hard", "estimated_min_steps": 8, "official_hard": true, "rationale": "在指定论坛创建包含日期信息的活动通知"}, + {"task_id": 719, "tier": "hard", "estimated_min_steps": 10, "official_hard": false, "rationale": "筛选指定用户在论坛的全部投稿并逐项点赞"}, + {"task_id": 725, "tier": "hard", "estimated_min_steps": 10, "official_hard": false, "rationale": "筛选指定用户在论坛的全部投稿并逐项点踩"}, + {"task_id": 731, "tier": "hard", "estimated_min_steps": 8, "official_hard": false, "rationale": "定位自己的指定帖子并在正文末尾追加一行"}, + {"task_id": 29, "tier": "hard", "estimated_min_steps": 12, "official_hard": true, "rationale": "定位最新帖子并按作者和票数条件统计评论"}, + {"task_id": 635, "tier": "hard", "estimated_min_steps": 8, "official_hard": false, "rationale": "判断最合适论坛并发布包含预算的推荐请求"}, + {"task_id": 600, "tier": "hard", "estimated_min_steps": 8, "official_hard": false, "rationale": "自主选择最可能获得回答的论坛并发布问题"}, + {"task_id": 349, "tier": "hard", "estimated_min_steps": 8, "official_hard": true, "rationale": "进入仓库成员设置并列出其他可访问用户"}, + {"task_id": 784, "tier": "hard", "estimated_min_steps": 8, "official_hard": false, "rationale": "定位仓库分支贡献者并读取提交最多者邮箱"}, + {"task_id": 303, "tier": "hard", "estimated_min_steps": 10, "official_hard": true, "rationale": "按年度范围检查提交历史并汇总指定用户次数"}, + {"task_id": 314, "tier": "hard", "estimated_min_steps": 9, "official_hard": false, "rationale": "对仓库贡献者排序并提取前三名完整姓名"}, + {"task_id": 658, "tier": "hard", "estimated_min_steps": 12, "official_hard": true, "rationale": "创建议题并配置受理人和远期截止日期"}, + {"task_id": 666, "tier": "hard", "estimated_min_steps": 12, "official_hard": false, "rationale": "选择源分支和目标分支创建合并请求并指定审核人"}, + {"task_id": 590, "tier": "hard", "estimated_min_steps": 10, "official_hard": true, "rationale": "在项目中创建并配置起止日期里程碑"}, + {"task_id": 576, "tier": "hard", "estimated_min_steps": 10, "official_hard": true, "rationale": "定位仓库并以指定角色添加两名成员"}, + {"task_id": 415, "tier": "hard", "estimated_min_steps": 9, "official_hard": true, "rationale": "定位合并请求、检查最后评论作者并条件化回复"} ] } diff --git a/examples/browser_use/README.md b/examples/browser_use/README.md index 6aa44185a..006facbb6 100644 --- a/examples/browser_use/README.md +++ b/examples/browser_use/README.md @@ -5,9 +5,9 @@ WebArena-Verified evaluator. It is independent from `harness/trajectory_replay`, which replays a fixed list of browser-tool calls to isolate browser-engine behavior and does not measure autonomous planning. -The pilot selection is pre-registered in -`evaluation/browser-use/webarena-verified-pilot.json`: 5 easy, 10 medium, and -15 hard cases. These are project-defined operational tiers based on an audited +The current selection is pre-registered in +`evaluation/browser-use/webarena-verified-pilot.json`: 15 easy, 30 medium, and +45 hard cases. These are project-defined operational tiers based on an audited minimum number of visible UI operations, not WebArena's official hard-subset label. The pilot uses the self-hosted shopping, shopping_admin, reddit, and gitlab sites. Wikipedia and Map are deferred because their official local setup @@ -40,9 +40,9 @@ Astra attempts must never be resumed into or reported as the same cohort. `COWAGENT_REPO` is a trusted code-execution boundary, not a data input. The runner refuses a dirty checkout and records its Git commit in the run config. -Every task runs once per engine. Only a failed official evaluation receives one -retry. The official benchmark score is calculated from primary attempts only; -retry recovery is reported separately and never replaces the primary score. +Every task-browser pair has at most two physical executions: one execution and, +only after failure, one retry. The final execution is the single logical result; +all physical-call latency and model usage remain part of that result. After the run, create the deterministic per-attempt evidence review before building or publishing the report: diff --git a/harness/browser_use/selection.py b/harness/browser_use/selection.py index 5ec004458..09f280431 100644 --- a/harness/browser_use/selection.py +++ b/harness/browser_use/selection.py @@ -4,6 +4,7 @@ import hashlib import json +from collections import Counter from dataclasses import dataclass from functools import lru_cache from importlib.resources import files @@ -12,7 +13,7 @@ PILOT_SCHEMA = "lex_webarena_verified_pilot/1" ALLOWED_SITES = frozenset({"shopping", "shopping_admin", "reddit", "gitlab"}) -TIER_COUNTS = {"easy": 5, "medium": 10, "hard": 15} +TIER_COUNTS = {"easy": 15, "medium": 30, "hard": 45} TIER_BOUNDS = {"easy": (1, 4), "medium": (5, 7), "hard": (8, None)} TASK_EXCLUSIONS_PATH = Path(__file__).parents[2] / "evaluation/browser-use/task-exclusions.json" @@ -207,6 +208,7 @@ def load_pilot(path: str | Path) -> PilotSelection: payload = json.loads(source.read_text(encoding="utf-8")) if payload.get("schema") != PILOT_SCHEMA: raise ValueError(f"unsupported pilot schema: {payload.get('schema')!r}") + metadata = dict(payload.get("metadata") or {}) tasks = tuple( PilotTask( task_id=int(item["task_id"]), @@ -223,11 +225,13 @@ def load_pilot(path: str | Path) -> PilotSelection: counts = {tier: sum(task.tier == tier for task in tasks) for tier in TIER_COUNTS} if counts != TIER_COUNTS: raise ValueError(f"pilot tier counts must be {TIER_COUNTS}, got {counts}") + if metadata.get("tier_counts") != TIER_COUNTS: + raise ValueError("selection metadata tier_counts must match the frozen tier counts") for task in tasks: low, high = TIER_BOUNDS[task.tier] if task.estimated_min_steps < low or (high is not None and task.estimated_min_steps > high): raise ValueError(f"task {task.task_id}: step estimate is outside {task.tier}") - return PilotSelection(tasks, source, _sha256(source), dict(payload.get("metadata") or {})) + return PilotSelection(tasks, source, _sha256(source), metadata) def validate_against_official( @@ -284,4 +288,25 @@ def validate_against_official( "start_urls": list(task.start_urls), } ) + site_counts = Counter(row["sites"][0] for row in resolved) + expected_site_counts = selection.metadata.get("site_counts") + if expected_site_counts is not None and expected_site_counts != dict(site_counts): + raise ValueError( + f"selection site counts must be {expected_site_counts}, got {dict(site_counts)}" + ) + expected_task_type_counts = selection.metadata.get("task_type_counts") + if expected_task_type_counts is not None: + task_type_counts: Counter[str] = Counter() + for selected in selection.tasks: + task = benchmark.get_task(selected.task_id) + response_evaluator = next( + item for item in task.eval if item.evaluator == "AgentResponseEvaluator" + ) + task_type = response_evaluator.expected.task_type + task_type_counts[getattr(task_type, "value", str(task_type))] += 1 + if expected_task_type_counts != dict(task_type_counts): + raise ValueError( + "selection task type counts must be " + f"{expected_task_type_counts}, got {dict(task_type_counts)}" + ) return resolved diff --git a/tests/browser_use/test_runner.py b/tests/browser_use/test_runner.py index eaad1786a..e9b7e2b7f 100644 --- a/tests/browser_use/test_runner.py +++ b/tests/browser_use/test_runner.py @@ -91,7 +91,7 @@ def test_max_tasks_limits_only_the_current_invocation() -> None: assert _scheduled_tasks(tasks, None) == tasks assert _scheduled_tasks(tasks, 1) == tasks[:1] - with pytest.raises(ValueError, match="between 1 and 30"): + with pytest.raises(ValueError, match="between 1 and 90"): _scheduled_tasks(tasks, 0) diff --git a/tests/browser_use/test_selection.py b/tests/browser_use/test_selection.py index 902ab5f4e..41789eba2 100644 --- a/tests/browser_use/test_selection.py +++ b/tests/browser_use/test_selection.py @@ -16,11 +16,11 @@ def test_pilot_has_pre_registered_tier_counts() -> None: selection = load_pilot(Path("evaluation/browser-use/webarena-verified-pilot.json")) - assert len(selection.tasks) == 30 + assert len(selection.tasks) == 90 assert { tier: sum(task.tier == tier for task in selection.tasks) for tier in TIER_COUNTS } == TIER_COUNTS - assert sum(task.official_hard for task in selection.tasks) == 16 + assert sum(task.official_hard for task in selection.tasks) == 39 assert len(selection.sha256) == 64 From 76c0c138ffa3aa71261ff1b6ab214b888f39ef30 Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 11 Sep 2026 00:48:49 +0800 Subject: [PATCH 051/127] make browser report task count dynamic --- tests/browser_use/test_report.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/browser_use/test_report.py b/tests/browser_use/test_report.py index e97ce4c03..9a9c4e65d 100644 --- a/tests/browser_use/test_report.py +++ b/tests/browser_use/test_report.py @@ -3,6 +3,7 @@ import harness.browser_use.report as report_module from harness.browser_use.report_i18n import localize_html +from harness.browser_use.selection import load_pilot from harness.browser_use.report import ( _action_duration_cards, _best, @@ -379,7 +380,10 @@ def test_report_renders_measured_runtime_identities(tmp_path: Path) -> None: assert page.is_file() if name not in {"guide/article.html", "en/index.html"}: assert "切换主题" in page.read_text(encoding="utf-8") - assert len(list((output / "datasets").glob("[0-9]*.html"))) == 30 + expected_tasks = len( + load_pilot("evaluation/browser-use/webarena-verified-pilot.json").tasks + ) + assert len(list((output / "datasets").glob("[0-9]*.html"))) == expected_tasks assert (output / "guide/article.html").read_text(encoding="utf-8").find(" Date: Fri, 11 Sep 2026 00:58:15 +0800 Subject: [PATCH 052/127] clarify missing response evaluator errors --- harness/browser_use/selection.py | 12 +++++++++++- tests/browser_use/test_selection.py | 27 +++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/harness/browser_use/selection.py b/harness/browser_use/selection.py index 09f280431..b1e90004f 100644 --- a/harness/browser_use/selection.py +++ b/harness/browser_use/selection.py @@ -300,8 +300,18 @@ def validate_against_official( for selected in selection.tasks: task = benchmark.get_task(selected.task_id) response_evaluator = next( - item for item in task.eval if item.evaluator == "AgentResponseEvaluator" + ( + item + for item in task.eval + if item.evaluator == "AgentResponseEvaluator" + ), + None, ) + if response_evaluator is None: + raise ValueError( + f"task {selected.task_id}: missing AgentResponseEvaluator " + "for task_type_counts" + ) task_type = response_evaluator.expected.task_type task_type_counts[getattr(task_type, "value", str(task_type))] += 1 if expected_task_type_counts != dict(task_type_counts): diff --git a/tests/browser_use/test_selection.py b/tests/browser_use/test_selection.py index 41789eba2..7cc58205c 100644 --- a/tests/browser_use/test_selection.py +++ b/tests/browser_use/test_selection.py @@ -175,3 +175,30 @@ def test_official_contract_validation_rejects_task_68_null_author() -> None: enforce_contract_integrity=False, ) assert resolved[0]["task_id"] == 68 + + +def test_task_type_count_validation_reports_missing_response_evaluator() -> None: + task = SimpleNamespace( + eval=[], + sites=[SimpleNamespace(value="reddit")], + intent="intent", + intent_template_id=1, + revision=0, + start_urls=["__REDDIT__"], + ) + selection = PilotSelection( + tasks=(PilotTask(1, "easy", 1, "rationale", False),), + source_path=Path("selection.json"), + sha256="0" * 64, + metadata={ + "site_counts": {"reddit": 1}, + "task_type_counts": {"RETRIEVE": 1}, + }, + ) + + with pytest.raises(ValueError, match="missing AgentResponseEvaluator"): + validate_against_official( + selection, + SimpleNamespace(get_task=lambda _: task), + enforce_contract_integrity=False, + ) From c4e8b85e64fd8348a15d243163466151de21a1b4 Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 11 Sep 2026 01:16:00 +0800 Subject: [PATCH 053/127] derive browser report scope from evidence --- harness/browser_use/report.py | 117 +++++++++++++++++++--------------- 1 file changed, 65 insertions(+), 52 deletions(-) diff --git a/harness/browser_use/report.py b/harness/browser_use/report.py index dff5bd94a..fd3812bfe 100644 --- a/harness/browser_use/report.py +++ b/harness/browser_use/report.py @@ -49,12 +49,6 @@ MUTATE_IDS = {399, 404, 441, 506} NAVIGATE_IDS = {44, 45, 118} RESEARCH_ROOT = Path(__file__).parents[2] / "research" -SHARED_FAILURE_DIAGNOSES = { - int(task_id): diagnosis - for task_id, diagnosis in json.loads( - (RESEARCH_ROOT / "browser-use-failure-analysis.json").read_text(encoding="utf-8") - )["tasks"].items() -} MAC_EXPANSION = json.loads( (RESEARCH_ROOT / "webarena-mac-expansion.json").read_text(encoding="utf-8") ) @@ -1143,28 +1137,48 @@ def _artifact_links(attempt_id: str) -> str: def _article_body( - paired: dict[str, Any], attempts: list[dict[str, Any]], tasks: list[dict[str, Any]] + paired: dict[str, Any], + attempts: list[dict[str, Any]], + tasks: list[dict[str, Any]], + verified_causes: dict[str, dict[str, Any]], ) -> str: summary = _primary_summary(attempts) chromium, moli = summary["chromium"], summary["moli"] - failures = [ + task_count = len(tasks) + failed_result_count = sum( + item["completed"] - item["passed"] for item in summary.values() + ) + discordant = paired["chromium_only"] + paired["moli_only"] + tier_counts = { + tier: sum(task["tier"] == tier for task in tasks) for tier in ("easy", "medium", "hard") + } + cause_rows = [ [ - f"#{task_id}", - _finding_text(diagnosis, "root_cause"), - _finding_text(diagnosis, "owner"), - _finding_text(diagnosis, "proof"), - _finding_text(diagnosis, "corrective_action"), - _finding_text(diagnosis, "verification"), + result_id, + (diagnosis.get("subcategory") or {}).get("zh") or "—", + (diagnosis.get("reason") or {}).get("zh") or "—", ] - for task_id, diagnosis in SHARED_FAILURE_DIAGNOSES.items() + for result_id, diagnosis in sorted(verified_causes.items()) ] + if failed_result_count: + cause_section = ( + _table( + ["逻辑结果", "根因子类", "已核验根因"], + cause_rows, + caption="表 2|根因核验不修改官方分数;修复后必须产生新的执行证据。", + ) + if len(cause_rows) == failed_result_count + else f'

    当前有 {failed_result_count} 个失败结果,根因核验尚未完成。正式发布前必须逐个绑定当前证据并补齐根因子类和具体原因。

    ' + ) + else: + cause_section = '

    本轮没有失败结果,无需生成失败根因记录。

    ' return ( '
    Browser Use 实测解读
    ' - f"

    {chromium['passed']} 比 {moli['passed']} 不是同因平局:五个零分包含四类根因

    " - f'

    同一 CowAgent 与同一模型替换浏览器后,{paired["paired"]} 个配对任务的官方分数没有出现单边胜出,但逐条追查证明相同分数并不代表相同执行结果。根因分别位于官方任务合同、Agent 完成策略、本项目响应协议和证据采集器;Task 506 的 Chromium 与 Moli 甚至不是同一种失败。

    ' + f"

    {chromium['passed']}/{chromium['completed']} 对 {moli['passed']}/{moli['completed']}:{discordant} 个任务出现浏览器分歧

    " + f'

    同一 CowAgent、模型、任务、站点状态和 evaluator 下,只替换浏览器后端。总分给出结果,{paired["paired"]} 个同题配对用于定位差异;每个失败结果还要沿官方合同、最终回答、网络证据和自主轨迹追到可复核根因。

    ' '

    测量口径:WebArena‑Verified 1.2.3;每题×浏览器一个最终逻辑结果;官方确定性 evaluator;无 LLM judge。

    ' - "

    一、最重要的结果不是平分,而是零个不一致配对

    " - f"

    Chromium 与 Moli 分别通过 {chromium['passed']}/{chromium['completed']} 和 {moli['passed']}/{moli['completed']}。逐题配对后,{paired['both_pass']} 题共同通过、{paired['both_fail']} 题共同失败,两个浏览器单边通过都是 0。由于固定样本里没有不一致任务,这批数据不能估计浏览器的方向性效应,也不对 WebArena‑Verified 全集作统计外推。

    " + "

    一、先看总分,再看同题分歧

    " + f"

    Chromium 与 Moli 分别通过 {chromium['passed']}/{chromium['completed']} 和 {moli['passed']}/{moli['completed']}。逐题配对后,{paired['both_pass']} 题共同通过、{paired['both_fail']} 题共同失败、{paired['chromium_only']} 题仅 Chromium 通过、{paired['moli_only']} 题仅 Moli 通过。

    " + _table( ["配对结果", "任务数", "可支持的判断"], [ @@ -1173,17 +1187,13 @@ def _article_body( ["仅 Moli", paired["moli_only"], "本轮未观察到"], ["两者都失败", paired["both_fail"], "优先排查共同链路"], ], - caption="表 1|30 个任务的逐题最终结果配对。", - ) - + f"

    严谨表述只能是“本轮没有观察到成功率差异”,不能写成“两种内核等价”。{paired['paired']} 题对小效应检出力有限;更关键的是,样本没有诱发内核分歧。

    " - '

    二、五个零分的根因与修复边界

    ' - f"

    两个浏览器共享 CowAgent、模型、任务合同与 evaluator,但这不意味着共同零分必然来自共同原因。根因审计把“页面上出现了什么”继续追到产生错误的合同、策略或采集实现,并为每项给出必须通过的复测条件。

    " - + _table( - ["任务", "根因", "责任层", "证据闭环", "根治措施", "复测门槛"], - failures, - caption="表 2|历史官方分数不被人工改写;合同或运行协议修复后必须产生新证据。", + caption=f"表 1|{paired['paired']} 个任务的逐题最终结果配对。", ) - + "

    可执行结论:#68 因官方 expected 与 schema 冲突而退出下一轮干净 A/B;#142 因类别验收边界未定义而退出,直至补齐可观察口径;#168 修正本项目的空结果状态协议;#124 增加候选语义核验和提交前自检;#506 修复 HAR 即时采集、末步工具调用 fail-closed,并在预注册的 40 步上限下复测。旧的 25/30 仍保留为历史官方分数,但不能再解释成五个同因的 Agent 失败。

    三、简单、中等、困难都覆盖了,但“难度”只是操作长度

    WebArena‑Verified 没有官方 easy/medium/hard 三档。本项目在运行前按最少可见 UI 操作数注册为 1–4、5–7、8+ 三档,配额 5/10/15。它控制操作链覆盖,不等同于推理难度。

    " + + f"

    固定样本中观察到 {discordant} 个不一致配对。这个数字用于发现浏览器相关差异,但不能单独证明整个浏览器内核的优劣,也不对 WebArena‑Verified 全集作统计外推。

    " + '

    二、失败结果必须追到根因

    ' + "

    程序化 evaluator 先给出失败环节;核验再检查最终执行的任务合同、回答、网络记录和 Agent 轨迹,形成更具体的根因子类与原因。共同失败不默认是共同根因,单边失败也不默认是浏览器缺陷。

    " + + cause_section + + f"

    三、简单、中等、困难都覆盖,但难度只表示操作长度

    WebArena‑Verified 没有官方 easy/medium/hard 三档。本项目在运行前按最少可见 UI 操作数注册为 1–4、5–7、8+ 三档,配额 {tier_counts['easy']}/{tier_counts['medium']}/{tier_counts['hard']}。它控制操作链覆盖,不等同于推理难度。

    " + _table( ["本项目分层", "Chromium", "Moli"], _group_rows(attempts, tasks, "tier"), @@ -1194,7 +1204,7 @@ def _article_body( _group_rows(attempts, tasks, "sites"), caption="表 4|四个可重置自托管站点。", ) - + f'

    四、选择 WebArena‑Verified 是为了归因,不是因为它最流行

    Mind2Web 家族采用面更广,但原版主要评离线动作预测;Online‑Mind2Web 和 WebVoyager贴近实时网页,却引入网页漂移和模型裁判。WebArena‑Verified 可以自托管、逐题重置,并用结构化最终答案和必要网络副作用做程序化验收,更适合“只替换浏览器”的 A/B。

    代价也明确:部署重,环境兼容本身会成为工程变量;官方 evaluator 不衡量路径效率,也不能覆盖开放互联网泛化。

    五、下一轮先扩展四个已验证站点

    官方包有 {MAC_COUNTS["full"]} 题和 {MAC_COUNTS["hard"]} 题 hard 子集,没有官方 179 题版本;179 只能是另行声明的自定义样本。当前 Mac 已实跑的四个站点可覆盖 {MAC_COUNTS["current_four_sites_single_site"]} 个单站任务;再计入只依赖这四站的跨站任务,共有 {MAC_COUNTS["current_four_sites_including_cross_site"]} 个候选,其中 hard 子集有 {MAC_COUNTS["current_four_sites_hard_including_cross_site"]} 个。下一轮应在这些候选中先做登录、重置、健康检查与 evaluator 预检,再按简单、中等、困难和内核敏感交互分层抽样。

    Wikipedia 需要约 100 GB 数据,Map 需要约 60 GB 数据且当前公开镜像为 amd64;二者可作为后续扩展,但没有理由成为本机下一批的前置阻塞。

    保留 CowAgent、模型、reasoning=low 和每题最多两次执行的固定协议,优先加入长页面、复杂编辑器、弹窗、多标签页、上传下载等更可能诱发浏览器差异的任务。实时网站另开 Online‑Mind2Web / WebVoyager 泛化轨道,LLM judge 与人工抽检单列。

    决策边界

    本轮支持“流程可比、当前样本无方向性差异”;不支持“内核等价”“Moli 已全面替代 Chromium”或“端到端耗时就是纯内核速度”。Astra 获权后必须形成独立运行。

    ' + + f'

    四、选择 WebArena‑Verified 是为了可归因的结果验收

    Mind2Web 家族采用面更广,但原版主要评离线动作预测;Online‑Mind2Web 和 WebVoyager贴近实时网页,却引入网页漂移和模型裁判。WebArena‑Verified 可以自托管、逐题重置,并用结构化最终答案和必要网络副作用做程序化验收,更适合“只替换浏览器”的 A/B。

    代价也明确:部署重,环境兼容本身会成为工程变量;官方 evaluator 不衡量路径效率,也不能覆盖开放互联网泛化。

    五、结论只适用于本轮受控样本

    本轮固定 {task_count} 题,覆盖四个已验证站点。结果只说明当前 CowAgent、模型、任务与运行协议下的浏览器差异;不支持把总分直接解释为浏览器内核等价、全面替代关系或纯内核速度。端到端耗时包含模型、调度、页面等待和工具开销,动作延迟也包含 CDP 往返。

    决策边界

    先修复已核验根因并形成新证据,再扩大任务与浏览器范围;历史官方分数不被人工覆盖。

    ' ) @@ -1501,6 +1511,12 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: tasks = _resolve_tasks( selection, expected_data_checksum=str(official.get("data_checksum") or "") or None ) + task_count = len(tasks) + result_count = len(attempts) + max_agent_steps = int(config.get("max_agent_steps") or protocol.get("max_agent_steps") or 0) + tier_counts = { + tier: sum(task["tier"] == tier for task in tasks) for tier in ("easy", "medium", "hard") + } summary, paired = _primary_summary(attempts), _paired_summary(attempts) measured_solvable_ids = { int(row["task_id"]) for row in attempts if float(row.get("score") or 0) == 1 @@ -1547,7 +1563,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: METHODOLOGY_FIGURE, stage / "assets/webarena-autonomous-evaluation-flow.png", ) - article = _article_body(paired, attempts, tasks) + article = _article_body(paired, attempts, tasks, verified_causes) c, m = summary["chromium"], summary["moli"] families = _task_family_summary(tasks, attempts) coverage = review.get("coverage") or {} @@ -1563,7 +1579,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: home = _hero( f"Browser Use Capability Evaluation · {run_date}", "Chromium 与 Moli:Browser Use 能力实测", - f"在同一 CowAgent、同一 {model}(reasoning=low)和同一 WebArena‑Verified 30 题样本下,对比两种浏览器完成真实网页任务的能力。", + f"在同一 CowAgent、同一 {model}(reasoning=low)和同一 WebArena‑Verified {task_count} 题样本下,对比两种浏览器完成真实网页任务的能力。", ) home += _section( "先看结论", @@ -1591,7 +1607,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) home += _section( "从结论进入证据", - '
    ', + f'

    {result_count} 个逻辑结果

    查看最终判定、耗时、轨迹和核验状态。

    打开结果库 →
    ', ) home += _section( "逐题配对比总分更重要", @@ -1641,7 +1657,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: datasets = _hero( "Dataset", - "候选 benchmark、选择依据与 30 个预注册任务", + f"候选 benchmark、选择依据与 {task_count} 个预注册任务", "先区分最终结果、动作过程和模型裁判,再谈采用率;同一家族不同变体的成绩不能合并。", ) datasets += _section( @@ -1651,7 +1667,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: datasets += ( _section( "Mind2Web 的拆分不是 easy / medium / hard", - "

    原始 Mind2Web 使用 cross-task、cross-website、cross-domain 衡量泛化;Online‑Mind2Web 没有统一三档;Mind2Web 2 的 short / medium / long 按动作跨度划分。本报告的简单 5、中等 10、困难 15 是预注册的操作长度层级。

    ", + f"

    原始 Mind2Web 使用 cross-task、cross-website、cross-domain 衡量泛化;Online‑Mind2Web 没有统一三档;Mind2Web 2 的 short / medium / long 按动作跨度划分。本报告的简单 {tier_counts['easy']}、中等 {tier_counts['medium']}、困难 {tier_counts['hard']} 是预注册的操作长度层级。

    ", ) + _section( "怎样阅读任务 ground truth", @@ -1659,9 +1675,9 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) + _section( "题集范围", - "

    本轮在模型执行前固定 30 个任务,覆盖四个已部署站点以及 RETRIEVE、NAVIGATE、MUTATE 三种最终合同。这里仅描述题目与验收规则;通过率、耗时、轨迹和可完成性观察统一放在结果分析中。

    ", + f"

    本轮在模型执行前固定 {task_count} 个任务,覆盖四个已部署站点以及 RETRIEVE、NAVIGATE、MUTATE 三种最终合同。这里仅描述题目与验收规则;通过率、耗时、轨迹和可完成性观察统一放在结果分析中。

    ", ) - + _section("30 个任务", _task_table(tasks)) + + _section(f"{task_count} 个任务", _task_table(tasks)) ) (stage / "datasets/index.html").write_text( _page(title="评测集", active="datasets", body=datasets, depth=1), encoding="utf-8" @@ -1744,15 +1760,15 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) methodology += _section( "本轮判分链", - '
    1. 预注册 30 题
    2. 逐题重置站点
    3. CowAgent 自主规划
    4. 只替换浏览器
    5. 官方 evaluator 验收
    6. 确定性证据复核

    官方数据 812 题全部配置 AgentResponseEvaluator,其中 663 题还配置 NetworkEventEvaluator。所有已配置 evaluator 都返回 1.0 才通过。它检查最终结果和必要副作用,不要求复刻参考动作序列。

    ', + f'
    1. 预注册 {task_count} 题
    2. 逐题重置站点
    3. CowAgent 自主规划
    4. 只替换浏览器
    5. 官方 evaluator 验收
    6. 确定性证据复核

    官方数据 812 题全部配置 AgentResponseEvaluator,其中 663 题还配置 NetworkEventEvaluator。所有已配置 evaluator 都返回 1.0 才通过。它检查最终结果和必要副作用,不要求复刻参考动作序列。

    ', ) methodology += _section( - "这 30 题如何部署并由 Agent 执行", + f"这 {task_count} 题如何部署并由 Agent 执行", '
    WebArena-Verified 本地部署、CowAgent 自主操作、双浏览器对照与程序化验收流程
    图 1 · 从官方站点镜像、逐题隔离与健康检查,到 CowAgent 观察—决策—操作闭环,再到 Chromium/Moli 对照和官方程序化验收。窄屏可横向滚动查看。
    ' "

    站点确实部署在本机,不是公网代站。macOS 上的 Docker 由 Colima 虚拟机承载;本轮 Colima VM 地址是 192.168.5.3,其默认路由指向 192.168.5.2。后者是 VM 视角下的 macOS 宿主网关,不是虚拟机地址,也不是评测网站。Shopping、Shopping Admin、Reddit、GitLab 四个官方站点镜像通过 Docker 端口发布到这个宿主入口,独立的 Chromium/Moli 浏览器容器据此访问本地站点。

    " - "

    WebArena‑Verified 当前提供 Shopping、Shopping Admin、Reddit、GitLab、Wikipedia、Map 六类环境。本轮 30 题只覆盖前四类;Wikipedia 与 Map 需要额外大型数据卷,因此没有部署,也没有任务进入成绩。

    " + f"

    WebArena‑Verified 当前提供 Shopping、Shopping Admin、Reddit、GitLab、Wikipedia、Map 六类环境。本轮 {task_count} 题只覆盖前四类;Wikipedia 与 Map 需要额外大型数据卷,因此没有部署,也没有任务进入成绩。

    " "

    运行器代码在每个执行前调用 ContainerManager.start(reset=True),只保留该题所需站点;env-ctrl 报告 ready 后,再做连续应用健康检查、清浏览器状态、登录测试账号并打开起始页。当前冻结证据保存了代码提交、运行协议和镜像身份;事件流从环境准备完成后开始,因此逐次 reset、ready 与 health 结果未形成独立绑定的准备回执。新版运行协议已要求保存该回执。

    " - "

    预注册文件只含任务 ID、层级和入选理由,不含点击序列。CowAgent 收到自然语言目标后,在最多 30 步内循环读取页面观察、请求模型决策并生成下一次 BrowserTool 调用。Chromium 与 Moli 分别连接独立 CDP 浏览器容器;每个逻辑结果保留最终 JSON、Agent 事件流、网络 HAR 和 Playwright trace。

    " + f"

    预注册文件只含任务 ID、层级和入选理由,不含点击序列。CowAgent 收到自然语言目标后,在最多 {max_agent_steps} 步内循环读取页面观察、请求模型决策并生成下一次 BrowserTool 调用。Chromium 与 Moli 分别连接独立 CDP 浏览器容器;每个逻辑结果保留最终 JSON、Agent 事件流、网络 HAR 和 Playwright trace。

    " + _table( ["站点", "本轮访问地址", "应用端口", "环境控制端口", "状态"], [ @@ -1766,8 +1782,8 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ], ["Reddit", "http://192.168.5.2:9999", "9999", "9998", "本轮已部署"], ["GitLab", "http://192.168.5.2:8023", "8023", "8024", "本轮已部署"], - ["Wikipedia", "—", "8888", "8889", "未部署;不在 30 题"], - ["Map", "—", "3030", "3031", "未部署;不在 30 题"], + ["Wikipedia", "—", "8888", "8889", f"未部署;不在 {task_count} 题"], + ["Map", "—", "3030", "3031", f"未部署;不在 {task_count} 题"], ], ) + '

    证据边界:事件流能证明 CowAgent 在准备完成后产生了 BrowserTool 调用序列;镜像 ID、digest、代码提交与网关记录限定了本地运行身份。当前证据没有独立的逐次环境准备 receipt,因此报告不把“每次重置均成功”当成已经独立核验的事实。

    ' @@ -1864,7 +1880,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: + _same_task_comparison(tasks, attempts), ).replace('
    ', '
    ', 1) analysis += _section( - "60 个逻辑结果", + f"{result_count} 个逻辑结果", "

    默认只显示失败;可按浏览器、层级、站点、任务类型、失败环节和全文关键词筛选。

    " + _attempt_table(attempts, tasks, verified_causes), ) @@ -2036,17 +2052,17 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: f"{model} · reasoning={protocol.get('reasoning_effort', '—')}", "为两个选手生成决策", ], - ["任务与站点", "同一 30 题;同一批本地站点镜像", "提供一致输入与网页状态"], + ["任务与站点", f"同一 {task_count} 题;同一批本地站点镜像", "提供一致输入与网页状态"], ["动作接口", "CowAgent BrowserTool · CDP", "把 Agent 动作交给浏览器"], [ "验收", "WebArena‑Verified 1.2.3 官方 evaluator", "对最终 JSON 与必要网络副作用判分", ], - ["资源与上限", "相同 CPU、内存、viewport 与最多 30 步", "控制运行预算"], + ["资源与上限", f"相同 CPU、内存、viewport 与最多 {max_agent_steps} 步", "控制运行预算"], ], ) - + "

    这 30 题由 CowAgent 自主执行。仓库中的 Browser Trajectory Replay(轨迹重放)固定动作微基准属于另一条实验轨道,不是本报告的参评选手,也不计入这 30 题成绩。

    ", + + f"

    这 {task_count} 题由 CowAgent 自主执行。仓库中的 Browser Trajectory Replay(轨迹重放)固定动作微基准属于另一条实验轨道,不是本报告的参评选手,也不计入这 {task_count} 题成绩。

    ", ) adoption += _section( "如何增加下一名选手", @@ -2168,7 +2184,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) reproduce += _section( "公开复现入口", - '

    lexmount/browser-eval 是代码、题集和锁文件的唯一来源。

    ./examples/browser_use/run.sh\nmake browser-use-review EVIDENCE="$BROWSER_USE_EVIDENCE"\nmake browser-use-report EVIDENCE="$BROWSER_USE_EVIDENCE"\nmake browser-use-report-check EVIDENCE="$BROWSER_USE_EVIDENCE"

    每个逻辑结果的脱敏回答、evaluator、事件和轨迹位于 artifacts/<result-id>/;evidence-index.json 给出全部 60 个逻辑结果的页面与下载路径。

    ', + f'

    lexmount/browser-eval 是代码、题集和锁文件的唯一来源。

    ./examples/browser_use/run.sh\nmake browser-use-review EVIDENCE="$BROWSER_USE_EVIDENCE"\nmake browser-use-report EVIDENCE="$BROWSER_USE_EVIDENCE"\nmake browser-use-report-check EVIDENCE="$BROWSER_USE_EVIDENCE"

    每个逻辑结果的脱敏回答、evaluator、事件和轨迹位于 artifacts/<result-id>/;evidence-index.json 给出全部 {result_count} 个逻辑结果的页面与下载路径。

    ', ) (stage / "reproduce/index.html").write_text( _page(title="环境与复现", active="reproduce", body=reproduce, depth=1), encoding="utf-8" @@ -2196,10 +2212,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: "schema": "lex_browser_use_interpretation/1", "claims": [ {"claim": "当前样本未观察到成功率差异", "basis": paired}, - { - "claim": "失败任务完全重合;证据确认四题止于最终答案或状态协议,一题止于步数上限前的结账流程", - "basis": SHARED_FAILURE_DIAGNOSES, - }, + {"claim": "失败根因以当前证据绑定的逐结果核验为准", "basis": verified_causes}, ], "bound_attempts_sha256": _sha256(evidence / "attempts.jsonl"), "selection_sha256": selection.sha256, From 7eae48345651dd168850593999f0cf326ea1b171 Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 11 Sep 2026 01:21:08 +0800 Subject: [PATCH 054/127] exclude internal report work logs --- .../claim-ledger.md | 13 -------- .../reference-gap-matrix.md | 31 ------------------- .gitignore | 1 + 3 files changed, 1 insertion(+), 44 deletions(-) delete mode 100644 .ai/topics/browser-use-report-rewrite/claim-ledger.md delete mode 100644 .ai/topics/browser-use-report-rewrite/reference-gap-matrix.md diff --git a/.ai/topics/browser-use-report-rewrite/claim-ledger.md b/.ai/topics/browser-use-report-rewrite/claim-ledger.md deleted file mode 100644 index 2bd7184eb..000000000 --- a/.ai/topics/browser-use-report-rewrite/claim-ledger.md +++ /dev/null @@ -1,13 +0,0 @@ -# Browser Use report claim ledger - -This file is an editorial input, not part of the published report bundle. - -| Claim | Status | Evidence | Boundary | -|---|---|---|---| -| Chromium and Moli each passed 25/30 primary attempts. | measured | `attempts.jsonl`, primary attempts only | Applies to this 30-task cohort and fixed run configuration. | -| There were zero discordant primary pairs. | derived | paired join on task ID and engine | Does not establish browser equivalence. | -| The five primary failures were shared: 68, 124, 142, 168, 506. | derived | primary attempt score pairs | Shared variables include agent, model, prompt, step limit, and evaluator interface. | -| Ten permitted failure retries produced zero recoveries. | measured | attempt 2 records | Retry results are diagnostic and never replace pass@1. | -| All 70 recorded attempts passed deterministic evidence review. | measured | `result-review.json` | Review checks integrity and agreement; it is not a second evaluator. | -| WebArena-Verified is the best first benchmark for this browser A/B. | interpretation | deterministic result evaluation, resettable self-hosted sites | It is operationally heavy and does not cover open-web drift. | -| Next expansion should seek kernel-sensitive, deterministically verifiable tasks. | proposal | zero discordance plus shared failure pattern | Must remain a new pre-registered run; do not alter this cohort. | diff --git a/.ai/topics/browser-use-report-rewrite/reference-gap-matrix.md b/.ai/topics/browser-use-report-rewrite/reference-gap-matrix.md deleted file mode 100644 index 97a85a551..000000000 --- a/.ai/topics/browser-use-report-rewrite/reference-gap-matrix.md +++ /dev/null @@ -1,31 +0,0 @@ -# Browser Use report parity matrix - -This is an internal implementation checklist. It is not part of the public report. - -| Reader question / function | Web Fetch reference | BrowserTool reference | Browser Use implementation | -|---|---|---|---| -| What was tested? | Searchable dataset inventory and per-case pages | Search, difficulty/site/action filters, per-task cards | Searchable 30-task inventory; per-task contract pages | -| What is the ground truth? | Full reference, stable anchors, field contract | Exact outputs, derivation, fixed reference actions | Official WebArena-Verified expected response and network-event contracts | -| Is there one correct path? | Not relevant to fetch | Yes, fixed harness action sequence | No canonical path; disclose outcome-based evaluation and separately label whether this run contains a successful trajectory proving current-snapshot solvability | -| What did the method actually do? | Provider output | Step-by-step actual BrowserTool calls | Full CowAgent BrowserTool trajectory projected from agent events | -| What did it return? | Full actual output | Saved outputs per step | Final agent response plus normalized evaluator actual values | -| Why did it pass/fail? | Rule-level checks and reference/output comparison | Output and process validation | Per-evaluator expected/actual/assertions/error with derived failure stage | -| What are the raw records? | Lazy complete output, downloadable evidence | Modal raw parameters/output/error | Sanitized response, evaluation, events, trajectory and evidence metadata downloads | -| What stays private? | Explicit publication privacy manifest | Public-web evidence only | HAR and Playwright trace remain private because they may contain session state; hashes and sizes are public | -| Can readers find failures? | Default-fail filters and search | Multi-dimensional result explorer | Default-fail result explorer with tier/site/type/engine/attempt/evaluator/search filters | -| Can readers compare engines per task? | Provider rows on dataset pages | Side-by-side reference/action table | Side-by-side primary result, actual final answer, failed evaluator and trajectory links | -| Can readers sort and preserve context? | Sortable rankings; loader restores filters/scroll | Search/filter and deep links | Sortable tables, URL-backed filters and sort, scroll restoration, counts and deep links; no virtual loader needed at this report size | -| Is review auditable? | Review rounds, protocol, coverage, disagreements | Programmatic result/process checks | Review protocol, full attempt coverage, per-attempt checks, disagreements and unresolved inventory | -| Are methodology and benchmark choice explained? | Track-local contracts and boundaries | Step/result metric definitions | Benchmark adoption, judge types, selection decision, deterministic outcome contract and limits | -| Are current report-only modules retained? | N/A | N/A | Benchmark landscape, Astra access probe, paired inference, autonomous-vs-fixed harness boundary | -| Is reproduction bounded? | Code/dataset identity and current commands | Source and raw data | Commit/model/reasoning/browser/site/evaluator identities and current commands | -| Is there a compact English entry point? | One-page English edition | No | One-page English results, scope, method, evidence, review, access and privacy summary | -| Can machine readers discover every artifact? | Root manifests and virtual artifact catalog | One bundled data index | Public evidence index maps all 70 attempt pages and five sanitized artifact types | - -Implementation decisions: - -- Do not invent a canonical action path for WebArena-Verified. Its ground truth is a conjunction of evaluator contracts. -- Do not treat an official contract as proof that the current site snapshot is solvable. Label tasks with a measured successful trajectory separately from tasks that have only the official contract. -- Publish benchmark answers because they are part of the public benchmark package; redact credentials and session-bearing material. -- Publish sanitized CowAgent events and evaluator records. Do not publish raw HAR or Playwright trace archives. -- Keep the direct static-site architecture. The projected report remains far below the reference report's virtual-file threshold, so a gzip virtual loader would add complexity without reader value. diff --git a/.gitignore b/.gitignore index c15162ba6..3fcfaa2df 100644 --- a/.gitignore +++ b/.gitignore @@ -32,3 +32,4 @@ run.log artifacts/trajectory_replay/ artifacts/browser_use/ /docs/reports/trajectory-replay-evaluation-* +.ai/topics/ From b84359404ebb885d3be7cb9d9c53339d9bde1692 Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 11 Sep 2026 01:25:36 +0800 Subject: [PATCH 055/127] report measured environment preparation --- harness/browser_use/report.py | 40 ++++++++++++++++++++++++-------- tests/browser_use/test_report.py | 1 + 2 files changed, 31 insertions(+), 10 deletions(-) diff --git a/harness/browser_use/report.py b/harness/browser_use/report.py index fd3812bfe..b960ece49 100644 --- a/harness/browser_use/report.py +++ b/harness/browser_use/report.py @@ -1514,6 +1514,14 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: task_count = len(tasks) result_count = len(attempts) max_agent_steps = int(config.get("max_agent_steps") or protocol.get("max_agent_steps") or 0) + site_access_host = str(environment.get("site_access_host") or config.get("site_access_host") or "未记录") + preparation_receipt_count = sum( + Path(str((row.get("paths") or {}).get("environment_prepare") or "")).is_file() + for row in source_attempts + ) + preparation_receipts_complete = bool(source_attempts) and preparation_receipt_count == len( + source_attempts + ) tier_counts = { tier: sum(task["tier"] == tier for task in tasks) for tier in ("easy", "medium", "hard") } @@ -1765,29 +1773,37 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: methodology += _section( f"这 {task_count} 题如何部署并由 Agent 执行", '
    WebArena-Verified 本地部署、CowAgent 自主操作、双浏览器对照与程序化验收流程
    图 1 · 从官方站点镜像、逐题隔离与健康检查,到 CowAgent 观察—决策—操作闭环,再到 Chromium/Moli 对照和官方程序化验收。窄屏可横向滚动查看。
    ' - "

    站点确实部署在本机,不是公网代站。macOS 上的 Docker 由 Colima 虚拟机承载;本轮 Colima VM 地址是 192.168.5.3,其默认路由指向 192.168.5.2。后者是 VM 视角下的 macOS 宿主网关,不是虚拟机地址,也不是评测网站。Shopping、Shopping Admin、Reddit、GitLab 四个官方站点镜像通过 Docker 端口发布到这个宿主入口,独立的 Chromium/Moli 浏览器容器据此访问本地站点。

    " + f"

    站点确实部署在本机,不是公网代站。macOS 上的 Docker 由 Colima 虚拟机承载;运行配置记录的 {_e(site_access_host)} 是浏览器容器访问 macOS 宿主发布端口所用的 Colima 网关,不是评测网站自身地址。Shopping、Shopping Admin、Reddit、GitLab 四个官方站点镜像通过 Docker 端口发布到这个入口,独立的 Chromium/Moli 浏览器容器据此访问本地站点。

    " f"

    WebArena‑Verified 当前提供 Shopping、Shopping Admin、Reddit、GitLab、Wikipedia、Map 六类环境。本轮 {task_count} 题只覆盖前四类;Wikipedia 与 Map 需要额外大型数据卷,因此没有部署,也没有任务进入成绩。

    " - "

    运行器代码在每个执行前调用 ContainerManager.start(reset=True),只保留该题所需站点;env-ctrl 报告 ready 后,再做连续应用健康检查、清浏览器状态、登录测试账号并打开起始页。当前冻结证据保存了代码提交、运行协议和镜像身份;事件流从环境准备完成后开始,因此逐次 reset、ready 与 health 结果未形成独立绑定的准备回执。新版运行协议已要求保存该回执。

    " - f"

    预注册文件只含任务 ID、层级和入选理由,不含点击序列。CowAgent 收到自然语言目标后,在最多 {max_agent_steps} 步内循环读取页面观察、请求模型决策并生成下一次 BrowserTool 调用。Chromium 与 Moli 分别连接独立 CDP 浏览器容器;每个逻辑结果保留最终 JSON、Agent 事件流、网络 HAR 和 Playwright trace。

    " + + ( + f"

    运行器在每个物理执行前调用 ContainerManager.start(reset=True),只保留该题所需站点;env-ctrl 报告 ready 后,再做连续应用健康检查、清浏览器状态、登录测试账号并打开起始页。本轮 {preparation_receipt_count}/{len(source_attempts)} 个物理执行都保存了环境准备回执,并与对应执行记录绑定。

    " + if preparation_receipts_complete + else "

    运行器在每个物理执行前调用 ContainerManager.start(reset=True),只保留该题所需站点;env-ctrl 报告 ready 后,再做连续应用健康检查、清浏览器状态、登录测试账号并打开起始页。当前证据尚未完整保存逐执行环境准备回执,因此不能把每次准备成功当成已独立核验的事实。

    " + ) + + f"

    预注册文件只含任务 ID、层级和入选理由,不含点击序列。CowAgent 收到自然语言目标后,在最多 {max_agent_steps} 步内循环读取页面观察、请求模型决策并生成下一次 BrowserTool 调用。Chromium 与 Moli 分别连接独立 CDP 浏览器容器;每个逻辑结果保留最终 JSON、Agent 事件流、网络 HAR 和 Playwright trace。

    " + _table( ["站点", "本轮访问地址", "应用端口", "环境控制端口", "状态"], [ - ["Shopping", "http://192.168.5.2:7770", "7770", "7771", "本轮已部署"], + ["Shopping", f"http://{site_access_host}:7770", "7770", "7771", "本轮已部署"], [ "Shopping Admin", - "http://192.168.5.2:7780/admin", + f"http://{site_access_host}:7780/admin", "7780", "7781", "本轮已部署", ], - ["Reddit", "http://192.168.5.2:9999", "9999", "9998", "本轮已部署"], - ["GitLab", "http://192.168.5.2:8023", "8023", "8024", "本轮已部署"], + ["Reddit", f"http://{site_access_host}:9999", "9999", "9998", "本轮已部署"], + ["GitLab", f"http://{site_access_host}:8023", "8023", "8024", "本轮已部署"], ["Wikipedia", "—", "8888", "8889", f"未部署;不在 {task_count} 题"], ["Map", "—", "3030", "3031", f"未部署;不在 {task_count} 题"], ], ) - + '

    证据边界:事件流能证明 CowAgent 在准备完成后产生了 BrowserTool 调用序列;镜像 ID、digest、代码提交与网关记录限定了本地运行身份。当前证据没有独立的逐次环境准备 receipt,因此报告不把“每次重置均成功”当成已经独立核验的事实。

    ' - '', + + ( + f'

    环境准备证据:{preparation_receipt_count}/{len(source_attempts)} 个物理执行均记录 reset、ready、应用健康检查和登录准备状态;事件流随后证明 CowAgent 自主产生 BrowserTool 调用序列。

    ' + if preparation_receipts_complete + else '

    证据边界:事件流能证明 CowAgent 在准备完成后产生了 BrowserTool 调用序列;镜像 ID、digest、代码提交与网关记录限定了本地运行身份。逐执行环境准备回执尚不完整。

    ' + ) + + '', ) methodology += _section( "Mac 上可以继续扩展哪些实例", @@ -2180,7 +2196,11 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: for site, image in site_images.items() ], ) - + '

    逐执行的环境准备 receipt 尚未包含在这批冻结证据中;现有记录可核对运行器配置与镜像身份,但不能独立重放每次 reset/ready/health 的完整证明链。

    ', + + ( + f'

    本轮 {preparation_receipt_count}/{len(source_attempts)} 个物理执行均保存环境准备回执;公开环境摘要保留逐项状态,原始登录态不发布。

    ' + if preparation_receipts_complete + else '

    逐执行环境准备回执尚不完整;现有记录只能核对运行器配置与镜像身份。

    ' + ), ) reproduce += _section( "公开复现入口", diff --git a/tests/browser_use/test_report.py b/tests/browser_use/test_report.py index 9a9c4e65d..82b3ae89f 100644 --- a/tests/browser_use/test_report.py +++ b/tests/browser_use/test_report.py @@ -309,6 +309,7 @@ def test_report_renders_measured_runtime_identities(tmp_path: Path) -> None: "reasoning_effort": "low", }, "environment": { + "site_access_host": "192.168.5.2", "docker_architecture": "aarch64", "docker_server_version": "28.0", "webarena_verified_version": "1.2.3", From 893a39afa33d8032f21ccc7c0217d5c42eb4d7a7 Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 11 Sep 2026 01:28:19 +0800 Subject: [PATCH 056/127] generate report conclusions from current evidence --- harness/browser_use/report.py | 41 ++++++++++++++++++++++++++++------- 1 file changed, 33 insertions(+), 8 deletions(-) diff --git a/harness/browser_use/report.py b/harness/browser_use/report.py index b960ece49..41fe4dc50 100644 --- a/harness/browser_use/report.py +++ b/harness/browser_use/report.py @@ -11,7 +11,7 @@ import shutil import statistics import tempfile -from collections import defaultdict +from collections import Counter, defaultdict from collections.abc import Callable, Iterable from pathlib import Path from typing import Any @@ -1575,6 +1575,33 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: c, m = summary["chromium"], summary["moli"] families = _task_family_summary(tasks, attempts) coverage = review.get("coverage") or {} + participant_score_cards = "".join( + '
    ' + + _e(f"{ENGINE_LABELS[engine]} 任务通过") + + f'
    {values["passed"]}/{values["completed"]}
    ' + + f'

    {_pct(values["score"])}

    ' + for engine, values in summary.items() + ) + cause_counts = Counter( + str((item.get("subcategory") or {}).get("zh") or "未分类") + for item in verified_causes.values() + ) + cause_cards = "".join( + f'
    {_e(category)}
    {count}

    个失败结果

    ' + for category, count in sorted(cause_counts.items()) + ) + if failed_result_ids and set(verified_causes) == failed_result_ids: + failure_overview = ( + f'
    {cause_cards}
    ' + f'

    根因核验:{len(verified_causes)}/{len(failed_result_ids)} 个失败结果已逐个绑定当前证据并完成核验。

    ' + '

    查看逐结果根因与证据边界 →

    ' + ) + elif failed_result_ids: + failure_overview = ( + f'

    根因核验:{len(verified_causes)}/{len(failed_result_ids)} 个失败结果已完成;未核验结果不展示推测性原因。

    ' + ) + else: + failure_overview = '

    根因核验:本轮没有失败结果。

    ' family_cards = "".join( '
    ' f'{_e(family["kind"])}' @@ -1591,14 +1618,12 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) home += _section( "先看结论", - f'
    Chromium 任务通过
    {c["passed"]}/{c["completed"]}

    {_pct(c["score"])}

    Moli 任务通过
    {m["passed"]}/{m["completed"]}

    {_pct(m["score"])}

    单边胜出
    {paired["chromium_only"] + paired["moli_only"]}

    {paired["paired"]} 个完整配对

    共同零分
    {paired["both_fail"]}

    不等于共同根因

    当前结论:本轮没有观察到浏览器成功率差异;这证明对照流程可比,不证明两个内核等价。

    根因结论:#68 是官方任务合同冲突,#124 是候选语义核验缺失,#142 是类别验收边界未定义,#168 是本项目响应协议错误;#506 的 Chromium 是 HAR 采集误判,Moli 是执行步数与完成确认问题。

    这是固定选择的 30 题试验,不是从全部 WebArena‑Verified 任务随机抽样;比例只描述本样本,不外推总体能力。

    ', + f'
    {participant_score_cards}
    单边通过
    {paired["chromium_only"] + paired["moli_only"]}

    {paired["paired"]} 个完整配对

    共同失败
    {paired["both_fail"]}

    不等于共同根因

    结果边界:本轮观察到 {paired["chromium_only"] + paired["moli_only"]} 个浏览器间不一致配对;结论只适用于这批受控任务,不证明内核等价或全面替代关系。

    这是固定选择的 {task_count} 题试验,不是从全部 WebArena‑Verified 任务随机抽样;比例只描述本样本,不外推总体能力。

    ', ) home += _section( - "共同失败是报告解读的核心证据", - "

    25:25 只说明当前样本没有产生浏览器分歧。理解这个结果必须继续看五个共同失败发生在哪个验收点,而不是把平分直接解释成内核等价。

    " - '
    任务合同问题
    2 题

    #68 schema 冲突;#142 类别口径缺失

    Agent 协议与策略
    2 题

    #124 未核验候选;#168 空结果状态错误

    不同根因的同分
    1 题

    #506:Chromium 采集误判;Moli 未确认完成

    ' - '

    处理原则:合同不稳定的任务退出下一轮样本;代码与协议缺陷修复后只通过新执行验证,不覆盖历史官方分数。

    ' - '

    查看五题根因、证据闭环与复测门槛 →

    ', + "失败根因是报告解读的核心证据", + "

    官方 evaluator 的错误信息只定位未满足的验收规则;根因核验继续检查任务合同、最终回答、网络记录和自主轨迹。未完成核验前,报告保留自动分类,但不展示推测性根因。

    " + + failure_overview, ) home += _section( "任务族覆盖", @@ -1638,7 +1663,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: guide = ( _hero( "Report interpretation", - "从 25 比 25 读出下一轮该怎么做", + f"从 {c['passed']}/{c['completed']} 对 {m['passed']}/{m['completed']} 读出下一轮该怎么做", "完整文章与首页共用同一份测量数据;另提供无 JavaScript 的独立导出版。", ) + _section( From ec81667599aa8aad19291b272c5dab894ff9ac76 Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 11 Sep 2026 01:37:45 +0800 Subject: [PATCH 057/127] localize dynamic browser report scope --- harness/browser_use/report.py | 4 +- harness/browser_use/report_i18n.py | 131 ++++++++++++++++++++++++++++- 2 files changed, 132 insertions(+), 3 deletions(-) diff --git a/harness/browser_use/report.py b/harness/browser_use/report.py index 41fe4dc50..1e4e1661c 100644 --- a/harness/browser_use/report.py +++ b/harness/browser_use/report.py @@ -1513,7 +1513,9 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) task_count = len(tasks) result_count = len(attempts) - max_agent_steps = int(config.get("max_agent_steps") or protocol.get("max_agent_steps") or 0) + max_agent_steps = str( + config.get("max_agent_steps") or protocol.get("max_agent_steps") or "未记录" + ) site_access_host = str(environment.get("site_access_host") or config.get("site_access_host") or "未记录") preparation_receipt_count = sum( Path(str((row.get("paths") or {}).get("environment_prepare") or "")).is_file() diff --git a/harness/browser_use/report_i18n.py b/harness/browser_use/report_i18n.py index 651516d05..6fa2b1c82 100644 --- a/harness/browser_use/report_i18n.py +++ b/harness/browser_use/report_i18n.py @@ -448,6 +448,35 @@ # Current home page and dataset-only inventory. "Chromium 任务通过": "Chromium tasks passed", "Moli 任务通过": "Moli tasks passed", + "单边通过": "One-browser pass", + "共同失败": "Both fail", + "结果边界:": "Result boundary:", + "失败根因是报告解读的核心证据": "Root causes are core interpretation evidence", + "官方 evaluator 的错误信息只定位未满足的验收规则;根因核验继续检查任务合同、最终回答、网络记录和自主轨迹。未完成核验前,报告保留自动分类,但不展示推测性根因。": "The official evaluator identifies the unmet acceptance rule. Root-cause review then checks the task contract, final answer, network record, and autonomous trajectory. Until that review is complete, the report retains the automatic classification but shows no speculative cause.", + "根因核验:": "Root-cause review:", + "本轮没有失败结果。": "This run has no failed results.", + "解释配对结果、失败根因和结论边界。": "Interpret paired outcomes, failure root causes, and conclusion boundaries.", + "筛选任务并下钻验收合同。": "Filter tasks and inspect their acceptance contracts.", + "一、先看总分,再看同题分歧": "1. Start with aggregate scores, then inspect task-level disagreements", + "二、失败结果必须追到根因": "2. Every failed result requires a root-cause review", + "三、简单、中等、困难都覆盖,但难度只表示操作长度": "3. Easy, medium, and hard are all covered, but difficulty means operation length only", + "四、选择 WebArena‑Verified 是为了可归因的结果验收": "4. WebArena-Verified was selected for attributable outcome evaluation", + "五、结论只适用于本轮受控样本": "5. Conclusions apply only to this controlled sample", + "程序化 evaluator 先给出失败环节;核验再检查最终执行的任务合同、回答、网络记录和 Agent 轨迹,形成更具体的根因子类与原因。共同失败不默认是共同根因,单边失败也不默认是浏览器缺陷。": "The programmatic evaluator first identifies the failed acceptance stage. Review then checks the score-bearing task contract, answer, network record, and agent trajectory to establish a specific root-cause subtype and reason. A shared failure does not imply a shared cause, and a one-browser failure does not automatically imply a browser defect.", + "本轮没有失败结果,无需生成失败根因记录。": "This run has no failed results, so no failure-cause records are needed.", + "先修复已核验根因并形成新证据,再扩大任务与浏览器范围;历史官方分数不被人工覆盖。": "Fix verified root causes and produce new evidence before expanding the task and browser scope. Historical official scores are never overwritten manually.", + "逻辑结果": "Logical result", + "根因子类": "Root-cause subtype", + "已核验根因": "Verified root cause", + "表 2|根因核验不修改官方分数;修复后必须产生新的执行证据。": "Table 2 | Root-cause review does not alter official scores; fixes require new execution evidence.", + "环境准备证据:": "Environment-preparation evidence:", + "未记录": "Not recorded", + "macOS 上的 Docker 由 Colima 虚拟机承载;运行配置记录的": "Docker on macOS runs inside a Colima virtual machine. The run configuration records", + "是浏览器容器访问 macOS 宿主发布端口所用的 Colima 网关,不是评测网站自身地址。Shopping、Shopping Admin、Reddit、GitLab 四个官方站点镜像通过 Docker 端口发布到这个入口,独立的 Chromium/Moli 浏览器容器据此访问本地站点。": "as the Colima gateway through which browser containers reach ports published on the macOS host; it is not the address of an evaluation website. The four official Shopping, Shopping Admin, Reddit, and GitLab images publish Docker ports to this entry point, which the isolated Chromium and Moli containers use to access the local sites.", + "运行器在每个物理执行前调用": "Before every physical execution, the runner calls", + ",只保留该题所需站点;env-ctrl 报告 ready 后,再做连续应用健康检查、清浏览器状态、登录测试账号并打开起始页。当前证据尚未完整保存逐执行环境准备回执,因此不能把每次准备成功当成已独立核验的事实。": ", retaining only the sites required by that task. After env-ctrl reports ready, it performs repeated application health checks, clears browser state, signs into the benchmark account, and opens the start page. The current evidence does not contain a complete preparation receipt for every execution, so successful preparation cannot be claimed as independently verified for every run.", + "事件流能证明 CowAgent 在准备完成后产生了 BrowserTool 调用序列;镜像 ID、digest、代码提交与网关记录限定了本地运行身份。逐执行环境准备回执尚不完整。": "The event stream shows that CowAgent generated BrowserTool calls after preparation. Image IDs, digests, code commits, and the recorded gateway bind the local runtime identity. Per-execution preparation receipts remain incomplete.", + "逐执行环境准备回执尚不完整;现有记录只能核对运行器配置与镜像身份。": "Per-execution environment-preparation receipts are incomplete; current records can verify only runner configuration and image identity.", "需要可完成性核验": "Solvability review required", "已确认失败点": "Confirmed failure points", "失败结论:": "Failure conclusion:", @@ -992,6 +1021,11 @@ def translate_text(value: str) -> str: f"{match.group(3)} of {match.group(2)} expected item(s) matched." ), ), + ( + r"Browser Use Capability Evaluation · (.+)", + lambda m: "Browser Use Capability Evaluation · " + + ("Not recorded" if m.group(1) == "未记录" else m.group(1)), + ), ( r"最终状态不符:期望 (.+),实际为 (.+)。", lambda match: ( @@ -999,11 +1033,104 @@ def translate_text(value: str) -> str: ), ), ( - r"在同一 CowAgent、同一 (.+?)(reasoning=low)和同一 WebArena‑Verified 30 题样本下,对比两种浏览器完成真实网页任务的能力。", + r"在同一 CowAgent、同一 (.+?)(reasoning=low)和同一 WebArena‑Verified (\d+) 题样本下,对比两种浏览器完成真实网页任务的能力。", lambda match: ( - f"Compare how Chromium and Moli complete real browser tasks with the same CowAgent, {match.group(1)} (reasoning=low), and the same 30-task WebArena-Verified sample." + f"Compare how Chromium and Moli complete real browser tasks with the same CowAgent, {match.group(1)} (reasoning=low), and the same {match.group(2)}-task WebArena-Verified sample." ), ), + ( + r"(.+?) 任务通过", + lambda match: f"{match.group(1)} tasks passed", + ), + ( + r"本轮观察到 (\d+) 个浏览器间不一致配对;结论只适用于这批受控任务,不证明内核等价或全面替代关系。", + lambda match: f"This run observed {match.group(1)} discordant browser pair(s). The conclusion applies only to this controlled cohort and does not establish browser-core equivalence or comprehensive substitutability.", + ), + ( + r"这是固定选择的 (\d+) 题试验,不是从全部 WebArena‑Verified 任务随机抽样;比例只描述本样本,不外推总体能力。", + lambda match: f"This is a fixed {match.group(1)}-task experiment, not a random sample of all WebArena-Verified tasks. Percentages describe this cohort only and are not population estimates.", + ), + ( + r"从 (\d+)/(\d+) 对 (\d+)/(\d+) 读出下一轮该怎么做", + lambda match: f"What {match.group(1)}/{match.group(2)} versus {match.group(3)}/{match.group(4)} implies for the next run", + ), + ( + r"(\d+)/(\d+) 对 (\d+)/(\d+):(\d+) 个任务出现浏览器分歧", + lambda match: f"{match.group(1)}/{match.group(2)} versus {match.group(3)}/{match.group(4)}: {match.group(5)} task(s) show a browser disagreement", + ), + ( + r"同一 CowAgent、模型、任务、站点状态和 evaluator 下,只替换浏览器后端。总分给出结果,(\d+) 个同题配对用于定位差异;每个失败结果还要沿官方合同、最终回答、网络证据和自主轨迹追到可复核根因。", + lambda match: f"The browser backend is the only changed variable; CowAgent, model, tasks, site state, and evaluator are held constant. Aggregate scores summarize outcomes, while {match.group(1)} paired tasks locate differences. Every failed result is then traced through the official contract, final answer, network evidence, and autonomous trajectory to a reviewable root cause.", + ), + ( + r"Chromium 与 Moli 分别通过 (\d+)/(\d+) 和 (\d+)/(\d+)。逐题配对后,(\d+) 题共同通过、(\d+) 题共同失败、(\d+) 题仅 Chromium 通过、(\d+) 题仅 Moli 通过。", + lambda m: f"Chromium and Moli passed {m.group(1)}/{m.group(2)} and {m.group(3)}/{m.group(4)}, respectively. Across paired tasks, both passed {m.group(5)}, both failed {m.group(6)}, Chromium alone passed {m.group(7)}, and Moli alone passed {m.group(8)}.", + ), + ( + r"表 1|(\d+) 个任务的逐题最终结果配对。", + lambda match: f"Table 1 | Paired final outcomes for {match.group(1)} tasks.", + ), + ( + r"固定样本中观察到 (\d+) 个不一致配对。这个数字用于发现浏览器相关差异,但不能单独证明整个浏览器内核的优劣,也不对 WebArena‑Verified 全集作统计外推。", + lambda match: f"The fixed cohort contains {match.group(1)} discordant pair(s). This identifies browser-related differences, but does not by itself establish overall browser-core superiority or support statistical generalization to the complete WebArena-Verified benchmark.", + ), + ( + r"WebArena‑Verified 没有官方 easy/medium/hard 三档。本项目在运行前按最少可见 UI 操作数注册为 1–4、5–7、8\+ 三档,配额 (\d+)/(\d+)/(\d+)。它控制操作链覆盖,不等同于推理难度。", + lambda m: f"WebArena-Verified has no official easy/medium/hard tiers. Before execution, this project registered tasks by minimum visible UI actions as 1-4, 5-7, and 8+, with quotas of {m.group(1)}/{m.group(2)}/{m.group(3)}. This controls operation-chain coverage and is not equivalent to reasoning difficulty.", + ), + ( + r"本轮固定 (\d+) 题,覆盖四个已验证站点。结果只说明当前 CowAgent、模型、任务与运行协议下的浏览器差异;不支持把总分直接解释为浏览器内核等价、全面替代关系或纯内核速度。端到端耗时包含模型、调度、页面等待和工具开销,动作延迟也包含 CDP 往返。", + lambda match: f"This run fixes {match.group(1)} tasks across four verified sites. Results describe browser differences only under the current CowAgent, model, tasks, and run protocol. Aggregate scores do not establish browser-core equivalence, comprehensive substitutability, or pure engine speed. End-to-end latency includes model, scheduling, page waits, and tool overhead; action latency also includes CDP round trips.", + ), + ( + r"(\d+) 个逻辑结果", + lambda match: f"{match.group(1)} logical results", + ), + ( + r"(\d+) 个任务×浏览器结果均保留最终回答、官方 evaluator 结果、Agent 事件和证据哈希。", + lambda match: f"All {match.group(1)} task-browser results retain the final answer, official evaluator output, agent events, and evidence hashes.", + ), + ( + r"(\d+)/(\d+) 个逻辑结果已覆盖,(\d+) 个未决。它不重新判题,也不是独立语义复核。", + lambda m: f"Evidence-integrity review covers {m.group(1)}/{m.group(2)} logical results, with {m.group(3)} unresolved. It does not rescore tasks or constitute independent semantic review.", + ), + (r"预注册 (\d+) 题", lambda m: f"Preregister {m.group(1)} tasks"), + ( + r"这 (\d+) 题如何部署并由 Agent 执行", + lambda m: f"How the {m.group(1)} tasks were deployed and run by the agent", + ), + ( + r"WebArena‑Verified 当前提供 Shopping、Shopping Admin、Reddit、GitLab、Wikipedia、Map 六类环境。本轮 (\d+) 题只覆盖前四类;Wikipedia 与 Map 需要额外大型数据卷,因此没有部署,也没有任务进入成绩。", + lambda m: f"WebArena-Verified provides six environments: Shopping, Shopping Admin, Reddit, GitLab, Wikipedia, and Map. This {m.group(1)}-task run covers only the first four. Wikipedia and Map require additional large data volumes, so they were not deployed and contribute no scored tasks.", + ), + ( + r"预注册文件只含任务 ID、层级和入选理由,不含点击序列。CowAgent 收到自然语言目标后,在最多 (.+) 步内循环读取页面观察、请求模型决策并生成下一次 BrowserTool 调用。Chromium 与 Moli 分别连接独立 CDP 浏览器容器;每个逻辑结果保留最终 JSON、Agent 事件流、网络 HAR 和 Playwright trace。", + lambda m: f"The preregistration contains task IDs, tiers, and selection rationales, but no click sequence. Given a natural-language objective, CowAgent repeatedly reads the page, requests a model decision, and generates the next BrowserTool call for up to {'an unrecorded number of' if m.group(1) == '未记录' else m.group(1)} steps. Chromium and Moli connect to separate CDP browser containers. Every logical result retains the final JSON, agent event stream, network HAR, and Playwright trace.", + ), + ( + r"未部署;不在 (\d+) 题", + lambda m: f"Not deployed; outside the {m.group(1)} tasks", + ), + ( + r"给出全部 (\d+) 个逻辑结果的页面与下载路径。", + lambda m: f"lists page and download paths for all {m.group(1)} logical results.", + ), + ( + r"同一 (\d+) 题;同一批本地站点镜像", + lambda m: f"Same {m.group(1)} tasks; same local site images", + ), + ( + r"相同 CPU、内存、viewport 与最多 (.+) 步", + lambda m: f"Same CPU, memory, viewport, and up to {'an unrecorded number of' if m.group(1) == '未记录' else m.group(1)} steps", + ), + ( + r"这 (\d+) 题由 CowAgent 自主执行。", + lambda m: f"CowAgent executes all {m.group(1)} tasks autonomously.", + ), + ( + r"仓库中的 Browser Trajectory Replay(轨迹重放)固定动作微基准属于另一条实验轨道,不是本报告的参评选手,也不计入这 (\d+) 题成绩。", + lambda m: f"The repository's fixed-action Browser Trajectory Replay microbenchmark is a separate evaluation track, not a participant in this report, and is excluded from these {m.group(1)} task results.", + ), ( r"CowAgent \+ (.+?)(reasoning=low)在预注册的 (\d+) 题 WebArena‑Verified 样本上对比 Chromium 与 Moli。", lambda match: ( From ed56f79dfeb943235e7d6c95f04323bfd9868b7a Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 11 Sep 2026 01:51:51 +0800 Subject: [PATCH 058/127] recover transient browser context readiness --- harness/browser_use/runner.py | 24 +++++++++-- tests/browser_use/test_runner.py | 69 +++++++++++++++++++++++++++++++- 2 files changed, 89 insertions(+), 4 deletions(-) diff --git a/harness/browser_use/runner.py b/harness/browser_use/runner.py index 44bd9f053..f09de88bd 100644 --- a/harness/browser_use/runner.py +++ b/harness/browser_use/runner.py @@ -373,7 +373,7 @@ def _login_context( sites: tuple[WebArenaSite, ...], benchmark: WebArenaVerified, start_urls: list[str], -) -> None: +) -> int: def login() -> None: context = service._context assert context is not None @@ -430,7 +430,21 @@ def login() -> None: primary_page.goto(start_urls[0], wait_until="domcontentloaded") primary_page.wait_for_timeout(500) - service._submit(login) + readiness_retries = 0 + try: + service._submit(login) + except Exception as exc: + # Moli can expose its CDP endpoint just before the default browser + # context is ready. This is a session precondition failure: no Agent + # step has started and no score-bearing attempt exists yet. Recover it + # once, record the recovery on the eventual attempt, and keep every + # other setup failure fatal. + if "BrowserContextNotLoaded" not in str(exc): + raise + readiness_retries = 1 + time.sleep(1) + service._submit(login) + return readiness_retries def _system_prompt() -> str: @@ -586,6 +600,7 @@ def _run_attempt( # noqa: PLR0913 recorder = CdpHarRecorder() sampler_started = False browser_session_setup_ms = 0.0 + browser_session_readiness_retries = 0 agent_run_ms = 0.0 evidence_finalize_ms = 0.0 evaluation_ms = 0.0 @@ -595,7 +610,9 @@ def _run_attempt( # noqa: PLR0913 model.reset() service = tool._get_service() service._submit(lambda: None) - _login_context(service, sites, benchmark, start_urls) + browser_session_readiness_retries = _login_context( + service, sites, benchmark, start_urls + ) service._submit( lambda: ( recorder.start(service._context), @@ -716,6 +733,7 @@ def _run_attempt( # noqa: PLR0913 "evaluation_ms": round(evaluation_ms, 3), "measured_run_wall_ms": round(duration_ms, 3), }, + "browser_session_readiness_retries": browser_session_readiness_retries, "agent_response": parsed_response, "error": error, "cleanup_errors": cleanup_errors, diff --git a/tests/browser_use/test_runner.py b/tests/browser_use/test_runner.py index e9b7e2b7f..17e53f416 100644 --- a/tests/browser_use/test_runner.py +++ b/tests/browser_use/test_runner.py @@ -135,12 +135,79 @@ class Service: def _submit(self, callback): return callback() - _login_context(Service(), (), object(), ["http://official.test/start"]) + readiness_retries = _login_context( + Service(), (), object(), ["http://official.test/start"] + ) + assert readiness_retries == 0 assert stale.closed is True assert primary.url == "http://official.test/start" +def test_login_context_recovers_one_transient_unloaded_context(monkeypatch) -> None: + calls = 0 + + class Service: + def _submit(self, callback): + nonlocal calls + calls += 1 + if calls == 1: + raise RuntimeError("Protocol error: BrowserContextNotLoaded") + return callback() + + class Context: + pages = [] + + @staticmethod + def clear_cookies() -> None: + pass + + @staticmethod + def set_extra_http_headers(_headers) -> None: + pass + + class Page: + @staticmethod + def set_default_timeout(_timeout: int) -> None: + pass + + @staticmethod + def set_default_navigation_timeout(_timeout: int) -> None: + pass + + @staticmethod + def goto(_url: str, **_kwargs) -> None: + pass + + @staticmethod + def wait_for_timeout(_timeout: int) -> None: + pass + + _context = Context() + _page = Page() + + monkeypatch.setattr("harness.browser_use.runner.time.sleep", lambda _seconds: None) + + assert _login_context(Service(), (), object(), ["http://official.test/start"]) == 1 + assert calls == 2 + + +def test_login_context_does_not_retry_other_setup_errors(monkeypatch) -> None: + calls = 0 + + class Service: + def _submit(self, _callback): + nonlocal calls + calls += 1 + raise RuntimeError("authentication failed") + + monkeypatch.setattr("harness.browser_use.runner.time.sleep", lambda _seconds: None) + + with pytest.raises(RuntimeError, match="authentication failed"): + _login_context(Service(), (), object(), ["http://official.test/start"]) + assert calls == 1 + + def test_container_host_gateway_returns_literal_ip() -> None: class Result: exit_code = 0 From 9b571f476f1a35122bad381611fa9e4efa2b2245 Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 11 Sep 2026 01:53:38 +0800 Subject: [PATCH 059/127] make browser report article check data independent --- scripts/check_browser_use_report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/check_browser_use_report.py b/scripts/check_browser_use_report.py index 79000d426..3c47cedb2 100644 --- a/scripts/check_browser_use_report.py +++ b/scripts/check_browser_use_report.py @@ -236,7 +236,7 @@ def check( standalone = (report_dir / "guide/article.html").read_text(encoding="utf-8") if " Date: Fri, 11 Sep 2026 07:07:31 +0800 Subject: [PATCH 060/127] publish reviewed 90-task browser use report --- evaluation/browser-use/current-report.json | 25 ++++- harness/browser_use/report.py | 4 +- harness/browser_use/report_i18n.py | 3 + .../browser-use-failure-cause-review.json | 97 ++++++++----------- research/report-changes.json | 44 +++++++++ scripts/check_browser_use_report.py | 17 ++-- scripts/check_browser_use_visual.py | 13 ++- 7 files changed, 132 insertions(+), 71 deletions(-) diff --git a/evaluation/browser-use/current-report.json b/evaluation/browser-use/current-report.json index c9f355bbf..1b67a242d 100644 --- a/evaluation/browser-use/current-report.json +++ b/evaluation/browser-use/current-report.json @@ -24,14 +24,31 @@ "independent_semantic_review": false, "failure_cause_review": { "required_for_publication": true, - "source_sha256": null + "source_sha256": "3b05bf34914a52dddbd740ac3174bcc0dc457467bee64e8c6cf043a27228043f" }, - "tasks_without_measured_success_path": [], + "tasks_without_measured_success_path": [ + 124, + 258, + 279, + 284, + 339, + 464, + 470, + 475, + 510, + 528, + 547, + 600, + 625, + 676, + 694, + 699 + ], "publication": { "base_url": "https://artifact-hub.dev.lexmount.net", "site_slug": "28OH4YZkUYF1", "share_url": "https://artifact-hub.dev.lexmount.net/v/5Vhnhdc_f8FEM2RzctQm-ChRlXUf174Y", - "expected_version": "ver_0bb766584e944330995bb009c4562dc4", - "state": "pending-90-task-evidence" + "expected_version": "ver_0f62710dd3a24b10adbc24b1b1635acb", + "state": "verified" } } diff --git a/harness/browser_use/report.py b/harness/browser_use/report.py index 1e4e1661c..e0d7a5116 100644 --- a/harness/browser_use/report.py +++ b/harness/browser_use/report.py @@ -797,7 +797,7 @@ def _summary_comparison_rows(summary: dict[str, dict[str, Any]]) -> list[list[An REPORT_STYLE = ( SHARED_STYLE + """ -.hero h1,.publication-article h1,.publication-article h2{overflow-wrap:break-word;word-break:normal} +.hero h1,.publication-article h1,.publication-article h2{overflow-wrap:anywhere;word-break:break-word;max-width:100%} .callout strong{margin-right:.3em} .top nav{gap:18px;flex-wrap:nowrap;overflow-x:auto;overscroll-behavior-inline:contain;scrollbar-width:thin}.top nav>a,.top nav>button{flex:0 0 auto}.top nav .brand{font-weight:850;color:var(--ink)}.top nav a[aria-current=page]{color:var(--ink);font-weight:800;text-decoration:underline;text-decoration-thickness:2px;text-underline-offset:8px}.top .theme-toggle{margin-left:auto}.page-main{min-height:78vh}.hero.compact{padding-bottom:22px}.hero.compact h1{font-size:clamp(34px,4.5vw,52px)}.fine{font-size:13px;color:var(--muted)}.result-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px}.result-grid .card{border-top:3px solid var(--accent)}.overview-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.overview-card{border:1px solid var(--line);border-radius:14px;background:var(--panel);padding:20px;min-width:0}.overview-card h3{margin:8px 0}.audit-strip{display:grid;gap:10px}.audit-strip .callout{margin:0}.filters,.pair-controls{display:flex;gap:10px;flex-wrap:wrap;align-items:end;margin:18px 0}.filters select,.filters input,.pair-controls select{padding:9px 12px;border:1px solid var(--line);border-radius:9px;background:var(--panel);color:var(--ink)}.filters input{min-width:260px}.comparison-count{margin-left:auto;padding:8px 12px;border-radius:999px;background:var(--wash);color:var(--accent);font-weight:750}.status-pass{color:var(--good);font-weight:750}.status-fail{color:var(--bad);font-weight:750}.status-unproven{color:var(--warn,var(--bad));font-weight:750}.flow{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:9px;list-style:none;padding:0}.flow li{padding:14px;border:1px solid var(--line);border-radius:11px;background:var(--panel);font-size:13px;min-width:0}.flow li+li:before{content:'→';color:var(--accent);margin-right:7px}.method-figure{margin:24px 0}.method-figure img{display:block;width:100%;height:auto;border:1px solid var(--line);border-radius:16px;background:#f6faff}.method-figure figcaption{margin-top:10px;color:var(--muted);font-size:13px;line-height:1.55}.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:12px}.detail-grid .kv{min-height:92px;min-width:0}.page-links{display:flex;gap:10px;flex-wrap:wrap}.page-links a{border:1px solid var(--line);background:var(--panel);padding:8px 12px;border-radius:9px}.rank-list{counter-reset:rank}.rank-item{counter-increment:rank;display:grid;grid-template-columns:54px minmax(0,1fr);gap:16px;padding:20px 0;border-top:1px solid var(--line)}.rank-item:before{content:counter(rank);font-size:34px;font-weight:850;color:var(--accent)}.rank-item h3{margin:4px 0}.article-entry{border-left:4px solid var(--accent)}.source-list li{margin:9px 0}.table-wrap{margin:18px 0}.table-wrap table{min-width:760px}#task-table{min-width:1280px}#pair-table{min-width:1180px}.table-wrap caption{text-align:left;padding:10px 12px;color:var(--muted);caption-side:bottom}.footer{border-top:1px solid var(--line)}.evidence-pair{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:14px}.evidence-card{border:1px solid var(--line);border-radius:12px;background:var(--panel);padding:16px;min-width:0}.evidence-card h3{margin-top:0}.json-view,.log-view{white-space:pre-wrap;overflow-wrap:anywhere;max-height:540px;overflow:auto;padding:14px;border:1px solid var(--line);border-radius:9px;background:var(--bg);font-size:12px;line-height:1.55}.trajectory{list-style:none;padding:0;counter-reset:step}.trajectory>li{counter-increment:step;display:grid;grid-template-columns:46px minmax(0,1fr);gap:12px;padding:15px 0;border-top:1px solid var(--line)}.trajectory>li:before{content:counter(step);display:grid;place-items:center;width:34px;height:34px;border-radius:50%;background:var(--accent);color:white;font-weight:800}.trajectory summary{cursor:pointer;font-weight:750}.rule-fail{border-left:4px solid var(--bad)}.rule-pass{border-left:4px solid var(--good)}.count-note{margin-left:auto;color:var(--muted);font-size:13px}.badge{display:inline-block;padding:3px 8px;border-radius:999px;background:var(--wash);font-size:12px;margin-right:6px}.muted-box{padding:14px;border:1px dashed var(--line);border-radius:10px;color:var(--muted)}@media(max-width:900px){.result-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.overview-grid{grid-template-columns:1fr}.flow{grid-template-columns:repeat(3,minmax(0,1fr))}.evidence-pair{grid-template-columns:1fr}.grid>.card{grid-column:1/-1!important}}@media(max-width:600px){.wrap{width:calc(100% - 28px)}.top nav{gap:14px}.result-grid,.flow{grid-template-columns:1fr}.flow li+li:before{content:'↓';display:block}.hero{padding-top:28px}.hero.compact h1{font-size:30px;line-height:1.18}.filters input{min-width:0;width:100%}.comparison-count{margin-left:0}.rank-item{grid-template-columns:36px minmax(0,1fr)}}@media print{.top,.filters,.pair-controls{display:none!important}.card,.table-wrap{box-shadow:none}.json-view,.log-view{max-height:none;overflow:visible}.evidence-pair{grid-template-columns:1fr}} /* Publication QA overrides: readable dense tables, code, and text-heavy figures. */ @@ -836,7 +836,7 @@ def _summary_comparison_rows(summary: dict[str, dict[str, Any]]) -> list[list[An .section[id]{scroll-margin-top:76px} @media(max-width:980px){.attempt-summary{grid-template-columns:repeat(2,minmax(0,1fr))}.attempt-verdict{grid-column:1/-1}.attempt-secondary{grid-template-columns:repeat(2,minmax(0,1fr))}} @media(max-width:760px){.browser-comparison{padding:18px}.comparison-heading{display:block}.comparison-count{display:inline-block;margin-top:12px}.comparison-controls{grid-template-columns:1fr}.action-card-grid{grid-template-columns:1fr}} -@media(max-width:600px){.method-figure img{min-width:900px}.table-wrap{margin-left:-4px;margin-right:-4px}.json-view,.log-view{font-size:13px}.review-round header{display:block}.review-status{display:inline-block;margin-top:8px}.review-fields,.attempt-summary,.attempt-secondary{grid-template-columns:1fr}} +@media(max-width:600px){.hero h1{word-break:break-all;line-break:anywhere;text-wrap:balance}.method-figure img{min-width:900px}.table-wrap{margin-left:-4px;margin-right:-4px}.json-view,.log-view{font-size:13px}.review-round header{display:block}.review-status{display:inline-block;margin-top:8px}.review-fields,.attempt-summary,.attempt-secondary{grid-template-columns:1fr}} """ ) NAV = [ diff --git a/harness/browser_use/report_i18n.py b/harness/browser_use/report_i18n.py index 6fa2b1c82..73294207d 100644 --- a/harness/browser_use/report_i18n.py +++ b/harness/browser_use/report_i18n.py @@ -409,6 +409,9 @@ "优": "Best", "更优": "Better", "单次均值更低": "Lower mean per call", + "通过数更高": "Higher pass count", + "通过率更高": "Higher pass rate", + "同组通过数更高": "Higher pass count in group", "结果详情导航": "Result detail navigation", "环境与复现": "Reproduction", "变更记录": "Changes", diff --git a/research/browser-use-failure-cause-review.json b/research/browser-use-failure-cause-review.json index 910103331..db842ea96 100644 --- a/research/browser-use-failure-cause-review.json +++ b/research/browser-use-failure-cause-review.json @@ -1,70 +1,57 @@ { "schema": "lex_browser_use_failure_cause_review/1", "benchmark": "WebArena-Verified 1.2.3", - "reviewed_at": "2026-09-10", + "reviewed_at": "2026-09-11", "reviewer_class": "evidence analyst", "bindings": { - "attempts_sha256": "517b613adcc40fc0ebe41af2ab2115d76f49faaba5378b426cc196b5b633e9c7", - "manifest_sha256": "23a747e6286e7b8c28c07abae83b343892f6376e9aae1dcc0f3ab4da98271cae", - "selection_sha256": "ba3c344df003dd4eba3a5d1ef97162ee98df2ed728f7c29c4188bc4247982d38" + "attempts_sha256": "73146500152db62d79f84b63c85a14027c005a6320c19bce7426651f167466bd", + "manifest_sha256": "5466f30619d19ffda87689a1b31c9c6782b8837f72e4acdf6020f48c2fe89422", + "selection_sha256": "2cb716552e36df7535e83ed6d2cf3f84283e3aca86072817cd6e039a38547c7b" }, "review_protocol": { "unit": "one score-bearing logical result per browser and task", "method": "manual root-cause verification against the official task contract, score-bearing final response, evaluator assertions, network evidence, and autonomous Agent event trajectory", "rule": "automatic evaluator classification remains available; root-cause subcategory and reason remain empty until status is verified" }, - "coverage": {"failed_results": 10, "verified_results": 10}, + "coverage": {"failed_results": 37, "verified_results": 37}, "unresolved": [], "results": { - "chromium-68": { - "status": "verified", - "subcategory": {"code": "benchmark_contract", "zh": "评测合同缺陷", "en": "Benchmark contract defect"}, - "reason": {"zh": "官方 expected 要求第二条图书记录的 author 为 null,但同题公开 results_schema 规定 author 只能是 string;官方答案与自身输出合同冲突。", "en": "The official expected value requires a null author for the second book record, while the task's published results_schema permits only a string; the official answer conflicts with its own output contract."} - }, - "moli-68": { - "status": "verified", - "subcategory": {"code": "benchmark_contract", "zh": "评测合同缺陷", "en": "Benchmark contract defect"}, - "reason": {"zh": "官方 expected 要求第二条图书记录的 author 为 null,但同题公开 results_schema 规定 author 只能是 string;官方答案与自身输出合同冲突。", "en": "The official expected value requires a null author for the second book record, while the task's published results_schema permits only a string; the official answer conflicts with its own output contract."} - }, - "chromium-124": { - "status": "verified", - "subcategory": {"code": "agent_strategy", "zh": "Agent 策略缺陷", "en": "Agent strategy defect"}, - "reason": {"zh": "Agent 直接对 Magento 全文搜索结果取价格极值,没有逐项确认商品是否属于“无线耳机”;最终把投影仪 17774.32 当成目标品类最高价。", "en": "The Agent took a price extreme directly from Magento full-text search results without verifying that each item was a wireless earphone; it returned a 17774.32 projector price as the category maximum."} - }, - "moli-124": { - "status": "verified", - "subcategory": {"code": "agent_strategy", "zh": "Agent 策略缺陷", "en": "Agent strategy defect"}, - "reason": {"zh": "Agent 直接对 Magento 全文搜索结果取价格极值,没有逐项确认商品是否属于“无线耳机”;最终把投影仪 17774.32 当成目标品类最高价。", "en": "The Agent took a price extreme directly from Magento full-text search results without verifying that each item was a wireless earphone; it returned a 17774.32 projector price as the category maximum."} - }, - "chromium-142": { - "status": "verified", - "subcategory": {"code": "benchmark_acceptance", "zh": "验收边界缺失", "en": "Undefined acceptance boundary"}, - "reason": {"zh": "任务未给出“hair care and hair style”的可观察归类规则。Agent 按商品名纳入 11.99 的 Crystal Headband,官方答案却排除该商品,现有页面和合同无法推出这一边界。", "en": "The task gives no observable membership rule for 'hair care and hair style'. The Agent included the 11.99 Crystal Headband by product name, while the official answer excludes it; the boundary cannot be derived from the page or contract."} - }, - "moli-142": { - "status": "verified", - "subcategory": {"code": "benchmark_acceptance", "zh": "验收边界缺失", "en": "Undefined acceptance boundary"}, - "reason": {"zh": "任务未给出“hair care and hair style”的可观察归类规则。Agent 按商品名纳入 11.99 的 Crystal Headband,官方答案却排除该商品,现有页面和合同无法推出这一边界。", "en": "The task gives no observable membership rule for 'hair care and hair style'. The Agent included the 11.99 Crystal Headband by product name, while the official answer excludes it; the boundary cannot be derived from the page or contract."} - }, - "chromium-168": { - "status": "verified", - "subcategory": {"code": "harness_protocol", "zh": "Agent 适配协议缺陷", "en": "Agent adapter protocol defect"}, - "reason": {"zh": "站点中没有符合条件的项目,但本项目系统协议错误地要求所有 RETRIEVE 返回列表,且未规定零结果必须映射为 NOT_FOUND_ERROR;Agent 因而输出 SUCCESS 与空列表。", "en": "No matching project existed, but this project's system protocol incorrectly required every RETRIEVE result to be a list and did not map zero results to NOT_FOUND_ERROR; the Agent therefore emitted SUCCESS with an empty list."} - }, - "moli-168": { - "status": "verified", - "subcategory": {"code": "harness_protocol", "zh": "Agent 适配协议缺陷", "en": "Agent adapter protocol defect"}, - "reason": {"zh": "站点中没有符合条件的项目,但本项目系统协议错误地要求所有 RETRIEVE 返回列表,且未规定零结果必须映射为 NOT_FOUND_ERROR;Agent 因而输出 SUCCESS 与空列表。", "en": "No matching project existed, but this project's system protocol incorrectly required every RETRIEVE result to be a list and did not map zero results to NOT_FOUND_ERROR; the Agent therefore emitted SUCCESS with an empty list."} - }, - "chromium-506": { - "status": "verified", - "subcategory": {"code": "evidence_capture", "zh": "证据采集缺陷", "en": "Evidence-capture defect"}, - "reason": {"zh": "结账实际完成,成功页和订单号已出现,response、商品、付款及成功页规则均通过;只有购物车总额规则失败,因为 HAR 到运行结束才读取付款响应体,此时 CDP 已淘汰该 body。该零分是采集假阴性。", "en": "Checkout completed and the success page and order number appeared; response, product, payment, and success-page checks passed. Only the cart-total rule failed because the HAR collector fetched the payment response body at run end after CDP had evicted it. This zero score is a collection false negative."} - }, - "moli-506": { - "status": "verified", - "subcategory": {"code": "completion_budget", "zh": "完成确认失败", "en": "Completion-confirmation failure"}, - "reason": {"zh": "第 30 个决策回合才发出 Place order 点击,之后没有剩余回合观察付款结果;证据中没有付款或成功事件,但超限收尾仍生成成功形态回答,导致任务未被确认完成。", "en": "The Agent issued the Place order click only on decision turn 30, leaving no turn to observe the payment result. The evidence contains no payment or success event, yet max-step finalization emitted a success-shaped answer, so completion was never confirmed."} - } + "chromium-124": {"status":"verified","subcategory":{"code":"agent_retrieval_coverage","zh":"检索覆盖不足","en":"Incomplete retrieval coverage"},"reason":{"zh":"Agent 只覆盖部分无线耳机,最高价返回 169.99,遗漏官方全集的 298.0 上界。","en":"The Agent covered only part of the wireless-earphone set, returning 169.99 and missing the official 298.0 upper bound."}}, + "moli-124": {"status":"verified","subcategory":{"code":"agent_retrieval_coverage","zh":"检索覆盖不足","en":"Incomplete retrieval coverage"},"reason":{"zh":"Agent 只覆盖部分无线耳机,最高价返回 199.9,遗漏官方全集的 298.0 上界。","en":"The Agent covered only part of the wireless-earphone set, returning 199.9 and missing the official 298.0 upper bound."}}, + "chromium-258": {"status":"verified","subcategory":{"code":"agent_navigation_semantics","zh":"导航语义错误","en":"Navigation-semantics error"},"reason":{"zh":"Agent 停在 GitLab Explore 总页 /explore,没有启用 Public 过滤参数 visibility_level=20。","en":"The Agent stopped at generic /explore without applying the Public filter visibility_level=20."}}, + "moli-258": {"status":"verified","subcategory":{"code":"agent_navigation_semantics","zh":"导航语义错误","en":"Navigation-semantics error"},"reason":{"zh":"Agent 停在 GitLab Explore 总页 /explore,没有启用 Public 过滤参数 visibility_level=20。","en":"The Agent stopped at generic /explore without applying the Public filter visibility_level=20."}}, + "chromium-279": {"status":"verified","subcategory":{"code":"agent_retrieval_coverage","zh":"检索覆盖不足","en":"Incomplete retrieval coverage"},"reason":{"zh":"Agent 仅收集 7/12 个 Sony 蓝牙耳机,遗漏条目同时包含 18.99 最低价和 406.0 最高价。","en":"The Agent collected only 7 of 12 Sony Bluetooth headphones, omitting both the 18.99 minimum and 406.0 maximum."}}, + "moli-279": {"status":"verified","subcategory":{"code":"agent_retrieval_coverage","zh":"检索覆盖不足","en":"Incomplete retrieval coverage"},"reason":{"zh":"Agent 仅收集 6/12 个 Sony 蓝牙耳机,遗漏条目同时包含 18.99 最低价和 406.0 最高价。","en":"The Agent collected only 6 of 12 Sony Bluetooth headphones, omitting both the 18.99 minimum and 406.0 maximum."}}, + "chromium-284": {"status":"verified","subcategory":{"code":"agent_comparison_strategy","zh":"候选比较错误","en":"Candidate-comparison error"},"reason":{"zh":"Agent 选择了可放 20 双鞋的 Kingpavonini,没有完成所有合格候选的价格比较;官方最低价是 OnlyEasy 24-pocket。","en":"The Agent chose a 20-pair Kingpavonini rack without completing the price comparison; the official cheapest qualifying item is OnlyEasy 24-pocket."}}, + "moli-284": {"status":"verified","subcategory":{"code":"agent_comparison_strategy","zh":"候选比较错误","en":"Candidate-comparison error"},"reason":{"zh":"Agent 选择了 72 双容量的 Aeitc,没有完成所有合格候选的价格比较;官方最低价是 OnlyEasy 24-pocket。","en":"The Agent chose a 72-pair Aeitc rack without completing the price comparison; the official cheapest qualifying item is OnlyEasy 24-pocket."}}, + "chromium-339": {"status":"verified","subcategory":{"code":"benchmark_route_contract","zh":"路由验收缺陷","en":"Route-acceptance defect"},"reason":{"zh":"Agent 已打开含 state=opened 与 label_name[]=bug 的正确页面;官方规则错误要求 /issues/.*,拒绝合法的无尾斜杠 /issues?... 路由。","en":"The Agent opened the correct filtered page; the official /issues/.* rule rejects the valid no-trailing-slash /issues?... route."}}, + "moli-339": {"status":"verified","subcategory":{"code":"benchmark_route_contract","zh":"路由验收缺陷","en":"Route-acceptance defect"},"reason":{"zh":"Agent 已打开含 state=opened 与 label_name[]=bug 的正确页面;官方规则错误要求 /issues/.*,拒绝合法的无尾斜杠 /issues?... 路由。","en":"The Agent opened the correct filtered page; the official /issues/.* rule rejects the valid no-trailing-slash /issues?... route."}}, + "chromium-464": {"status":"verified","subcategory":{"code":"agent_step_budget","zh":"步骤预算耗尽","en":"Step-budget exhaustion"},"reason":{"zh":"Agent 在 40 步内未完成统计评论、编辑描述和保存,证据中没有目标商品保存请求。","en":"The Agent did not finish counting reviews, editing, and saving within 40 steps; no target-product save request exists."}}, + "moli-464": {"status":"verified","subcategory":{"code":"agent_step_budget","zh":"步骤预算耗尽","en":"Step-budget exhaustion"},"reason":{"zh":"Agent 在 40 步内未完成统计评论、编辑描述和保存,证据中没有目标商品保存请求。","en":"The Agent did not finish counting reviews, editing, and saving within 40 steps; no target-product save request exists."}}, + "moli-448": {"status":"verified","subcategory":{"code":"evidence_capture","zh":"网络证据匹配缺陷","en":"Network-evidence matching defect"},"reason":{"zh":"HAR 有字段正确的 302 /profile POST,页面也已更新;同一提交又被记录为 200,evaluator 只选中 200 记录而误判。","en":"The HAR has a correct 302 /profile POST and the page is updated, but a duplicate 200 record is selected by the evaluator, causing a false negative."}}, + "chromium-470": {"status":"verified","subcategory":{"code":"site_dependency_failure","zh":"站点依赖故障","en":"Site dependency failure"},"reason":{"zh":"确认取消后 Magento 返回 No alive nodes found in your cluster,订单仍为 Pending;本地搜索服务不可用。","en":"After cancellation confirmation Magento returned No alive nodes found in your cluster and the order remained Pending; the local search service was unavailable."}}, + "moli-470": {"status":"verified","subcategory":{"code":"site_dependency_failure","zh":"站点依赖故障","en":"Site dependency failure"},"reason":{"zh":"确认取消后 Magento 返回 No alive nodes found in your cluster,订单仍为 Pending;本地搜索服务不可用。","en":"After cancellation confirmation Magento returned No alive nodes found in your cluster and the order remained Pending; the local search service was unavailable."}}, + "chromium-475": {"status":"verified","subcategory":{"code":"benchmark_form_contract","zh":"表单验收缺陷","en":"Form-acceptance defect"},"reason":{"zh":"空项目已按正确名称、路径和命名空间创建;HTML 会省略未勾选的 initialize_with_readme,官方规则却要求显式值 0。","en":"The empty project was created with the correct name, path, and namespace; HTML omits unchecked initialize_with_readme, while the official rule requires an explicit 0."}}, + "moli-475": {"status":"verified","subcategory":{"code":"benchmark_form_contract","zh":"表单验收缺陷","en":"Form-acceptance defect"},"reason":{"zh":"空项目已按正确名称、路径和命名空间创建;HTML 会省略未勾选的 initialize_with_readme,官方规则却要求显式值 0。","en":"The empty project was created with the correct name, path, and namespace; HTML omits unchecked initialize_with_readme, while the official rule requires an explicit 0."}}, + "moli-506": {"status":"verified","subcategory":{"code":"agent_step_budget","zh":"步骤预算耗尽","en":"Step-budget exhaustion"},"reason":{"zh":"达到 40 步时仍在购物流程;证据只有购物车增删,没有付款或成功页事件。","en":"At the 40-step limit the Agent was still shopping; evidence has cart changes but no payment or success-page event."}}, + "chromium-510": {"status":"verified","subcategory":{"code":"agent_product_selection","zh":"商品选择错误","en":"Incorrect product selection"},"reason":{"zh":"Agent 完成结账,但买的是 Hiquphon Tweeters,而非官方目标 Atlantic Technology FS-7.0-GLB;筛选排序执行错误。","en":"The Agent completed checkout with Hiquphon Tweeters instead of the official Atlantic Technology FS-7.0-GLB target; the ranking was applied incorrectly."}}, + "moli-510": {"status":"verified","subcategory":{"code":"agent_step_budget","zh":"步骤预算耗尽","en":"Step-budget exhaustion"},"reason":{"zh":"Agent 在 40 步内反复比较和调整购物车,最终没有付款成功事件。","en":"The Agent repeatedly compared products and changed the cart within 40 steps, but never produced a successful payment event."}}, + "chromium-528": {"status":"verified","subcategory":{"code":"benchmark_instruction_contract","zh":"任务与验收矛盾","en":"Instruction-evaluator contradiction"},"reason":{"zh":"Agent 正确填写退款表单并按“不要提交”停在提交前;官方 evaluator 却要求出现 dummy_bin POST,任务与验收相互矛盾。","en":"The Agent correctly filled the refund form and stopped before submission, but the evaluator requires a dummy_bin POST, contradicting the instruction."}}, + "moli-528": {"status":"verified","subcategory":{"code":"benchmark_instruction_contract","zh":"任务与验收矛盾","en":"Instruction-evaluator contradiction"},"reason":{"zh":"Agent 正确填写退款表单并按“不要提交”停在提交前;官方 evaluator 却要求出现 dummy_bin POST,任务与验收相互矛盾。","en":"The Agent correctly filled the refund form and stopped before submission, but the evaluator requires a dummy_bin POST, contradicting the instruction."}}, + "chromium-547": {"status":"verified","subcategory":{"code":"agent_step_budget","zh":"步骤预算耗尽","en":"Step-budget exhaustion"},"reason":{"zh":"Agent 在 40 步内未完成新增 Brown 色板和 S 码变体,两个必要保存请求都不存在。","en":"The Agent did not finish adding the Brown swatch and size-S variation within 40 steps; both required save requests are absent."}}, + "moli-547": {"status":"verified","subcategory":{"code":"site_backend_failure","zh":"站点保存故障","en":"Site save failure"},"reason":{"zh":"Agent 到达变体保存阶段后 Magento 返回 The stock item was unable to be saved,S/Brown 变体未建立。","en":"At the variation-save stage Magento returned The stock item was unable to be saved, so the S/Brown variation was not created."}}, + "moli-595": {"status":"verified","subcategory":{"code":"evidence_capture","zh":"网络证据缺失","en":"Missing network evidence"},"reason":{"zh":"页面显示已订阅且 HAR 有 subscribe.json 200 POST,但 Moli 记录缺少 Referer 和响应 JSON,evaluator 无法确认 subscribed=true。","en":"The page shows subscribed and the HAR has a 200 subscribe.json POST, but Moli lacks Referer and response JSON, so the evaluator cannot confirm subscribed=true."}}, + "chromium-600": {"status":"verified","subcategory":{"code":"benchmark_route_contract","zh":"路由验收缺陷","en":"Route-acceptance defect"},"reason":{"zh":"合法的 /submit/gaming 302 POST 含正确标题和论坛 ID 10039;evaluator 只接受精确 /submit。","en":"The valid /submit/gaming 302 POST contains the correct title and forum ID 10039; the evaluator accepts only exact /submit."}}, + "moli-600": {"status":"verified","subcategory":{"code":"benchmark_route_contract","zh":"路由验收缺陷","en":"Route-acceptance defect"},"reason":{"zh":"合法的 /submit/gaming 302 POST 含正确标题和论坛 ID 10039;evaluator 只接受精确 /submit。","en":"The valid /submit/gaming 302 POST contains the correct title and forum ID 10039; the evaluator accepts only exact /submit."}}, + "moli-615": {"status":"verified","subcategory":{"code":"agent_field_omission","zh":"提交字段遗漏","en":"Submission-field omission"},"reason":{"zh":"发帖请求包含正确标题和 Connecticut 论坛 ID,但 submission[url] 为空,遗漏 Bald Eagle 图片 URL。","en":"The post has the correct title and Connecticut forum ID, but submission[url] is empty, omitting the Bald Eagle image URL."}}, + "chromium-625": {"status":"verified","subcategory":{"code":"benchmark_route_contract","zh":"路由验收缺陷","en":"Route-acceptance defect"},"reason":{"zh":"合法的 /submit/technology 302 POST 含正确标题、正文和论坛 ID 10043;evaluator 只接受精确 /submit。","en":"The valid /submit/technology 302 POST contains the correct title, body, and forum ID 10043; the evaluator accepts only exact /submit."}}, + "moli-625": {"status":"verified","subcategory":{"code":"benchmark_route_contract","zh":"路由验收缺陷","en":"Route-acceptance defect"},"reason":{"zh":"合法的 /submit/technology 302 POST 含正确标题、正文和论坛 ID 10043;evaluator 只接受精确 /submit。","en":"The valid /submit/technology 302 POST contains the correct title, body, and forum ID 10043; the evaluator accepts only exact /submit."}}, + "chromium-676": {"status":"verified","subcategory":{"code":"benchmark_route_contract","zh":"路由验收缺陷","en":"Route-acceptance defect"},"reason":{"zh":"HAR 有完整 sales_order_grid 与 status=fraud 请求,Referer 为合法的 /sales/order/;官方规则把尾斜杠差异判为失败。","en":"The HAR has the complete sales_order_grid request with status=fraud and a valid /sales/order/ Referer; the official rule fails on the trailing slash."}}, + "moli-676": {"status":"verified","subcategory":{"code":"evidence_capture","zh":"网络证据缺失","en":"Missing network evidence"},"reason":{"zh":"HAR 有完整 sales_order_grid 与 status=fraud 请求,但 Moli 未记录 Referer,evaluator 无法匹配。","en":"The HAR has the complete sales_order_grid request with status=fraud, but Moli omits its Referer, preventing evaluator matching."}}, + "chromium-694": {"status":"verified","subcategory":{"code":"site_backend_failure","zh":"站点保存故障","en":"Site save failure"},"reason":{"zh":"Agent 两次创建商品均被 Magento 以 The stock item was unable to be saved 拒绝,目录仍为空。","en":"Magento rejected both product-creation attempts with The stock item was unable to be saved, leaving the catalog empty."}}, + "moli-694": {"status":"verified","subcategory":{"code":"site_backend_failure","zh":"站点保存故障","en":"Site save failure"},"reason":{"zh":"Agent 两次创建商品均被 Magento 以 The stock item was unable to be saved 拒绝,商品未创建。","en":"Magento rejected both product-creation attempts with The stock item was unable to be saved, so the product was not created."}}, + "chromium-699": {"status":"verified","subcategory":{"code":"benchmark_evaluator_defect","zh":"evaluator 实现错误","en":"Evaluator implementation defect"},"reason":{"zh":"evaluator 在比较实际提交前即因单元素 Alternatives 配置抛出 Alternatives require 2+ items,零分来自官方规则错误。","en":"Before comparing the submission, the evaluator throws Alternatives require 2+ items for a one-item configuration; the zero comes from the official rule."}}, + "moli-699": {"status":"verified","subcategory":{"code":"benchmark_evaluator_defect","zh":"evaluator 实现错误","en":"Evaluator implementation defect"},"reason":{"zh":"evaluator 在比较实际提交前即因单元素 Alternatives 配置抛出 Alternatives require 2+ items,零分来自官方规则错误。","en":"Before comparing the submission, the evaluator throws Alternatives require 2+ items for a one-item configuration; the zero comes from the official rule."}}, + "chromium-771": {"status":"verified","subcategory":{"code":"agent_workflow_completeness","zh":"批量操作遗漏","en":"Incomplete batch workflow"},"reason":{"zh":"任务要求批准全部至少 4 星评论;Chromium 没有保存官方要求的评论 352 和 347,而 Moli 在相同条件下完成了两项。","en":"The task requires approving all reviews rated at least four stars; Chromium did not save required reviews 352 and 347, while Moli completed both under the same controls."}} } } diff --git a/research/report-changes.json b/research/report-changes.json index 631889531..9026aee24 100644 --- a/research/report-changes.json +++ b/research/report-changes.json @@ -2,6 +2,16 @@ "schema_version": 1, "history_scope": "本页从有完整审计依据的修订开始记录。更早的站点版本仍保留在 Artifact Hub,但没有逐项变更说明,故不补编历史。", "browser_use_reviews": [ + { + "id": "review-2026-09-11-90-task-expansion", + "date": "2026-09-11", + "title": "90 题扩容与失败根因核验", + "status": "已处理", + "scope": "90 个预注册任务、180 个浏览器逻辑结果、220 次物理执行、环境准备回执、官方判分和全部最终失败结果。", + "findings": "Chromium 通过 73/90,Moli 通过 70/90;69 题共同通过、16 题共同失败、5 题结果不一致。37 个最终失败覆盖 Agent 检索与执行、站点依赖、官方验收合同及 Moli 网络证据采集四类责任层,不能用“模型能力不行”统一解释。", + "corrections": "逐结果绑定官方合同、最终回答、HAR 和 Agent 轨迹,完成 37/37 根因核验;报告改用 90 题动态口径并披露 220/220 环境准备回执。运行器对 Agent 启动前的瞬态 BrowserContextNotLoaded 增加一次仅限会话准备的恢复,不改变逻辑结果协议。", + "remaining": "Astra 未由当前 OpenAI 代理暴露,本轮保持 gpt-5.6-sol、reasoning=low。Wikipedia 与 Map 尚未进入实跑;官方 evaluator 的已核验缺陷需上游修复后以新证据复测。" + }, { "id": "review-2026-09-10-shared-failure-and-expansion", "date": "2026-09-10", @@ -64,6 +74,40 @@ } ], "browser_use_releases": [ + { + "id": "2026-09-11-90-task-browser-use-comparison", + "date": "2026-09-11", + "title": "扩展到 90 个 Browser Use 任务", + "summary": "预注册样本扩展为 90 题,覆盖 easy 15、medium 30、hard 45;Chromium 通过 73 题,Moli 通过 70 题。全部 37 个最终失败结果已追到可复核根因,官方分数保持原样,不以人工核验改分。", + "attempts_sha256": "73146500152db62d79f84b63c85a14027c005a6320c19bce7426651f167466bd", + "selection_sha256": "2cb716552e36df7535e83ed6d2cf3f84283e3aca86072817cd6e039a38547c7b", + "changes": [ + [ + "评测范围", + "上一批只覆盖 30 题,无法同时形成足够的简单、中等和长链任务观察。", + "扩展为 90 个结果盲选任务,按最少可见 UI 动作预注册为 easy 15、medium 30、hard 45,覆盖 Shopping、Shopping Admin、Reddit 和 GitLab。", + "分母由 30 增至 90;难度标签是本项目操作长度分层,不是官方难度。" + ], + [ + "正式结果", + "上一批 Chromium 与 Moli 均为 25/30。", + "本批 Chromium 为 73/90,Moli 为 70/90;69 题共同通过、16 题共同失败、Chromium 单边通过 4 题、Moli 单边通过 1 题。", + "结果只适用于当前 CowAgent、gpt-5.6-sol low、四个本地站点与冻结运行协议,不外推完整 benchmark。" + ], + [ + "失败根因", + "程序化 evaluator 只能先定位失败规则,不能单独解释错误由哪一层产生。", + "对 37 个最终失败逻辑结果逐项核对合同、回答、网络证据和自主轨迹,记录具体根因子类与原因。", + "37/37 已核验、0 项未决;人工核验不覆盖官方分数。" + ], + [ + "环境证据", + "旧批次没有逐执行绑定的环境准备回执。", + "本批 220/220 次物理执行均保存 reset、ready、应用健康检查和登录准备状态,并继续披露 Colima 网关与本地镜像拓扑。", + "支持确认本批确在自托管站点执行;不代表 Wikipedia 与 Map 已部署。" + ] + ] + }, { "id": "2026-09-10-shared-failure-diagnosis-and-mac-expansion", "date": "2026-09-10", diff --git a/scripts/check_browser_use_report.py b/scripts/check_browser_use_report.py index 3c47cedb2..30b190b20 100644 --- a/scripts/check_browser_use_report.py +++ b/scripts/check_browser_use_report.py @@ -186,7 +186,7 @@ def check( ): raise ValueError("review page must present review rounds, not a per-result audit table") methodology_index = rendered_pages[report_dir / "methodology/index.html"] - for marker in ("192.168.5.2", "Wikipedia", "Map", "未形成独立绑定的准备回执"): + for marker in ("192.168.5.2", "Wikipedia", "Map", "环境准备证据"): if marker not in methodology_index: raise ValueError(f"methodology is missing deployment evidence boundary: {marker}") analysis_index = rendered_pages[report_dir / "analysis/index.html"] @@ -448,13 +448,14 @@ def check( raise ValueError( f"published evidence contains a secret or local origin: {path}" ) - risky_patterns = ( - r"/Users/[^/\s<]+/", - r"-----BEGIN [A-Z ]*PRIVATE KEY-----", - r"\bBearer\s+[A-Za-z0-9._~+/=-]{12,}", - r"\bsk-[A-Za-z0-9_-]{12,}", - ) - if any(re.search(pattern, text, flags=re.I) for pattern in risky_patterns): + if re.search(r"/Users/[^/\s<]+/", text) or any( + re.search(pattern, text, flags=re.I) + for pattern in ( + r"-----BEGIN [A-Z ]*PRIVATE KEY-----", + r"\bBearer\s+[A-Za-z0-9._~+/=-]{12,}", + r"\bsk-[A-Za-z0-9_-]{12,}", + ) + ): raise ValueError(f"published evidence contains sensitive text: {path}") diff --git a/scripts/check_browser_use_visual.py b/scripts/check_browser_use_visual.py index 131630f3a..57cecfd04 100644 --- a/scripts/check_browser_use_visual.py +++ b/scripts/check_browser_use_visual.py @@ -87,7 +87,11 @@ def check(report: Path, receipt: Path) -> None: const box=button.getBoundingClientRect(),style=getComputedStyle(button),line=parseFloat(style.lineHeight)||16; if(button.textContent.trim().length>2 && box.height>line*2.6 && box.width<52)vertical.push(button.textContent.trim()); } -return {overflow,clipped:[...new Set(clipped)],vertical}; +const textOverflow=[]; +for(const element of document.querySelectorAll('.hero h1,.hero .lead')){ + if(element.scrollWidth > element.clientWidth + 2)textOverflow.push(element.tagName+'.'+element.className); +} +return {overflow,clipped:[...new Set(clipped)],vertical,textOverflow}; } """ ) @@ -99,7 +103,12 @@ def check(report: Path, receipt: Path) -> None: **result, } states.append(state) - if result["overflow"] > 2 or result["clipped"] or result["vertical"]: + if ( + result["overflow"] > 2 + or result["clipped"] + or result["vertical"] + or result["textOverflow"] + ): failures.append(state) page.close() finally: From eb9963e2dd1c9e4bd4d283637959ae845d29b5c7 Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 11 Sep 2026 17:31:17 +0800 Subject: [PATCH 061/127] Expand browser evaluation across model cohorts --- Makefile | 8 +- README.md | 20 +- evaluation/browser-use/current-report.json | 17 +- examples/browser_use/README.md | 15 + harness/browser_use/failure_cause_review.py | 102 +- harness/browser_use/report.py | 1337 ++++++++++++----- harness/browser_use/report_i18n.py | 114 +- harness/browser_use/review.py | 28 +- harness/browser_use/runner.py | 98 +- .../browser-use-failure-cause-review.json | 136 +- research/report-changes.json | 279 +++- scripts/check_browser_use_report.py | 547 ++++--- scripts/package_browser_use_artifact_hub.py | 17 +- scripts/publish_browser_use_artifact_hub.py | 36 +- .../browser_use/test_failure_cause_review.py | 170 ++- tests/browser_use/test_publish.py | 63 + tests/browser_use/test_report.py | 324 +++- tests/browser_use/test_report_i18n.py | 91 ++ tests/browser_use/test_report_ui.mjs | 12 +- tests/browser_use/test_review.py | 4 +- tests/browser_use/test_runner.py | 166 +- 21 files changed, 2779 insertions(+), 805 deletions(-) create mode 100644 tests/browser_use/test_publish.py create mode 100644 tests/browser_use/test_report_i18n.py diff --git a/Makefile b/Makefile index 1d57510db..a446ca601 100644 --- a/Makefile +++ b/Makefile @@ -41,8 +41,8 @@ report-check: runtime-check $(PY) scripts/check_report_revision.py reports/webfetch browser-use-report: runtime-check - @test -n "$(EVIDENCE)" || (echo 'EVIDENCE=/absolute/path is required' >&2; exit 2) - $(PY) scripts/build_browser_use_report.py --evidence "$(EVIDENCE)" --output reports/browser-use + @test -n "$(EVIDENCE)" || (echo 'EVIDENCE="/absolute/path [/absolute/path...]" is required' >&2; exit 2) + $(PY) scripts/build_browser_use_report.py $(foreach path,$(EVIDENCE),--evidence "$(path)") --output reports/browser-use browser-use-review: runtime-check @test -n "$(EVIDENCE)" || (echo 'EVIDENCE=/absolute/path is required' >&2; exit 2) @@ -52,7 +52,7 @@ browser-use-visual-check: runtime-check $(PY) scripts/check_browser_use_visual.py reports/browser-use browser-use-report-check: runtime-check - @test -n "$(EVIDENCE)" || (echo 'EVIDENCE=/absolute/path is required' >&2; exit 2) - $(PY) scripts/check_browser_use_report.py reports/browser-use --evidence "$(EVIDENCE)" --require-complete + @test -n "$(EVIDENCE)" || (echo 'EVIDENCE="/absolute/path [/absolute/path...]" is required' >&2; exit 2) + $(PY) scripts/check_browser_use_report.py reports/browser-use $(foreach path,$(EVIDENCE),--evidence "$(path)") --require-complete $(PY) scripts/check_report_links.py reports/browser-use $(MAKE) browser-use-visual-check diff --git a/README.md b/README.md index 32beb19ab..66b5d8e46 100644 --- a/README.md +++ b/README.md @@ -53,11 +53,10 @@ Web Fetch has no legacy top-level package or compatibility imports. ## Autonomous browser-use pilot -The second browser track runs the real CowAgent loop with GPT-6 Astra -(`reasoning.effort=low`) against 30 pre-registered WebArena-Verified tasks. Both -engines receive the same task, model, agent, site snapshot, limits, and official -evaluator. Each engine gets one primary attempt; only a failed primary receives -one retry, reported separately from pass@1. +The second browser track runs the real CowAgent loop against pre-registered +WebArena-Verified tasks. Browsers remain the evaluated participants; models are +separate experiment layers. Each model is collected in its own evidence +directory, and the report builder merges those immutable cohorts in memory. ```bash ./examples/browser_use/run.sh @@ -66,6 +65,17 @@ make browser-use-report EVIDENCE="$HOME/.browser-eval/browser-use/current" make browser-use-report-check EVIDENCE="$HOME/.browser-eval/browser-use/current" ``` +For a joint report, repeat `--evidence` once per model cohort; never concatenate +their `attempts.jsonl` files: + +```bash +uv run --frozen python scripts/build_browser_use_report.py \ + --evidence "$MODEL_A_EVIDENCE" --evidence "$MODEL_B_EVIDENCE" \ + --output reports/browser-use +uv run --frozen python scripts/check_browser_use_report.py reports/browser-use \ + --evidence "$MODEL_A_EVIDENCE" --evidence "$MODEL_B_EVIDENCE" +``` + See [examples/browser_use/README.md](examples/browser_use/README.md) for the local-site and credential boundary. diff --git a/evaluation/browser-use/current-report.json b/evaluation/browser-use/current-report.json index 1b67a242d..2c3f153ac 100644 --- a/evaluation/browser-use/current-report.json +++ b/evaluation/browser-use/current-report.json @@ -1,5 +1,5 @@ { - "schema": "lex_browser_use_current_report/1", + "schema": "lex_browser_use_current_report/2", "benchmark": "WebArena-Verified", "benchmark_version": "1.2.3", "task_count": 90, @@ -13,7 +13,15 @@ "chromium": "Chromium", "moli": "Moli 1.1.3" }, - "model": "gpt-5.6-sol", + "baseline_engine": "chromium", + "models": [ + "gpt-5.6-sol", + "gpt-6-astra" + ], + "model_labels": { + "gpt-5.6-sol": "GPT-5.6 Sol", + "gpt-6-astra": "GPT-6 Astra" + }, "reasoning_effort": "low", "max_agent_steps": 40, "max_task_executions": 2, @@ -24,7 +32,8 @@ "independent_semantic_review": false, "failure_cause_review": { "required_for_publication": true, - "source_sha256": "3b05bf34914a52dddbd740ac3174bcc0dc457467bee64e8c6cf043a27228043f" + "source_path": "research/browser-use-failure-cause-review.json", + "source_sha256": "08e555e6d991892780a8b967741f42921424299e53dac079a7901d47446106e9" }, "tasks_without_measured_success_path": [ 124, @@ -48,7 +57,7 @@ "base_url": "https://artifact-hub.dev.lexmount.net", "site_slug": "28OH4YZkUYF1", "share_url": "https://artifact-hub.dev.lexmount.net/v/5Vhnhdc_f8FEM2RzctQm-ChRlXUf174Y", - "expected_version": "ver_0f62710dd3a24b10adbc24b1b1635acb", + "expected_version": "ver_f46875f6e36f429a9826cb49424544d6", "state": "verified" } } diff --git a/examples/browser_use/README.md b/examples/browser_use/README.md index 006facbb6..6f0446035 100644 --- a/examples/browser_use/README.md +++ b/examples/browser_use/README.md @@ -36,10 +36,25 @@ Astra-enabled project is available, keep the evidence cohort separate and run: The selected model is immutable within one evidence directory. GPT-5.6 and Astra attempts must never be resumed into or reported as the same cohort. +To build one layered report from several completed cohorts, pass each directory +separately. The builder rejects duplicate models and mismatched primary +browser-task cohorts: + + uv run --frozen python scripts/build_browser_use_report.py \ + --evidence "$MODEL_A_EVIDENCE" --evidence "$MODEL_B_EVIDENCE" \ + --output reports/browser-use `COWAGENT_REPO` is a trusted code-execution boundary, not a data input. The runner refuses a dirty checkout and records its Git commit in the run config. +The official sites use one shared fixed-port stack. The runner therefore holds a +cross-process lock for each complete physical execution, from environment reset +through browser-session setup, CowAgent, evidence finalization, and evaluation. +Separate runner processes wait rather than execute against that stack in parallel. +Parallel execution is valid only after every worker has an isolated complete site +stack, site and control ports, and browser containers; changing output directories +alone does not provide isolation. + Every task-browser pair has at most two physical executions: one execution and, only after failure, one retry. The final execution is the single logical result; all physical-call latency and model usage remain part of that result. diff --git a/harness/browser_use/failure_cause_review.py b/harness/browser_use/failure_cause_review.py index f37bc8eb3..205c5361c 100644 --- a/harness/browser_use/failure_cause_review.py +++ b/harness/browser_use/failure_cause_review.py @@ -1,4 +1,4 @@ -"""Load hash-bound, per-result Browser Use failure-cause reviews.""" +"""Load model-scoped Browser Use failure-cause reviews from one layered source.""" from __future__ import annotations @@ -8,6 +8,7 @@ from typing import Any +LAYERED_REVIEW_SCHEMA = "lex_browser_use_layered_failure_cause_review/2" DEFAULT_REVIEW_PATH = ( Path(__file__).parents[2] / "research/browser-use-failure-cause-review.json" ) @@ -21,61 +22,120 @@ def load_failure_cause_review(path: Path = DEFAULT_REVIEW_PATH) -> dict[str, Any return json.loads(path.read_text(encoding="utf-8")) if path.is_file() else {} +def extract_failure_cause_review_cohort( + review: dict[str, Any], model_id: str +) -> dict[str, Any]: + """Extract one model cohort from the authoritative layered review document.""" + + if not review: + return {} + if review.get("schema") != LAYERED_REVIEW_SCHEMA: + raise ValueError("unsupported Browser Use failure-cause review schema") + cohorts = review.get("cohorts") + if not isinstance(cohorts, dict): + raise ValueError("layered failure-cause review must contain model cohorts") + cohort = cohorts.get(model_id) + if cohort is None: + return {} + if not isinstance(cohort, dict): + raise ValueError(f"failure-cause review cohort must be an object: {model_id}") + return cohort + + def validate_failure_cause_review( review: dict[str, Any], *, + model_id: str, attempts_sha256: str | None, failed_result_ids: set[str], require_complete: bool, ) -> dict[str, dict[str, Any]]: - """Return only verified causes; never promote draft or stale diagnoses.""" - - if not review: - if require_complete and failed_result_ids: - raise ValueError("complete publication requires a failure-cause review") + """Return verified causes for one model; reject stale or cross-model records.""" + + result_prefix = f"{model_id}-" + foreign_failures = sorted( + result_id for result_id in failed_result_ids if not result_id.startswith(result_prefix) + ) + if foreign_failures: + raise ValueError( + f"failed-result inventory includes another model for {model_id}: {foreign_failures}" + ) + cohort = extract_failure_cause_review_cohort(review, model_id) + if not cohort: + if require_complete: + raise ValueError(f"complete publication requires a failure-cause review: {model_id}") return {} - if review.get("schema") != "lex_browser_use_failure_cause_review/1": - raise ValueError("unsupported Browser Use failure-cause review schema") - bound_hash = (review.get("bindings") or {}).get("attempts_sha256") + + bindings = cohort.get("bindings") + if not isinstance(bindings, dict): + raise ValueError(f"failure-cause review cohort has invalid bindings: {model_id}") + bound_hash = bindings.get("attempts_sha256") if not attempts_sha256 or bound_hash != attempts_sha256: if require_complete: - raise ValueError("failure-cause review is not bound to the current attempts") + raise ValueError( + f"failure-cause review is not bound to the current attempts: {model_id}" + ) return {} - results = review.get("results") or {} + coverage = cohort.get("coverage") + results = cohort.get("results") + unresolved = cohort.get("unresolved") + review_protocol = cohort.get("review_protocol") + if not isinstance(coverage, dict): + raise ValueError(f"failure-cause review cohort has invalid coverage: {model_id}") + if not isinstance(results, dict): + raise ValueError(f"failure-cause review cohort has invalid results: {model_id}") + if not isinstance(unresolved, list): + raise ValueError(f"failure-cause review cohort has invalid unresolved items: {model_id}") + if not isinstance(review_protocol, dict): + raise ValueError(f"failure-cause review cohort has invalid review protocol: {model_id}") + verified: dict[str, dict[str, Any]] = {} for result_id, item in results.items(): + if not isinstance(result_id, str) or not result_id.startswith(result_prefix): + raise ValueError( + f"failure-cause review includes a result outside its model cohort: {result_id}" + ) if result_id not in failed_result_ids: raise ValueError(f"failure-cause review includes a non-failed result: {result_id}") + if not isinstance(item, dict): + raise ValueError(f"failure-cause review result must be an object: {result_id}") status = str(item.get("status") or "") category = item.get("subcategory") or {} reason = item.get("reason") or {} if status != "verified": if category or reason: raise ValueError( - f"unverified failure cause must not contain a subcategory or reason: {result_id}" + "unverified failure cause must not contain a subcategory or reason: " + f"{result_id}" ) continue - if not all(category.get(language) for language in ("code", "zh", "en")): + if not isinstance(category, dict) or not all( + category.get(language) for language in ("code", "zh", "en") + ): raise ValueError(f"verified failure cause has an incomplete subcategory: {result_id}") - if not all(reason.get(language) for language in ("zh", "en")): + if not isinstance(reason, dict) or not all( + reason.get(language) for language in ("zh", "en") + ): raise ValueError(f"verified failure cause has an incomplete reason: {result_id}") verified[result_id] = item if require_complete and set(verified) != failed_result_ids: missing = sorted(failed_result_ids - set(verified)) - raise ValueError(f"failure-cause review is incomplete: {missing}") + raise ValueError(f"failure-cause review is incomplete for {model_id}: {missing}") if require_complete: expected_coverage = { "failed_results": len(failed_result_ids), "verified_results": len(verified), } - if review.get("coverage") != expected_coverage: - raise ValueError("failure-cause review coverage is inconsistent") - if review.get("unresolved"): - raise ValueError("failure-cause review contains unresolved items") - if not review.get("reviewer_class") or not review.get("reviewed_at"): - raise ValueError("failure-cause review must identify its reviewer class and date") + if coverage != expected_coverage: + raise ValueError(f"failure-cause review coverage is inconsistent: {model_id}") + if unresolved: + raise ValueError(f"failure-cause review contains unresolved items: {model_id}") + if not cohort.get("reviewer_class") or not cohort.get("reviewed_at"): + raise ValueError( + f"failure-cause review must identify its reviewer class and date: {model_id}" + ) return verified diff --git a/harness/browser_use/report.py b/harness/browser_use/report.py index e0d7a5116..7230e0f32 100644 --- a/harness/browser_use/report.py +++ b/harness/browser_use/report.py @@ -20,11 +20,17 @@ from .failure_cause_review import ( DEFAULT_REVIEW_PATH as FAILURE_CAUSE_REVIEW_PATH, +) +from .failure_cause_review import ( + LAYERED_REVIEW_SCHEMA, + extract_failure_cause_review_cohort, load_failure_cause_review, localized_cause, - sha256 as file_sha256, validate_failure_cause_review, ) +from .failure_cause_review import ( + sha256 as file_sha256, +) from .publication import ( evaluator_summary, evidence_metadata, @@ -39,6 +45,8 @@ from .selection import PilotSelection, load_pilot, validate_against_official ENGINE_LABELS = {"chromium": "Chromium", "moli": "Moli 1.1.3"} +MODEL_LABELS = {"gpt-5.6-sol": "GPT-5.6 Sol"} +BASELINE_ENGINE = "chromium" TIER_LABELS = {"easy": "简单", "medium": "中等", "hard": "困难"} SITE_LABELS = { "shopping": "Shopping", @@ -74,6 +82,23 @@ def _configure_engine_labels(profile: dict[str, Any]) -> None: } +def _configure_models(profile: dict[str, Any]) -> None: + models = [str(model) for model in profile.get("models") or []] + labels = profile.get("model_labels") or {} + baseline = str(profile.get("baseline_engine") or "") + if not models or len(models) != len(set(models)): + raise ValueError("current Browser Use profile must register unique models") + if any(not re.fullmatch(r"[A-Za-z0-9._-]+", model) for model in models): + raise ValueError("Browser Use model IDs must be safe path components") + if set(labels) - set(models): + raise ValueError("Browser Use model labels contain an unregistered model") + if baseline not in ENGINE_LABELS: + raise ValueError("Browser Use baseline_engine must be a registered browser engine") + global MODEL_LABELS, BASELINE_ENGINE + MODEL_LABELS = {model: str(labels.get(model) or model) for model in models} + BASELINE_ENGINE = baseline + + METHODOLOGY_FIGURE = Path(__file__).parent / "assets" / "webarena-autonomous-evaluation-flow.png" @@ -98,6 +123,110 @@ def _read_attempts(evidence: Path) -> list[dict[str, Any]]: ) +def _evidence_paths(evidence: Path | Iterable[Path]) -> list[Path]: + paths = [evidence] if isinstance(evidence, Path) else list(evidence) + resolved = [Path(path).resolve() for path in paths] + if not resolved: + raise ValueError("at least one Browser Use evidence directory is required") + if len(resolved) != len(set(resolved)): + raise ValueError("Browser Use evidence directories must be unique") + return resolved + + +def _load_evidence_bundles( + evidence: Path | Iterable[Path], +) -> list[dict[str, Any]]: + """Load isolated model cohorts without ever combining physical attempts on disk.""" + + paths = _evidence_paths(evidence) + bundles: list[dict[str, Any]] = [] + seen_models: set[str] = set() + for path in paths: + manifest = _read_json(path / "manifest.json") + config = _read_json(path / "run-config.json") + review = _read_json(path / "result-review.json") + protocol = manifest.get("protocol") or {} + model = str(protocol.get("model") or "") + if not model and len(paths) == 1 and len(MODEL_LABELS) == 1: + model = next(iter(MODEL_LABELS)) + if not model: + raise ValueError(f"evidence manifest does not identify its model: {path}") + if model not in MODEL_LABELS: + raise ValueError(f"evidence model is not registered in the current profile: {model}") + if model in seen_models: + raise ValueError(f"more than one evidence directory was supplied for model: {model}") + seen_models.add(model) + rows = _read_attempts(path) + for source in rows: + row_model = str(source.get("model") or model) + if row_model != model: + raise ValueError( + f"attempt model differs from its evidence manifest: {row_model} != {model}" + ) + source["model"] = model + if str(source.get("engine") or "") not in ENGINE_LABELS: + raise ValueError( + f"attempt browser is not registered in the current profile: {source.get('engine')}" + ) + bundles.append( + { + "path": path, + "model": model, + "manifest": manifest, + "config": config, + "review": review, + "source_attempts": rows, + "attempts_sha256": _sha256(path / "attempts.jsonl"), + "manifest_sha256": _sha256(path / "manifest.json"), + } + ) + expected_models = set(MODEL_LABELS) + if len(expected_models) > 1 and seen_models != expected_models: + missing = sorted(expected_models - seen_models) + extra = sorted(seen_models - expected_models) + raise ValueError( + f"model cohort does not match the current profile; missing={missing}, extra={extra}" + ) + if len(bundles) > 1: + contract_values = { + bundle["model"]: ( + bundle["manifest"].get("selection_sha256"), + ((bundle["manifest"].get("environment") or {}).get("official_evaluator") or {}).get( + "data_checksum" + ), + (bundle["manifest"].get("environment") or {}).get("webarena_verified_version"), + ) + for bundle in bundles + } + if len(set(contract_values.values())) != 1: + raise ValueError( + "model evidence directories do not share one frozen evaluation contract" + ) + cohorts = { + bundle["model"]: { + (str(row["engine"]), int(row["task_id"])) + for row in bundle["source_attempts"] + if int(row["attempt"]) == 1 + } + for bundle in bundles + } + reference_model = next(iter(MODEL_LABELS)) + reference = cohorts[reference_model] + if not reference: + raise ValueError("multi-model report cannot use an empty primary cohort") + for model, cohort in cohorts.items(): + if cohort != reference: + raise ValueError( + f"model cohort differs from {reference_model}: {model}; " + f"missing={sorted(reference - cohort)}, extra={sorted(cohort - reference)}" + ) + task_ids = {task_id for _, task_id in cohort} + expected = {(engine, task_id) for engine in ENGINE_LABELS for task_id in task_ids} + if cohort != expected: + raise ValueError(f"model cohort is missing a registered browser: {model}") + return bundles + + def _sha256(path: Path) -> str | None: return hashlib.sha256(path.read_bytes()).hexdigest() if path.is_file() else None @@ -180,7 +309,17 @@ def _task_type(task_id: int) -> str: def _attempt_id(row: dict[str, Any]) -> str: - return f"{row['engine']}-{int(row['task_id'])}" + model = str(row.get("model") or (next(iter(MODEL_LABELS)) if len(MODEL_LABELS) == 1 else "")) + if not model: + raise ValueError("logical result does not identify its model") + return f"{model}-{row['engine']}-{int(row['task_id'])}" + + +def _row_model(row: dict[str, Any]) -> str: + model = str(row.get("model") or (next(iter(MODEL_LABELS)) if len(MODEL_LABELS) == 1 else "")) + if not model: + raise ValueError("attempt does not identify its model") + return model def _execution_tool_metrics(row: dict[str, Any]) -> dict[str, Any]: @@ -213,11 +352,17 @@ def _logical_results( A failed task may be executed once more. The frozen run protocol chooses the authoritative execution; additive cost and latency include every execution. """ - grouped: dict[tuple[str, int], list[dict[str, Any]]] = defaultdict(list) + grouped: dict[tuple[str, str, int], list[dict[str, Any]]] = defaultdict(list) for row in rows: - grouped[(str(row["engine"]), int(row["task_id"]))].append(row) + model = str( + row.get("model") or (next(iter(MODEL_LABELS)) if len(MODEL_LABELS) == 1 else "") + ) + if not model: + raise ValueError("attempt does not identify its model") + row["model"] = model + grouped[(model, str(row["engine"]), int(row["task_id"]))].append(row) results: list[dict[str, Any]] = [] - for key in sorted(grouped, key=lambda item: (item[0], item[1])): + for key in sorted(grouped): executions = sorted(grouped[key], key=lambda row: int(row["attempt"])) if [int(row["attempt"]) for row in executions] != list(range(1, len(executions) + 1)): raise ValueError(f"non-contiguous execution sequence for {key}") @@ -285,9 +430,11 @@ def _logical_results( def _public_attempt(row: dict[str, Any]) -> dict[str, Any]: usage, resource = row.get("model_usage") or {}, row.get("resource") or {} + executions = row.get("_source_executions") or [row] return { "id": _attempt_id(row), "run_id": str(row.get("run_id") or ""), + "model": str(row["model"]), "engine": str(row["engine"]), "task_id": int(row["task_id"]), "tier": str(row["tier"]), @@ -310,6 +457,8 @@ def _public_attempt(row: dict[str, Any]) -> dict[str, Any]: ) }, "transport_error_count": len(row.get("model_transport_errors") or []), + "execution_count": len(executions), + "authoritative_execution": int(row["attempt"]), "resource": { key: round(float(resource[key]), 3) for key in ( @@ -326,6 +475,46 @@ def _public_attempt(row: dict[str, Any]) -> dict[str, Any]: } +def _public_execution(row: dict[str, Any], *, authoritative: bool) -> dict[str, Any]: + """Return the measurements needed to recompute one logical result's totals.""" + + usage, resource = row.get("model_usage") or {}, row.get("resource") or {} + tool_metrics = _execution_tool_metrics(row) + return { + "execution": int(row["attempt"]), + "authoritative": authoritative, + "run_id": str(row.get("run_id") or ""), + "score": float(row.get("score") or 0), + "status": str( + row.get("status") or ("success" if float(row.get("score") or 0) == 1 else "failure") + ), + "duration_ms": round(float(row.get("duration_ms") or 0), 3), + "timings": { + str(key): round(float(value), 3) + for key, value in (row.get("timings") or {}).items() + if isinstance(value, (int, float)) + }, + **tool_metrics, + "model_usage": { + key: int(usage.get(key) or 0) + for key in ( + "input_tokens", + "output_tokens", + "reasoning_tokens", + "total_tokens", + "request_count", + "request_attempt_count", + ) + }, + "transport_error_count": len(row.get("model_transport_errors") or []), + "resource": { + str(key): round(float(value), 3) + for key, value in resource.items() + if isinstance(value, (int, float)) + }, + } + + def _percentile(values: list[float], q: float) -> float | None: if not values: return None @@ -335,61 +524,98 @@ def _percentile(values: list[float], q: float) -> float | None: return values[lo] if lo == hi else values[lo] + (values[hi] - values[lo]) * (index - lo) -def _primary_summary(rows: list[dict[str, Any]]) -> dict[str, dict[str, Any]]: - result = {} - for engine in ENGINE_LABELS: - logical = [r for r in rows if r["engine"] == engine] - durations = [float(r.get("duration_ms") or 0) for r in logical] - tool_durations = [float(r.get("browser_tool_duration_ms") or 0) for r in logical] - tokens = [int((r.get("model_usage") or {}).get("total_tokens") or 0) for r in logical] - passed = sum(float(r["score"]) == 1 for r in logical) - result[engine] = { - "completed": len(logical), - "passed": passed, - "score": passed / len(logical) if logical else None, - "mean_duration_ms": statistics.fmean(durations) if durations else None, - "p50_duration_ms": _percentile(durations, 0.50), - "p95_duration_ms": _percentile(durations, 0.95), - "mean_browser_tool_duration_ms": statistics.fmean(tool_durations) - if tool_durations - else None, - "p50_browser_tool_duration_ms": _percentile(tool_durations, 0.50), - "p95_browser_tool_duration_ms": _percentile(tool_durations, 0.95), - "mean_tokens": statistics.fmean(tokens) if tokens else None, - "p50_tokens": _percentile(tokens, 0.50), - "p95_tokens": _percentile(tokens, 0.95), - } +def _primary_summary(rows: list[dict[str, Any]]) -> dict[str, dict[str, dict[str, Any]]]: + result: dict[str, dict[str, dict[str, Any]]] = {} + for model in MODEL_LABELS: + result[model] = {} + for engine in ENGINE_LABELS: + logical = [r for r in rows if r["model"] == model and r["engine"] == engine] + durations = [float(r.get("duration_ms") or 0) for r in logical] + tool_durations = [float(r.get("browser_tool_duration_ms") or 0) for r in logical] + tokens = [int((r.get("model_usage") or {}).get("total_tokens") or 0) for r in logical] + passed = sum(float(r["score"]) == 1 for r in logical) + result[model][engine] = { + "completed": len(logical), + "passed": passed, + "score": passed / len(logical) if logical else None, + "mean_duration_ms": statistics.fmean(durations) if durations else None, + "p50_duration_ms": _percentile(durations, 0.50), + "p95_duration_ms": _percentile(durations, 0.95), + "mean_browser_tool_duration_ms": statistics.fmean(tool_durations) + if tool_durations + else None, + "p50_browser_tool_duration_ms": _percentile(tool_durations, 0.50), + "p95_browser_tool_duration_ms": _percentile(tool_durations, 0.95), + "mean_tokens": statistics.fmean(tokens) if tokens else None, + "p50_tokens": _percentile(tokens, 0.50), + "p95_tokens": _percentile(tokens, 0.95), + } return result def _paired_summary(rows: list[dict[str, Any]]) -> dict[str, Any]: - primary = {(str(r["engine"]), int(r["task_id"])): float(r["score"]) == 1 for r in rows} - ids = sorted( - task_id - for task_id in {key[1] for key in primary} - if ("chromium", task_id) in primary and ("moli", task_id) in primary - ) - counts = {"both_pass": 0, "chromium_only": 0, "moli_only": 0, "both_fail": 0} - for task_id in ids: - c, m = primary[("chromium", task_id)], primary[("moli", task_id)] - counts[ - "both_pass" if c and m else "chromium_only" if c else "moli_only" if m else "both_fail" - ] += 1 - discordant = counts["chromium_only"] + counts["moli_only"] - p = ( - min( - 1.0, - 2 - * sum( - math.comb(discordant, k) - for k in range(min(counts["chromium_only"], counts["moli_only"]) + 1) + """Compare every browser with the profile baseline, always within one model.""" + + primary = { + (str(r["model"]), str(r["engine"]), int(r["task_id"])): float(r["score"]) == 1 for r in rows + } + result: dict[str, Any] = {} + for model in MODEL_LABELS: + comparisons: dict[str, Any] = {} + for engine in ENGINE_LABELS: + if engine == BASELINE_ENGINE: + continue + ids = sorted( + task_id + for task_id in {key[2] for key in primary if key[0] == model} + if (model, BASELINE_ENGINE, task_id) in primary + and (model, engine, task_id) in primary ) - / 2**discordant, - ) - if discordant - else (1.0 if ids else None) - ) - return {"paired": len(ids), **counts, "mcnemar_exact_p": p} + counts = { + "both_pass": 0, + "baseline_only": 0, + "comparison_only": 0, + "both_fail": 0, + } + for task_id in ids: + baseline_passed = primary[(model, BASELINE_ENGINE, task_id)] + comparison_passed = primary[(model, engine, task_id)] + key = ( + "both_pass" + if baseline_passed and comparison_passed + else "baseline_only" + if baseline_passed + else "comparison_only" + if comparison_passed + else "both_fail" + ) + counts[key] += 1 + discordant = counts["baseline_only"] + counts["comparison_only"] + p = ( + min( + 1.0, + 2 + * sum( + math.comb(discordant, k) + for k in range(min(counts["baseline_only"], counts["comparison_only"]) + 1) + ) + / 2**discordant, + ) + if discordant + else (1.0 if ids else None) + ) + comparisons[engine] = { + "baseline_engine": BASELINE_ENGINE, + "comparison_engine": engine, + "paired": len(ids), + **counts, + "mcnemar_exact_p": p, + } + result[model] = { + "baseline_engine": BASELINE_ENGINE, + "comparisons": comparisons, + } + return result def _resolve_tasks( @@ -460,7 +686,8 @@ def _task_family_summary( tasks: list[dict[str, Any]], attempts: list[dict[str, Any]] ) -> list[dict[str, Any]]: primary = { - (str(row["engine"]), int(row["task_id"])): float(row["score"]) == 1 for row in attempts + (str(row["model"]), str(row["engine"]), int(row["task_id"])): float(row["score"]) == 1 + for row in attempts } result = [] for kind in ("RETRIEVE", "NAVIGATE", "MUTATE"): @@ -470,10 +697,14 @@ def _task_family_summary( "kind": kind, "count": len(selected), "scores": { - engine: sum( - primary.get((engine, int(task["task_id"])), False) for task in selected - ) - for engine in ENGINE_LABELS + model: { + engine: sum( + primary.get((model, engine, int(task["task_id"])), False) + for task in selected + ) + for engine in ENGINE_LABELS + } + for model in MODEL_LABELS }, } ) @@ -518,56 +749,58 @@ def _adoption_top3(evidence: dict[str, Any]) -> str: def _same_task_comparison(tasks: list[dict[str, Any]], attempts: list[dict[str, Any]]) -> str: - results = {(str(row["engine"]), int(row["task_id"])): row for row in attempts} - ordered = sorted( - ENGINE_LABELS, - key=lambda engine: ( - -sum(float(row["score"]) == 1 for row in attempts if row["engine"] == engine), - ENGINE_LABELS[engine], - ), - ) + results = {(_row_model(row), str(row["engine"]), int(row["task_id"])): row for row in attempts} + ordered = [BASELINE_ENGINE, *(engine for engine in ENGINE_LABELS if engine != BASELINE_ENGINE)] if len(ordered) < 2: return '

    至少需要两个参评浏览器才能进行同题比较。

    ' rows = [] - for task in tasks: - task_id = int(task["task_id"]) - present = [engine for engine in ordered if (engine, task_id) in results] - passed = [engine for engine in present if float(results[(engine, task_id)]["score"]) == 1] - failed = [engine for engine in present if engine not in passed] - - def outcome_links(engines: list[str], state: str) -> str: - if not engines: - return "—" - return ( - '' - + "".join( - f'' - f"{_e(ENGINE_LABELS[engine])}" - for engine in engines + for model in MODEL_LABELS: + for task in tasks: + task_id = int(task["task_id"]) + present = [engine for engine in ordered if (model, engine, task_id) in results] + passed = [ + engine + for engine in present + if float(results[(model, engine, task_id)]["score"]) == 1 + ] + failed = [engine for engine in present if engine not in passed] + + def outcome_links( + engines: list[str], state: str, *, model: str = model, task_id: int = task_id + ) -> str: + if not engines: + return "—" + return ( + '' + + "".join( + f'' + f"{_e(ENGINE_LABELS[engine])}" + for engine in engines + ) + + "" ) - + "" - ) - site = str(task["sites"][0]) - attrs = ( - f' data-pass="{_e(" ".join(passed))}" data-fail="{_e(" ".join(failed))}"' - f' data-kind="{_e(task["task_type"])}"' - ) - task_cell = ( - f'
    #{task_id} · ' - f'{_e(task["rationale"])}
    ' - f'{_e(TIER_LABELS[task["tier"]])}' - f'{_e(SITE_LABELS.get(site, site))}' - f'{_e(task["task_type"])}
    ' - ) - rows.append( - f"{task_cell}{outcome_links(passed, 'pass')}" - f"{outcome_links(failed, 'fail')}" - ) + site = str(task["sites"][0]) + attrs = ( + f' data-model="{_e(model)}" data-pass="{_e(" ".join(passed))}"' + f' data-fail="{_e(" ".join(failed))}" data-kind="{_e(task["task_type"])}"' + ) + task_cell = ( + f'
    #{task_id} · ' + f'{_e(task["rationale"])}
    ' + f'{_e(MODEL_LABELS[model])}' + f'{_e(TIER_LABELS[task["tier"]])}' + f'{_e(SITE_LABELS.get(site, site))}' + f'{_e(task["task_type"])}
    ' + ) + rows.append( + f"{task_cell}{outcome_links(passed, 'pass')}" + f"{outcome_links(failed, 'fail')}" + ) options = "".join( - f'" for engine in ordered ) @@ -584,6 +817,12 @@ def outcome_links(engines: list[str], state: str) -> str: "

    同题结果组合

    选择一个基准浏览器和一个或多个对比浏览器;浏览器标签可直达该题完整结果。

    " '' '
    ' + '" f'' '' + '" + + f'' '' if attempts else "" @@ -1311,6 +1654,7 @@ def _attempt_table( task = task_meta.get(p["task_id"], {}) values = [ Safe(f'{p["id"]}'), + MODEL_LABELS[p["model"]], ENGINE_LABELS[p["engine"]], p["task_id"], TIER_LABELS[p["tier"]], @@ -1327,13 +1671,14 @@ def _attempt_table( rows.append( f'{cells}' ) headers = "".join( f"{_e(x)}" for x in [ "结果", + "模型", "浏览器", "任务", "层级", @@ -1372,6 +1717,7 @@ def _engine_attempts( rows.append( [ Safe(f'{_attempt_id(row)}'), + MODEL_LABELS[str(row["model"])], task["rationale"], TIER_LABELS[task["tier"]], _status(row), @@ -1386,6 +1732,7 @@ def _engine_attempts( return _table( [ "结果", + "模型", "任务", "层级", "判定", @@ -1478,29 +1825,70 @@ def _review_history(rounds: list[dict[str, Any]]) -> str: return "".join(cards) -def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: +def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_path: Path) -> Path: profile = _read_json(Path("evaluation/browser-use/current-report.json")) _configure_engine_labels(profile) + _configure_models(profile) selection = load_pilot(selection_path) - source_attempts = _read_attempts(evidence) - manifest, config, review = ( - _read_json(evidence / name) - for name in ("manifest.json", "run-config.json", "result-review.json") - ) + bundles = _load_evidence_bundles(evidence) + source_attempts = [row for bundle in bundles for row in bundle["source_attempts"]] + manifest, config = bundles[0]["manifest"], bundles[0]["config"] + review_attempts = [] + for bundle in bundles: + for item in bundle["review"].get("attempts") or []: + review_attempts.append({**item, "model": bundle["model"]}) + review = { + "coverage": { + "recorded_attempts": sum( + int((bundle["review"].get("coverage") or {}).get("recorded_attempts") or 0) + for bundle in bundles + ), + "reviewed_attempts": sum( + int((bundle["review"].get("coverage") or {}).get("reviewed_attempts") or 0) + for bundle in bundles + ), + }, + "attempts": review_attempts, + "disagreements": [ + item for bundle in bundles for item in bundle["review"].get("disagreements") or [] + ], + "unresolved": [ + item for bundle in bundles for item in bundle["review"].get("unresolved") or [] + ], + "bindings": {bundle["model"]: bundle["review"].get("bindings") or {} for bundle in bundles}, + } protocol, environment = manifest.get("protocol") or {}, manifest.get("environment") or {} - attempts = _logical_results(source_attempts, protocol) + attempts = [ + row + for bundle in bundles + for row in _logical_results( + bundle["source_attempts"], bundle["manifest"].get("protocol") or {} + ) + ] public_attempts = [_public_attempt(r) for r in attempts] - attempts_sha256 = _sha256(evidence / "attempts.jsonl") - failed_result_ids = { - row["id"] for row in public_attempts if float(row.get("score") or 0) < 1 - } - failure_cause_review = load_failure_cause_review() - verified_causes = validate_failure_cause_review( - failure_cause_review, - attempts_sha256=attempts_sha256, - failed_result_ids=failed_result_ids, - require_complete=False, - ) + attempts_sha256 = {bundle["model"]: bundle["attempts_sha256"] for bundle in bundles} + failed_result_ids = {row["id"] for row in public_attempts if float(row.get("score") or 0) < 1} + default_failure_cause_review = load_failure_cause_review() + verified_causes: dict[str, dict[str, Any]] = {} + failure_reviews: dict[str, dict[str, Any]] = {} + for bundle in bundles: + model_failed_ids = { + _attempt_id(row) + for row in attempts + if row["model"] == bundle["model"] and float(row.get("score") or 0) < 1 + } + failure_reviews[bundle["model"]] = extract_failure_cause_review_cohort( + default_failure_cause_review, bundle["model"] + ) + verified_causes.update( + validate_failure_cause_review( + default_failure_cause_review, + model_id=bundle["model"], + attempts_sha256=bundle["attempts_sha256"], + failed_result_ids=model_failed_ids, + require_complete=False, + ) + ) for source, public in zip(attempts, public_attempts, strict=True): evaluation = read_public_json(Path((source.get("paths") or {}).get("evaluation", ""))) public["automatic_failure_type"] = _automatic_failure_type(evaluation)[1] @@ -1516,7 +1904,9 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: max_agent_steps = str( config.get("max_agent_steps") or protocol.get("max_agent_steps") or "未记录" ) - site_access_host = str(environment.get("site_access_host") or config.get("site_access_host") or "未记录") + site_access_host = str( + environment.get("site_access_host") or config.get("site_access_host") or "未记录" + ) preparation_receipt_count = sum( Path(str((row.get("paths") or {}).get("environment_prepare") or "")).is_file() for row in source_attempts @@ -1527,17 +1917,16 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: tier_counts = { tier: sum(task["tier"] == tier for task in tasks) for tier in ("easy", "medium", "hard") } - summary, paired = _primary_summary(attempts), _paired_summary(attempts) + summary_by_model, paired_by_model = _primary_summary(attempts), _paired_summary(attempts) measured_solvable_ids = { int(row["task_id"]) for row in attempts if float(row.get("score") or 0) == 1 } unproven_ids = sorted( int(task["task_id"]) for task in tasks if int(task["task_id"]) not in measured_solvable_ids ) - model, run_date = ( - str(protocol.get("model") or "未记录"), - str(manifest.get("created_at") or "")[:10] or "未记录", - ) + model = "、".join(MODEL_LABELS.values()) + run_dates = [str(bundle["manifest"].get("created_at") or "")[:10] for bundle in bundles] + run_date = " – ".join(value for value in (min(run_dates), max(run_dates)) if value) or "未记录" adoption_evidence = _read_json(Path("research/browser-use-benchmark-adoption.json")) parent = output.parent parent.mkdir(parents=True, exist_ok=True) @@ -1573,17 +1962,33 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: METHODOLOGY_FIGURE, stage / "assets/webarena-autonomous-evaluation-flow.png", ) - article = _article_body(paired, attempts, tasks, verified_causes) - c, m = summary["chromium"], summary["moli"] + article = _article_body(paired_by_model, attempts, tasks, verified_causes) families = _task_family_summary(tasks, attempts) - coverage = review.get("coverage") or {} participant_score_cards = "".join( '
    ' - + _e(f"{ENGINE_LABELS[engine]} 任务通过") + + _e(f"{MODEL_LABELS[model_id]} · {ENGINE_LABELS[engine]} 任务通过") + f'
    {values["passed"]}/{values["completed"]}
    ' - + f'

    {_pct(values["score"])}

    ' - for engine, values in summary.items() + + f"

    {_pct(values['score'])}

    " + for model_id, model_summary in summary_by_model.items() + for engine, values in model_summary.items() ) + pairing_rows = [ + [ + MODEL_LABELS[model_id], + ENGINE_LABELS[item["baseline_engine"]], + ENGINE_LABELS[item["comparison_engine"]], + item["paired"], + item["both_pass"], + item["baseline_only"], + item["comparison_only"], + item["both_fail"], + ] + for model_id, model_pairs in paired_by_model.items() + for item in model_pairs["comparisons"].values() + ] + discordant_total = sum(row[5] + row[6] for row in pairing_rows) + paired_total = sum(row[3] for row in pairing_rows) + both_fail_total = sum(row[7] for row in pairing_rows) cause_counts = Counter( str((item.get("subcategory") or {}).get("zh") or "未分类") for item in verified_causes.values() @@ -1599,28 +2004,35 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: '

    查看逐结果根因与证据边界 →

    ' ) elif failed_result_ids: + failure_overview = f'

    根因核验:{len(verified_causes)}/{len(failed_result_ids)} 个失败结果已完成;未核验结果不展示推测性原因。

    ' + else: failure_overview = ( - f'

    根因核验:{len(verified_causes)}/{len(failed_result_ids)} 个失败结果已完成;未核验结果不展示推测性原因。

    ' + '

    根因核验:本轮没有失败结果。

    ' ) - else: - failure_overview = '

    根因核验:本轮没有失败结果。

    ' family_cards = "".join( '
    ' f'{_e(family["kind"])}' f"

    {family['count']} 个任务

    " - f"

    Chromium {family['scores']['chromium']}/{family['count']} · " - f"Moli {family['scores']['moli']}/{family['count']}

    " - f'查看这类任务 →
    ' + + "".join( + f"

    {_e(MODEL_LABELS[model_id])}:" + + " · ".join( + f"{_e(ENGINE_LABELS[engine])} {family['scores'][model_id][engine]}/{family['count']}" + for engine in ENGINE_LABELS + ) + + "

    " + for model_id in MODEL_LABELS + ) + + f'查看这类任务 →
    ' for family in families ) home = _hero( f"Browser Use Capability Evaluation · {run_date}", - "Chromium 与 Moli:Browser Use 能力实测", - f"在同一 CowAgent、同一 {model}(reasoning=low)和同一 WebArena‑Verified {task_count} 题样本下,对比两种浏览器完成真实网页任务的能力。", + "浏览器 Browser Use 能力对比", + f"在同一 CowAgent 和同一 WebArena‑Verified {task_count} 题样本下,以 {', '.join(MODEL_LABELS.values())} 分层比较 {', '.join(ENGINE_LABELS.values())}。", ) home += _section( "先看结论", - f'
    {participant_score_cards}
    单边通过
    {paired["chromium_only"] + paired["moli_only"]}

    {paired["paired"]} 个完整配对

    共同失败
    {paired["both_fail"]}

    不等于共同根因

    结果边界:本轮观察到 {paired["chromium_only"] + paired["moli_only"]} 个浏览器间不一致配对;结论只适用于这批受控任务,不证明内核等价或全面替代关系。

    这是固定选择的 {task_count} 题试验,不是从全部 WebArena‑Verified 任务随机抽样;比例只描述本样本,不外推总体能力。

    ', + f'
    {participant_score_cards}
    单边通过
    {discordant_total}

    {paired_total} 个模型内完整配对

    共同失败
    {both_fail_total}

    不等于共同根因

    结果边界:本轮观察到 {discordant_total} 个模型内浏览器不一致配对;模型之间不配对、不混排。结论只适用于这批受控任务,不证明内核等价或全面替代关系。

    这是固定选择的 {task_count} 题试验,不是从全部 WebArena‑Verified 任务随机抽样;比例只描述本样本,不外推总体能力。

    ', ) home += _section( "失败根因是报告解读的核心证据", @@ -1635,7 +2047,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: home += _section( "运行与证据是否完整", '
    ' - f'

    逻辑结果:{len(attempts)} 个任务×浏览器结果均保留最终回答、官方 evaluator 结果、Agent 事件和证据哈希。

    ' + f'

    逻辑结果:{len(attempts)} 个模型×浏览器×任务结果保持单条展示;其全部 {len(source_attempts)} 次物理执行均保留脱敏回答、官方 evaluator 结果、Agent 事件、轨迹和证据哈希。

    ' f'

    证据完整性审计:{len(attempts)}/{len(attempts)} 个逻辑结果已覆盖,{len(review.get("unresolved") or [])} 个未决。它不重新判题,也不是独立语义复核。

    ' f'

    可完成性覆盖:{len(measured_solvable_ids)}/{len(tasks)} 题至少有一条自主成功轨迹;其余 {len(unproven_ids)} 题仅有官方验收合同。

    ' "
    ", @@ -1647,16 +2059,20 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: home += _section( "逐题配对比总分更重要", _table( - ["配对结果", "任务数"], [ - ["两者都通过", paired["both_pass"]], - ["仅 Chromium", paired["chromium_only"]], - ["仅 Moli", paired["moli_only"]], - ["两者都失败", paired["both_fail"]], + "模型", + "基准浏览器", + "对比浏览器", + "完整配对", + "共同通过", + "仅基准通过", + "仅对比通过", + "共同失败", ], - caption="同一任务的两个浏览器最终逻辑结果。", + pairing_rows, + caption="每个模型层内,从 profile 的基准浏览器动态生成配对。", ) - + '

    零个不一致任务意味着当前固定样本无法估计内核方向性效应;不对全部 Browser Use 任务作统计外推。

    ', + + f'

    本轮共有 {discordant_total} 个模型内不一致配对,且方向随模型层变化;固定样本不支持对全部 Browser Use 任务作统计外推。

    ', ) (stage / "index.html").write_text( _page(title="首页", active="home", body=home), encoding="utf-8" @@ -1665,7 +2081,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: guide = ( _hero( "Report interpretation", - f"从 {c['passed']}/{c['completed']} 对 {m['passed']}/{m['completed']} 读出下一轮该怎么做", + "从模型分层的浏览器对照读出下一轮该怎么做", "完整文章与首页共用同一份测量数据;另提供无 JavaScript 的独立导出版。", ) + _section( @@ -1718,7 +2134,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: _page(title="评测集", active="datasets", body=datasets, depth=1), encoding="utf-8" ) - by_attempt = {(r["engine"], int(r["task_id"])): r for r in attempts} + by_attempt = {(r["model"], r["engine"], int(r["task_id"])): r for r in attempts} for task in tasks: task_id, site = int(task["task_id"]), task["sites"][0] detail = _hero( @@ -1735,9 +2151,11 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: "查看本题运行结果", '

    评测集页面保持为冻结输入;实测输出、判分和轨迹在结果页查看。

    ', ) @@ -1807,7 +2225,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: if preparation_receipts_complete else "

    运行器在每个物理执行前调用 ContainerManager.start(reset=True),只保留该题所需站点;env-ctrl 报告 ready 后,再做连续应用健康检查、清浏览器状态、登录测试账号并打开起始页。当前证据尚未完整保存逐执行环境准备回执,因此不能把每次准备成功当成已独立核验的事实。

    " ) - + f"

    预注册文件只含任务 ID、层级和入选理由,不含点击序列。CowAgent 收到自然语言目标后,在最多 {max_agent_steps} 步内循环读取页面观察、请求模型决策并生成下一次 BrowserTool 调用。Chromium 与 Moli 分别连接独立 CDP 浏览器容器;每个逻辑结果保留最终 JSON、Agent 事件流、网络 HAR 和 Playwright trace。

    " + + f"

    预注册文件只含任务 ID、层级和入选理由,不含点击序列。CowAgent 收到自然语言目标后,在最多 {max_agent_steps} 步内循环读取页面观察、请求模型决策并生成下一次 BrowserTool 调用。Chromium 与 Moli 分别连接独立 CDP 浏览器容器;每次物理执行都保留最终 JSON、Agent 事件流、网络 HAR 和 Playwright trace。

    " + _table( ["站点", "本轮访问地址", "应用端口", "环境控制端口", "状态"], [ @@ -1830,7 +2248,14 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: if preparation_receipts_complete else '

    证据边界:事件流能证明 CowAgent 在准备完成后产生了 BrowserTool 调用序列;镜像 ID、digest、代码提交与网关记录限定了本地运行身份。逐执行环境准备回执尚不完整。

    ' ) - + '', + + '', ) methodology += _section( "Mac 上可以继续扩展哪些实例", @@ -1900,6 +2325,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: "汇总", _table( [ + "模型", "浏览器", "任务通过", "通过率", @@ -1907,7 +2333,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: "BrowserTool 耗时", "Tokens", ], - _summary_comparison_rows(summary), + _summary_comparison_rows(summary_by_model), ) + '

    标记方向明确且数值更优的指标:通过数与通过率越高越优,耗时与 Tokens 越低越优;数值相同时不标记。

    ' + '

    Agent 链路累计耗时把获得该任务结果实际发生的执行相加;每次从浏览器会话准备开始,到 CowAgent 完成、验收所需证据落盘前结束,包含登录、模型推理、调度、页面等待、BrowserTool 与收尾,不包含此前的 Docker 站点重置。BrowserTool 累计耗时是事件流中全部 browser 工具调用墙钟时间之和,包含工具封装、CDP 往返与页面等待,仍不是纯内核 CPU 时间。

    ', @@ -1932,25 +2358,52 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) review_by_key = { - (x["engine"], int(x["task_id"]), int(x["attempt"])): x + (x["model"], x["engine"], int(x["task_id"]), int(x["attempt"])): x for x in review.get("attempts") or [] } + public_evidence_results: list[dict[str, Any]] = [] for row, public in zip(attempts, public_attempts, strict=True): - item = review_by_key.get((public["engine"], public["task_id"], int(row["attempt"])), {}) + item = review_by_key.get( + (public["model"], public["engine"], public["task_id"], int(row["attempt"])), + {}, + ) task = next(t for t in tasks if int(t["task_id"]) == public["task_id"]) - paths = row.get("paths") or {} - response_record = read_public_json(Path(paths.get("response", ""))) - evaluation_record = read_public_json(Path(paths.get("evaluation", ""))) - events_record = read_public_json(Path(paths.get("events", ""))) - steps = trajectory(events_record if isinstance(events_record, list) else []) + source_executions = row.get("_source_executions") or [row] + execution_records = [] + response_record = evaluation_record = None + steps: list[dict[str, Any]] = [] + for source_execution in source_executions: + is_authoritative = int(source_execution["attempt"]) == int(row["attempt"]) + execution_record, response, evaluation, execution_steps = ( + _write_execution_artifacts( + stage, + public["id"], + source_execution, + authoritative=is_authoritative, + ) + ) + execution_records.append(execution_record) + if is_authoritative: + response_record, evaluation_record, steps = ( + response, + evaluation, + execution_steps, + ) metadata = evidence_metadata(row) - artifact_dir = stage / "artifacts" / public["id"] - artifact_dir.mkdir(parents=True) - _write_json(artifact_dir / "agent-response.json", response_record) - _write_json(artifact_dir / "evaluation.json", evaluation_record) - _write_json(artifact_dir / "agent-events.json", events_record) - _write_json(artifact_dir / "trajectory.json", steps) - _write_json(artifact_dir / "evidence-metadata.json", metadata) + public_evidence_results.append( + { + "result_id": public["id"], + "model": public["model"], + "engine": public["engine"], + "task_id": public["task_id"], + "score": public["score"], + "execution_count": len(execution_records), + "authoritative_execution": int(row["attempt"]), + "task_page": f"datasets/{public['task_id']}.html", + "result_page": f"results/{public['id']}.html", + "executions": execution_records, + } + ) usage, resource, checks = ( public["model_usage"], public["resource"], @@ -1958,7 +2411,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) detail = _hero( public["id"], - f"{ENGINE_LABELS[public['engine']]} · Task #{public['task_id']}", + f"{MODEL_LABELS[public['model']]} · {ENGINE_LABELS[public['engine']]} · Task #{public['task_id']}", "先看任务目标与验收结论,再按需展开技术明细和 CowAgent 的每一步 BrowserTool 调用。", ) # Pages declare so every internal URL is rooted at @@ -2009,7 +2462,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) detail += '
    ' + _section( "运行、日志与证据", - _artifact_links(public["id"]) + _execution_evidence_html(public["id"], execution_records) + '

    公开包包含脱敏的回答、evaluator、事件流与轨迹。原始 HAR 和 Playwright trace 可能携带登录态、Cookie、页面快照或请求正文,因此不公开;其字节数和 SHA-256 保留用于证明证据绑定。

    ' + _table(["指标", "值"], resource.items()) + '

    整次 Agent 运行的容器计量只作诊断;不同进程模型下不直接比较内存或 PID。

    ' @@ -2018,7 +2471,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ["检查", "结果"], [[key, "通过" if value else "未通过"] for key, value in checks.items()], ) - + "

    原始证据文件绑定

    " + + "

    判分权威执行的原始证据绑定

    " + _table( ["证据", "存在", "字节", "SHA-256", "公开策略"], [ @@ -2061,24 +2514,26 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: adoption = _hero( "Participants", "参评浏览器与统一实验条件", - "Chromium 与 Moli 作为浏览器后端参与同题对照;Agent、模型、任务、资源上限、站点与 evaluator 保持一致。", + "浏览器后端是参评选手;模型是分层实验变量,每个模型层内保持 Agent、任务、资源上限、站点与 evaluator 一致。", ) adoption += _section( - "两个参评浏览器", + "参评浏览器", _table( - ["参评浏览器", "运行版本", "镜像 ID", "接入方式", "本轮成绩"], + ["模型层", "参评浏览器", "运行版本", "镜像 ID", "接入方式", "本轮成绩"], [ [ + MODEL_LABELS[model_id], Safe(f'{_e(label)}'), (participant_versions.get(engine) or {}).get("Browser", "—"), (participant_images.get(engine) or {}).get("id", "—"), "独立容器 · CDP · CowAgent BrowserTool", - f"{summary[engine]['passed']}/{summary[engine]['completed']}", + f"{summary_by_model[model_id][engine]['passed']}/{summary_by_model[model_id][engine]['completed']}", ] + for model_id in MODEL_LABELS for engine, label in ENGINE_LABELS.items() ], ) - + "

    参评浏览器是本轮唯一实验变量;完整 Agent 栈的其余组成列为统一实验条件。

    ", + + "

    参评选手始终是浏览器;模型结果不跨层配对,也不把模型与浏览器混成同一排名。

    ", ) adoption += _section( "共同实验条件", @@ -2092,17 +2547,26 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ], [ "模型", - f"{model} · reasoning={protocol.get('reasoning_effort', '—')}", - "为两个选手生成决策", + ", ".join(MODEL_LABELS.values()) + + f" · reasoning={protocol.get('reasoning_effort', '—')}", + "作为分层变量为各浏览器生成决策", + ], + [ + "任务与站点", + f"同一 {task_count} 题;同一批本地站点镜像", + "提供一致输入与网页状态", ], - ["任务与站点", f"同一 {task_count} 题;同一批本地站点镜像", "提供一致输入与网页状态"], ["动作接口", "CowAgent BrowserTool · CDP", "把 Agent 动作交给浏览器"], [ "验收", "WebArena‑Verified 1.2.3 官方 evaluator", "对最终 JSON 与必要网络副作用判分", ], - ["资源与上限", f"相同 CPU、内存、viewport 与最多 {max_agent_steps} 步", "控制运行预算"], + [ + "资源与上限", + f"相同 CPU、内存、viewport 与最多 {max_agent_steps} 步", + "控制运行预算", + ], ], ) + f"

    这 {task_count} 题由 CowAgent 自主执行。仓库中的 Browser Trajectory Replay(轨迹重放)固定动作微基准属于另一条实验轨道,不是本报告的参评选手,也不计入这 {task_count} 题成绩。

    ", @@ -2142,8 +2606,20 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ["DevTools Protocol", version.get("Protocol-Version", "—")], ["架构", image.get("architecture", "—")], ["镜像 ID", image.get("id", "—")], - ["任务成绩", f"{summary[engine]['passed']}/{summary[engine]['completed']}"], - ["逻辑结果耗时 P50", _seconds(summary[engine]["p50_duration_ms"])], + [ + "各模型任务成绩", + ";".join( + f"{MODEL_LABELS[model_id]} {summary_by_model[model_id][engine]['passed']}/{summary_by_model[model_id][engine]['completed']}" + for model_id in MODEL_LABELS + ), + ], + [ + "各模型逻辑结果耗时 P50", + ";".join( + f"{MODEL_LABELS[model_id]} {_seconds(summary_by_model[model_id][engine]['p50_duration_ms'])}" + for model_id in MODEL_LABELS + ), + ], ], ) + '

    版本与镜像身份用于限定复现边界,不参与评分。

    ', @@ -2231,7 +2707,7 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) reproduce += _section( "公开复现入口", - f'

    lexmount/browser-eval 是代码、题集和锁文件的唯一来源。

    ./examples/browser_use/run.sh\nmake browser-use-review EVIDENCE="$BROWSER_USE_EVIDENCE"\nmake browser-use-report EVIDENCE="$BROWSER_USE_EVIDENCE"\nmake browser-use-report-check EVIDENCE="$BROWSER_USE_EVIDENCE"

    每个逻辑结果的脱敏回答、evaluator、事件和轨迹位于 artifacts/<result-id>/;evidence-index.json 给出全部 {result_count} 个逻辑结果的页面与下载路径。

    ', + f'

    lexmount/browser-eval 是代码、题集和锁文件的唯一来源。

    ./examples/browser_use/run.sh\nmake browser-use-review EVIDENCE="$BROWSER_USE_EVIDENCE"\nmake browser-use-report EVIDENCE="$BROWSER_USE_EVIDENCE"\nmake browser-use-report-check EVIDENCE="$BROWSER_USE_EVIDENCE"

    每次物理执行的脱敏回答、evaluator、事件和轨迹位于 artifacts/<result-id>/executions/<n>/;evidence-index.json 给出全部 {result_count} 个逻辑结果及其物理执行、下载路径和公开文件哈希。

    ', ) (stage / "reproduce/index.html").write_text( _page(title="环境与复现", active="reproduce", body=reproduce, depth=1), encoding="utf-8" @@ -2258,13 +2734,18 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: claims = { "schema": "lex_browser_use_interpretation/1", "claims": [ - {"claim": "当前样本未观察到成功率差异", "basis": paired}, + {"claim": "模型内浏览器配对结果", "basis": paired_by_model}, {"claim": "失败根因以当前证据绑定的逐结果核验为准", "basis": verified_causes}, ], - "bound_attempts_sha256": _sha256(evidence / "attempts.jsonl"), + "bound_attempts_sha256": attempts_sha256, "selection_sha256": selection.sha256, - "score_authority": protocol.get("official_score_uses") - or "final execution for each browser and task", + "score_authority": { + bundle["model"]: ( + (bundle["manifest"].get("protocol") or {}).get("official_score_uses") + or "final execution for each browser and task" + ) + for bundle in bundles + }, } _write_json(stage / "guide/interpretation-evidence.json", claims) _write_json( @@ -2282,59 +2763,53 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: _write_json( stage / "data/failure-cause-review.json", { - "schema": failure_cause_review.get("schema"), - "bindings": failure_cause_review.get("bindings") or {}, - "coverage": failure_cause_review.get("coverage") or {}, - "unresolved": failure_cause_review.get("unresolved") or [], - "reviewed_at": failure_cause_review.get("reviewed_at"), - "reviewer_class": failure_cause_review.get("reviewer_class"), - "review_protocol": failure_cause_review.get("review_protocol") or {}, + "schema": LAYERED_REVIEW_SCHEMA, + "bindings": { + model_id: review_value.get("bindings") or {} + for model_id, review_value in failure_reviews.items() + }, + "coverage": { + "failed_results": len(failed_result_ids), + "verified_results": len(verified_causes), + }, + "unresolved": sorted(failed_result_ids - set(verified_causes)), "results": verified_causes, }, ) _write_json( stage / "data/evidence-index.json", { - "schema": "lex_browser_use_public_evidence_index/2", - "results": [ - { - "result_id": row["id"], - "engine": row["engine"], - "task_id": row["task_id"], - "score": row["score"], - "task_page": f"datasets/{row['task_id']}.html", - "result_page": f"results/{row['id']}.html", - "artifacts": { - "agent_response": f"artifacts/{row['id']}/agent-response.json", - "evaluation": f"artifacts/{row['id']}/evaluation.json", - "agent_events": f"artifacts/{row['id']}/agent-events.json", - "trajectory": f"artifacts/{row['id']}/trajectory.json", - "evidence_metadata": f"artifacts/{row['id']}/evidence-metadata.json", - }, - } - for row in public_attempts - ], + "schema": "lex_browser_use_public_evidence_index/4", + "results": public_evidence_results, }, ) audit = { - "schema": "lex_browser_use_report/2", + "schema": "lex_browser_use_report/3", "result_count": len(attempts), - "score_authority": protocol.get("official_score_uses") - or "final execution for each browser and task", - "summary": summary, - "paired_summary": paired, + "score_authority": { + bundle["model"]: ( + (bundle["manifest"].get("protocol") or {}).get("official_score_uses") + or "final execution for each browser and task" + ) + for bundle in bundles + }, + "summary": summary_by_model, + "paired_summary": paired_by_model, "selection_sha256": selection.sha256, "evidence_bindings": { "attempts_sha256": attempts_sha256, - "manifest_sha256": _sha256(evidence / "manifest.json"), + "manifest_sha256": { + bundle["model"]: bundle["manifest_sha256"] for bundle in bundles + }, }, "failure_cause_review": { - "status": "complete" - if set(verified_causes) == failed_result_ids - else "incomplete", + "status": "complete" if set(verified_causes) == failed_result_ids else "incomplete", "reviewed_failures": len(verified_causes), "failed_results": len(failed_result_ids), - "bindings": failure_cause_review.get("bindings") or {}, + "bindings": { + model_id: review_value.get("bindings") or {} + for model_id, review_value in failure_reviews.items() + }, "source_sha256": file_sha256(FAILURE_CAUSE_REVIEW_PATH), }, "review": { @@ -2349,14 +2824,13 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: _write_json(stage / "data/summary.json", audit) _write_json(stage / "report-data.json", audit) public_env = { - "schema": "lex_browser_use_public_environment/1", + "schema": "lex_browser_use_public_environment/2", "protocol": { key: protocol.get(key) for key in ( "agent", "browser_eval_commit", "cowagent_commit", - "model", "reasoning_effort", "evaluator", "engine_order", @@ -2364,11 +2838,43 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: ) } | { + "models": list(MODEL_LABELS), + "model_labels": MODEL_LABELS, + "baseline_engine": BASELINE_ENGINE, "max_task_executions": 2, - "environment_reset": "runner configured for reset before each execution; legacy evidence has no independent per-execution preparation receipt", + "environment_reset": ( + "official site containers reset before every execution; " + f"{preparation_receipt_count}/{len(source_attempts)} execution-scoped " + "preparation receipts verified" + if preparation_receipts_complete + else "runner configured for reset before every execution; independent " + f"preparation receipts available for {preparation_receipt_count}/" + f"{len(source_attempts)} executions" + ), "score_uses": "frozen run manifest; report does not override measured score", "cost_and_latency": "sum of all executions used for the logical result", }, + "cohorts": { + bundle["model"]: { + "protocol": { + key: (bundle["manifest"].get("protocol") or {}).get(key) + for key in ( + "agent", + "browser_eval_commit", + "cowagent_commit", + "model", + "reasoning_effort", + "max_agent_steps", + "evaluator", + ) + }, + "run_id": bundle["manifest"].get("run_id"), + "created_at": bundle["manifest"].get("created_at"), + "attempts_sha256": bundle["attempts_sha256"], + "manifest_sha256": bundle["manifest_sha256"], + } + for bundle in bundles + }, "environment": { "platform": environment.get("platform"), "docker_name": environment.get("docker_name"), @@ -2407,10 +2913,9 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: "schema": "lex_browser_use_publication_privacy/1", "published": [ "official task intent and evaluator contracts", - "sanitized final agent response", - "sanitized evaluator expected/actual/assertions/errors", - "sanitized CowAgent event stream and BrowserTool trajectory", - "artifact byte sizes and SHA-256 bindings", + "sanitized response and evaluator evidence for every physical execution", + "sanitized CowAgent event stream and BrowserTool trajectory for every physical execution", + "per-execution measurements plus source and published-artifact SHA-256 bindings", ], "private_only": [ "raw HAR because it may contain cookies, headers and request bodies", @@ -2452,7 +2957,13 @@ def build_report(*, evidence: Path, output: Path, selection_path: Path) -> Path: def main() -> None: parser = argparse.ArgumentParser() - parser.add_argument("--evidence", required=True, type=Path) + parser.add_argument( + "--evidence", + required=True, + action="append", + type=Path, + help="isolated evidence directory for one model; repeat once per model", + ) parser.add_argument("--output", required=True, type=Path) parser.add_argument( "--selection", diff --git a/harness/browser_use/report_i18n.py b/harness/browser_use/report_i18n.py index 73294207d..a79c20c22 100644 --- a/harness/browser_use/report_i18n.py +++ b/harness/browser_use/report_i18n.py @@ -436,7 +436,10 @@ encoding="utf-8" ) ) -for _cause in _FAILURE_CAUSE_REVIEW["results"].values(): +_failure_cause_results = _FAILURE_CAUSE_REVIEW.get("results") or {} +for _cohort in (_FAILURE_CAUSE_REVIEW.get("cohorts") or {}).values(): + _failure_cause_results.update(_cohort.get("results") or {}) +for _cause in _failure_cause_results.values(): for _field in ("subcategory", "reason"): _localized = _cause.get(_field) if isinstance(_localized, dict) and _localized.get("zh") and _localized.get("en"): @@ -911,6 +914,114 @@ } ) +# Report-authored interface copy introduced by the multi-model renderer. Task +# names, task intents, expected/actual values, and execution excerpts are +# deliberately not covered here: they are measured material and may remain in +# their source language. +TEXT.update( + { + "Browser Use 联合多模型能力实测": "Browser Use capability evaluation across model cohorts", + "浏览器 Browser Use 能力对比": "Browser capability comparison on Browser Use tasks", + "先看结论": "Key findings", + "失败根因是报告解读的核心证据": "Failure root causes are core interpretation evidence", + "任务族覆盖": "Task-family coverage", + "单边通过": "One-browser-only passes", + "共同失败": "Both browsers failed", + "不等于共同根因": "Not necessarily the same root cause", + "模型内完整配对": "complete within-model pairs", + "本轮共有 9 个模型内不一致配对,且方向随模型层变化;固定样本不支持对全部 Browser Use 任务作统计外推。": "This run contains nine discordant within-model pairs, and their direction changes across model cohorts. The fixed sample does not support statistical generalization to all Browser Use tasks.", + "浏览器后端是参评选手;模型是分层实验变量,每个模型层内保持 Agent、任务、资源上限、站点与 evaluator 一致。": "Browser backends are the participants. Models are stratification variables; within each model cohort, the agent, tasks, resource limits, sites, and evaluators are held constant.", + "官方任务身份、公开意图与程序化验收合同。运行结果位于结果分析,不混入评测集定义。": "Official task identity, public intent, and programmatic acceptance contract. Measured outcomes appear in Results and are kept separate from the dataset definition.", + "查看本题运行结果": "View measured results for this task", + "评测集页面保持为冻结输入;实测输出、判分和轨迹在结果页查看。": "Dataset pages remain frozen inputs. Measured outputs, verdicts, and trajectories appear on result pages.", + "Chromium 结果": "Chromium results", + "Moli 1.1.3 结果": "Moli 1.1.3 results", + "全部浏览器对照": "Compare all browsers", + "程序自动定位:": "Automatic failure locator:", + "核验根因子类:": "Reviewed root-cause subtype:", + "核验原因:": "Reviewed cause:", + "查看修正依据、处理与影响": "View findings, treatment, and impact", + "每个版本只记录影响读者判断的正式修正;结论、分母、耗时与证据影响直接展示,技术依据按需展开。": "Each release records only formal corrections that affect reader interpretation. Effects on conclusions, denominators, latency, and evidence are shown directly, with supporting details available on demand.", + "判分权威": "Scoring authority", + "物理执行": "Physical execution", + "模型层": "Model cohort", + "正式处理": "Formal treatment", + "未命名版本": "Untitled release", + "未命名核验": "Untitled review", + "官方 Hard": "Official hard subset", + "步起": "minimum steps", + "模板 ID": "template ID", + "公开文件": "Public files", + "没有 evaluator 明细。": "No evaluator details are available.", + "没有可展示的 BrowserTool 调用。": "No BrowserTool calls are available to display.", + "最终回答 JSON": "Final answer JSON", + "完整执行证据 JSON": "Complete execution evidence JSON", + "Evaluator 明细 JSON": "Evaluator details JSON", + "动作轨迹 JSON": "Action trajectory JSON", + "脱敏事件日志 JSON": "Sanitized event log JSON", + "原始证据哈希": "Source-evidence hashes", + "返回任务页": "Return to the task page", + "任务与验收": "Task and acceptance", + "执行轨迹": "Execution trajectory", + "运行与证据": "Runtime and evidence", + "查看任务合同": "View task contract", + "查看任务与验收": "View task and acceptance", + "基准浏览器": "Baseline browser", + "对比浏览器": "Comparison browsers", + "完整配对": "Complete pairs", + "仅基准通过": "Baseline only", + "仅对比通过": "Comparison only", + "模型内浏览器配对结果": "Within-model browser pair outcomes", + "失败根因以当前证据绑定的逐结果核验为准": "Failure causes follow the per-result reviews bound to the current evidence", + "37/37 个失败结果已逐个绑定当前证据并完成核验。": "All 37 failed results are bound to the current evidence and have completed root-cause review.", + "查看逐结果根因与证据边界 →": "View per-result causes and evidence boundaries →", + "220/220 个物理执行均记录 reset、ready、应用健康检查和登录准备状态;事件流随后证明 CowAgent 自主产生 BrowserTool 调用序列。": "All 220 physical executions record reset, readiness, application-health, and sign-in preparation status. The subsequent event streams show CowAgent generating BrowserTool calls autonomously.", + ",只保留该题所需站点;env-ctrl 报告 ready 后,再做连续应用健康检查、清浏览器状态、登录测试账号并打开起始页。本轮 220/220 个物理执行都保存了环境准备回执,并与对应执行记录绑定。": ", keeping only the sites required by the task. After env-ctrl reports ready, the runner performs repeated application-health checks, clears browser state, signs in with test accounts, and opens the start page. All 220 physical executions retain preparation receipts bound to their execution records.", + "本轮 220/220 个物理执行均保存环境准备回执;公开环境摘要保留逐项状态,原始登录态不发布。": "All 220 physical executions retain environment-preparation receipts. The public environment summary preserves each status while authenticated session state remains private.", + "各模型任务成绩": "Task scores by model cohort", + "各模型逻辑结果耗时 P50": "P50 logical-result latency by model cohort", + "判分权威执行的原始证据绑定": "Source-evidence binding for the authoritative execution", + } +) + + +def _register_report_change_translations() -> None: + """Load explicit English release/review copy from the shared report model. + + Release history changes frequently, so keeping its English prose beside the + Chinese source prevents a new release from depending on a hand-maintained + global exact-string table. + """ + + path = Path(__file__).parents[2] / "research/report-changes.json" + if not path.is_file(): + return + data = json.loads(path.read_text(encoding="utf-8")) + + history_i18n = data.get("history_scope_i18n") or {} + if isinstance(data.get("history_scope"), str) and isinstance(history_i18n.get("en"), str): + TEXT[data["history_scope"]] = history_i18n["en"] + + for collection in ("browser_use_reviews", "browser_use_releases"): + for record in data.get(collection) or []: + english = ((record.get("i18n") or {}).get("en") or {}) + for field in ("title", "status", "scope", "findings", "corrections", "remaining", "summary"): + source, target = record.get(field), english.get(field) + if isinstance(source, str) and isinstance(target, str): + TEXT[source] = target + source_rows, target_rows = record.get("changes") or [], english.get("changes") or [] + if len(source_rows) != len(target_rows): + continue + for source_row, target_row in zip(source_rows, target_rows, strict=True): + if len(source_row) != len(target_row): + continue + for source, target in zip(source_row, target_row, strict=True): + if isinstance(source, str) and isinstance(target, str): + TEXT[source] = target + + +_register_report_change_translations() + TEXT.update( { # Project-authored task labels/rationales. The adjacent official task @@ -1002,6 +1113,7 @@ "任务 #": "Task #", "个试次": " attempts", "个任务": " tasks", + "个失败结果": " failed results", "实际": "actual", "期望": "expected", "暂无发布记录。": "No release has been recorded.", diff --git a/harness/browser_use/review.py b/harness/browser_use/review.py index 57484d06a..a8930a1eb 100644 --- a/harness/browser_use/review.py +++ b/harness/browser_use/review.py @@ -8,11 +8,10 @@ import hashlib import json -from datetime import datetime, timezone +from datetime import UTC, datetime from pathlib import Path from typing import Any - REQUIRED_ARTIFACTS = ("network_har", "agent_trace", "response", "evaluation", "events") @@ -30,9 +29,7 @@ def _attempts_sha256(path: Path) -> str: def _read_attempts(path: Path) -> list[dict[str, Any]]: return [ - json.loads(line) - for line in path.read_text(encoding="utf-8").splitlines() - if line.strip() + json.loads(line) for line in path.read_text(encoding="utf-8").splitlines() if line.strip() ] @@ -48,6 +45,9 @@ def review_results(*, evidence: Path, selection_sha256: str) -> dict[str, Any]: raise ValueError("manifest does not match the pre-registered selection") rows = _read_attempts(attempts_path) + model = str((manifest.get("protocol") or {}).get("model") or "") + if not model: + raise ValueError("manifest protocol must identify the evidence model") required_artifacts = list(REQUIRED_ARTIFACTS) if (manifest.get("protocol") or {}).get("environment_prepare_receipt") == "required": required_artifacts.append("environment_prepare") @@ -57,7 +57,11 @@ def review_results(*, evidence: Path, selection_sha256: str) -> dict[str, Any]: expected_provenance = manifest.get("environment", {}).get("official_evaluator", {}) for row in rows: + row_model = str(row.get("model") or model) + if row_model != model: + raise ValueError("attempt model differs from the evidence manifest") identity = { + "model": model, "engine": str(row.get("engine")), "task_id": int(row.get("task_id")), "attempt": int(row.get("attempt")), @@ -116,11 +120,9 @@ def review_results(*, evidence: Path, selection_sha256: str) -> dict[str, Any]: ) usage = row.get("model_usage") or {} transport_errors = row.get("model_transport_errors") or [] - checks["model_request_attempts_accounted"] = ( - isinstance(transport_errors, list) - and int(usage.get("request_attempt_count") or 0) - == int(usage.get("request_count") or 0) + len(transport_errors) - ) + checks["model_request_attempts_accounted"] = isinstance(transport_errors, list) and int( + usage.get("request_attempt_count") or 0 + ) == int(usage.get("request_count") or 0) + len(transport_errors) failed = sorted(name for name, passed in checks.items() if not passed) item = {**identity, "outcome": "verified" if not failed else "unresolved", "checks": checks} @@ -138,7 +140,7 @@ def review_results(*, evidence: Path, selection_sha256: str) -> dict[str, Any]: "kind": "deterministic programmatic review", "version": 1, }, - "reviewed_at": datetime.now(timezone.utc).isoformat(), + "reviewed_at": datetime.now(UTC).isoformat(), "bindings": { "attempts_sha256": _attempts_sha256(attempts_path), "manifest_sha256": _sha256(manifest_path), @@ -148,7 +150,9 @@ def review_results(*, evidence: Path, selection_sha256: str) -> dict[str, Any]: "scope": "evidence completeness, byte identity, and recorded score/status agreement", "score_authority": "unmodified WebArena-Verified evaluator", "manual_score_override": "not permitted", - "adjudication": "any disagreement or missing artifact remains unresolved and blocks publication", + "adjudication": ( + "any disagreement or missing artifact remains unresolved and blocks publication" + ), }, "coverage": {"recorded_attempts": len(rows), "reviewed_attempts": len(reviewed)}, "disagreements": disagreements, diff --git a/harness/browser_use/runner.py b/harness/browser_use/runner.py index f09de88bd..df90f6eaf 100644 --- a/harness/browser_use/runner.py +++ b/harness/browser_use/runner.py @@ -3,6 +3,7 @@ from __future__ import annotations import argparse +import fcntl import hashlib import importlib.metadata import ipaddress @@ -11,9 +12,12 @@ import platform import subprocess import sys +import tempfile import time import traceback import uuid +from collections.abc import Iterator +from contextlib import contextmanager from pathlib import Path from typing import Any from urllib.parse import urlparse @@ -77,6 +81,7 @@ # healthy shortly afterwards. A longer ceiling changes no measured task time; # it only keeps environment preparation from aborting a formal cohort. ENV_CTRL_READY_TIMEOUT_SECONDS = 600 +PHYSICAL_EXECUTION_LOCK_NAMESPACE = "webarena-verified-fixed-site-stack-v1" AMD64_PHP_SITES = frozenset( {WebArenaSite.SHOPPING, WebArenaSite.SHOPPING_ADMIN, WebArenaSite.REDDIT} ) @@ -85,6 +90,47 @@ QEMU_X86_64_CONTAINER_PATH = "/usr/local/bin/qemu-x86_64" +def _physical_execution_lock_path() -> Path: + # The WebArena stack uses fixed host ports and is shared by every run owned + # by this OS user. The lock therefore must not follow BROWSER_EVAL_DATA_HOME: + # two cohorts may intentionally write to different evidence directories + # while still mutating the same containers and databases. + return ( + Path(tempfile.gettempdir()) + / f"browser-eval-{os.getuid()}" + / "locks" + / f"{PHYSICAL_EXECUTION_LOCK_NAMESPACE}.lock" + ) + + +@contextmanager +def _physical_execution_lock(lock_path: Path) -> Iterator[float]: + """Serialize the full attempt lifecycle for the shared fixed-port site stack.""" + lock_path.parent.mkdir(parents=True, exist_ok=True) + with lock_path.open("a+b") as lock_file: + wait_started = time.perf_counter() + fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX) + wait_ms = (time.perf_counter() - wait_started) * 1000 + try: + yield wait_ms + finally: + fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN) + + +def _environment_prepare_receipt( + sites: tuple[WebArenaSite, ...], *, lock_wait_ms: float, lock_namespace: str +) -> dict[str, Any]: + return { + "required_sites": [site.value for site in sites], + "reset_requested": True, + "physical_execution_lock": { + "namespace": lock_namespace, + "wait_ms": round(lock_wait_ms, 3), + }, + "status": "running", + } + + def _load_repo_env(repo_root: Path) -> dict[str, str]: """Load repository defaults, with explicit process overrides for automation.""" env_file = repo_root / ".env" @@ -529,6 +575,19 @@ def _resource_summary( def _run_attempt( # noqa: PLR0913 + *, + lock_path: Path, + **kwargs: Any, +) -> dict[str, Any]: + with _physical_execution_lock(lock_path) as lock_wait_ms: + return _run_attempt_locked( + **kwargs, + lock_wait_ms=lock_wait_ms, + lock_namespace=PHYSICAL_EXECUTION_LOCK_NAMESPACE, + ) + + +def _run_attempt_locked( # noqa: PLR0913 *, benchmark: WebArenaVerified, selected: PilotTask, @@ -541,6 +600,8 @@ def _run_attempt( # noqa: PLR0913 model: OpenAIResponsesModel, sample_interval_ms: int, max_agent_steps: int, + lock_wait_ms: float, + lock_namespace: str, ) -> dict[str, Any]: task = benchmark.get_task(selected.task_id) sites = tuple(task.sites) @@ -550,11 +611,9 @@ def _run_attempt( # noqa: PLR0913 attempt_dir.mkdir(parents=True, exist_ok=True) prepare_path = attempt_dir / "environment_prepare.json" prepare_started = time.perf_counter() - prepare_receipt: dict[str, Any] = { - "required_sites": [site.value for site in sites], - "reset_requested": True, - "status": "running", - } + prepare_receipt = _environment_prepare_receipt( + sites, lock_wait_ms=lock_wait_ms, lock_namespace=lock_namespace + ) try: _prepare_sites(sites) prepare_receipt["status"] = "ready" @@ -610,9 +669,7 @@ def _run_attempt( # noqa: PLR0913 model.reset() service = tool._get_service() service._submit(lambda: None) - browser_session_readiness_retries = _login_context( - service, sites, benchmark, start_urls - ) + browser_session_readiness_retries = _login_context(service, sites, benchmark, start_urls) service._submit( lambda: ( recorder.start(service._context), @@ -716,6 +773,7 @@ def _run_attempt( # noqa: PLR0913 ) return { "schema": SCHEMA, + "model": model.model, "engine": engine, "task_id": selected.task_id, "tier": selected.tier, @@ -756,16 +814,26 @@ def _run_attempt( # noqa: PLR0913 } -def _load_completed_attempts(path: Path) -> dict[tuple[str, int, int], dict[str, Any]]: +def _load_completed_attempts( + path: Path, expected_model: str | None = None +) -> dict[tuple[str, str, int, int], dict[str, Any]]: """Load durable attempt rows so an interrupted multi-hour pilot can resume safely.""" if not path.exists(): return {} - completed: dict[tuple[str, int, int], dict[str, Any]] = {} + completed: dict[tuple[str, str, int, int], dict[str, Any]] = {} for line_number, line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): if not line.strip(): continue row = json.loads(line) - key = (str(row["engine"]), int(row["task_id"]), int(row["attempt"])) + model = str(row.get("model") or expected_model or "") + if not model: + raise ValueError(f"attempt does not identify its model at {path}:{line_number}") + if expected_model and model != expected_model: + raise ValueError( + f"attempt model differs from the evidence cohort at {path}:{line_number}" + ) + row["model"] = model + key = (model, str(row["engine"]), int(row["task_id"]), int(row["attempt"])) if key in completed: raise ValueError(f"duplicate attempt at {path}:{line_number}: {key}") completed[key] = row @@ -811,6 +879,7 @@ def run(args: argparse.Namespace) -> Path: # noqa: PLR0915 output_dir = Path(args.output).expanduser().resolve() output_dir.mkdir(parents=True, exist_ok=True) + physical_execution_lock_path = _physical_execution_lock_path() selection = load_pilot(args.selection) benchmark = _configure_benchmark() resolved = validate_against_official(selection, benchmark) @@ -829,7 +898,7 @@ def run(args: argparse.Namespace) -> Path: # noqa: PLR0915 endpoints: dict[str, str] = {} attempts_file = output_dir / "attempts.jsonl" run_config_file = output_dir / "run-config.json" - completed = _load_completed_attempts(attempts_file) + completed = _load_completed_attempts(attempts_file, args.model) image_metadata: dict[str, dict[str, Any]] = {} site_image_metadata = { site.value: { @@ -912,7 +981,7 @@ def run(args: argparse.Namespace) -> Path: # noqa: PLR0915 order = ("chromium", "moli") if task_index % 2 == 0 else ("moli", "chromium") for engine in order: for attempt in (1, 2): - key = (engine, selected.task_id, attempt) + key = (args.model, engine, selected.task_id, attempt) if key in completed: row = completed[key] print( @@ -930,10 +999,11 @@ def run(args: argparse.Namespace) -> Path: # noqa: PLR0915 if float(row["score"]) == 1.0: break continue - primary = completed.get((engine, selected.task_id, 1)) + primary = completed.get((args.model, engine, selected.task_id, 1)) if attempt == 2 and primary is not None and float(primary["score"]) == 1.0: break row = _run_attempt( + lock_path=physical_execution_lock_path, benchmark=benchmark, selected=selected, engine=engine, diff --git a/research/browser-use-failure-cause-review.json b/research/browser-use-failure-cause-review.json index db842ea96..cf6849387 100644 --- a/research/browser-use-failure-cause-review.json +++ b/research/browser-use-failure-cause-review.json @@ -1,6 +1,8 @@ { - "schema": "lex_browser_use_failure_cause_review/1", + "schema": "lex_browser_use_layered_failure_cause_review/2", "benchmark": "WebArena-Verified 1.2.3", + "cohorts": { + "gpt-5.6-sol": { "reviewed_at": "2026-09-11", "reviewer_class": "evidence analyst", "bindings": { @@ -16,42 +18,100 @@ "coverage": {"failed_results": 37, "verified_results": 37}, "unresolved": [], "results": { - "chromium-124": {"status":"verified","subcategory":{"code":"agent_retrieval_coverage","zh":"检索覆盖不足","en":"Incomplete retrieval coverage"},"reason":{"zh":"Agent 只覆盖部分无线耳机,最高价返回 169.99,遗漏官方全集的 298.0 上界。","en":"The Agent covered only part of the wireless-earphone set, returning 169.99 and missing the official 298.0 upper bound."}}, - "moli-124": {"status":"verified","subcategory":{"code":"agent_retrieval_coverage","zh":"检索覆盖不足","en":"Incomplete retrieval coverage"},"reason":{"zh":"Agent 只覆盖部分无线耳机,最高价返回 199.9,遗漏官方全集的 298.0 上界。","en":"The Agent covered only part of the wireless-earphone set, returning 199.9 and missing the official 298.0 upper bound."}}, - "chromium-258": {"status":"verified","subcategory":{"code":"agent_navigation_semantics","zh":"导航语义错误","en":"Navigation-semantics error"},"reason":{"zh":"Agent 停在 GitLab Explore 总页 /explore,没有启用 Public 过滤参数 visibility_level=20。","en":"The Agent stopped at generic /explore without applying the Public filter visibility_level=20."}}, - "moli-258": {"status":"verified","subcategory":{"code":"agent_navigation_semantics","zh":"导航语义错误","en":"Navigation-semantics error"},"reason":{"zh":"Agent 停在 GitLab Explore 总页 /explore,没有启用 Public 过滤参数 visibility_level=20。","en":"The Agent stopped at generic /explore without applying the Public filter visibility_level=20."}}, - "chromium-279": {"status":"verified","subcategory":{"code":"agent_retrieval_coverage","zh":"检索覆盖不足","en":"Incomplete retrieval coverage"},"reason":{"zh":"Agent 仅收集 7/12 个 Sony 蓝牙耳机,遗漏条目同时包含 18.99 最低价和 406.0 最高价。","en":"The Agent collected only 7 of 12 Sony Bluetooth headphones, omitting both the 18.99 minimum and 406.0 maximum."}}, - "moli-279": {"status":"verified","subcategory":{"code":"agent_retrieval_coverage","zh":"检索覆盖不足","en":"Incomplete retrieval coverage"},"reason":{"zh":"Agent 仅收集 6/12 个 Sony 蓝牙耳机,遗漏条目同时包含 18.99 最低价和 406.0 最高价。","en":"The Agent collected only 6 of 12 Sony Bluetooth headphones, omitting both the 18.99 minimum and 406.0 maximum."}}, - "chromium-284": {"status":"verified","subcategory":{"code":"agent_comparison_strategy","zh":"候选比较错误","en":"Candidate-comparison error"},"reason":{"zh":"Agent 选择了可放 20 双鞋的 Kingpavonini,没有完成所有合格候选的价格比较;官方最低价是 OnlyEasy 24-pocket。","en":"The Agent chose a 20-pair Kingpavonini rack without completing the price comparison; the official cheapest qualifying item is OnlyEasy 24-pocket."}}, - "moli-284": {"status":"verified","subcategory":{"code":"agent_comparison_strategy","zh":"候选比较错误","en":"Candidate-comparison error"},"reason":{"zh":"Agent 选择了 72 双容量的 Aeitc,没有完成所有合格候选的价格比较;官方最低价是 OnlyEasy 24-pocket。","en":"The Agent chose a 72-pair Aeitc rack without completing the price comparison; the official cheapest qualifying item is OnlyEasy 24-pocket."}}, - "chromium-339": {"status":"verified","subcategory":{"code":"benchmark_route_contract","zh":"路由验收缺陷","en":"Route-acceptance defect"},"reason":{"zh":"Agent 已打开含 state=opened 与 label_name[]=bug 的正确页面;官方规则错误要求 /issues/.*,拒绝合法的无尾斜杠 /issues?... 路由。","en":"The Agent opened the correct filtered page; the official /issues/.* rule rejects the valid no-trailing-slash /issues?... route."}}, - "moli-339": {"status":"verified","subcategory":{"code":"benchmark_route_contract","zh":"路由验收缺陷","en":"Route-acceptance defect"},"reason":{"zh":"Agent 已打开含 state=opened 与 label_name[]=bug 的正确页面;官方规则错误要求 /issues/.*,拒绝合法的无尾斜杠 /issues?... 路由。","en":"The Agent opened the correct filtered page; the official /issues/.* rule rejects the valid no-trailing-slash /issues?... route."}}, - "chromium-464": {"status":"verified","subcategory":{"code":"agent_step_budget","zh":"步骤预算耗尽","en":"Step-budget exhaustion"},"reason":{"zh":"Agent 在 40 步内未完成统计评论、编辑描述和保存,证据中没有目标商品保存请求。","en":"The Agent did not finish counting reviews, editing, and saving within 40 steps; no target-product save request exists."}}, - "moli-464": {"status":"verified","subcategory":{"code":"agent_step_budget","zh":"步骤预算耗尽","en":"Step-budget exhaustion"},"reason":{"zh":"Agent 在 40 步内未完成统计评论、编辑描述和保存,证据中没有目标商品保存请求。","en":"The Agent did not finish counting reviews, editing, and saving within 40 steps; no target-product save request exists."}}, - "moli-448": {"status":"verified","subcategory":{"code":"evidence_capture","zh":"网络证据匹配缺陷","en":"Network-evidence matching defect"},"reason":{"zh":"HAR 有字段正确的 302 /profile POST,页面也已更新;同一提交又被记录为 200,evaluator 只选中 200 记录而误判。","en":"The HAR has a correct 302 /profile POST and the page is updated, but a duplicate 200 record is selected by the evaluator, causing a false negative."}}, - "chromium-470": {"status":"verified","subcategory":{"code":"site_dependency_failure","zh":"站点依赖故障","en":"Site dependency failure"},"reason":{"zh":"确认取消后 Magento 返回 No alive nodes found in your cluster,订单仍为 Pending;本地搜索服务不可用。","en":"After cancellation confirmation Magento returned No alive nodes found in your cluster and the order remained Pending; the local search service was unavailable."}}, - "moli-470": {"status":"verified","subcategory":{"code":"site_dependency_failure","zh":"站点依赖故障","en":"Site dependency failure"},"reason":{"zh":"确认取消后 Magento 返回 No alive nodes found in your cluster,订单仍为 Pending;本地搜索服务不可用。","en":"After cancellation confirmation Magento returned No alive nodes found in your cluster and the order remained Pending; the local search service was unavailable."}}, - "chromium-475": {"status":"verified","subcategory":{"code":"benchmark_form_contract","zh":"表单验收缺陷","en":"Form-acceptance defect"},"reason":{"zh":"空项目已按正确名称、路径和命名空间创建;HTML 会省略未勾选的 initialize_with_readme,官方规则却要求显式值 0。","en":"The empty project was created with the correct name, path, and namespace; HTML omits unchecked initialize_with_readme, while the official rule requires an explicit 0."}}, - "moli-475": {"status":"verified","subcategory":{"code":"benchmark_form_contract","zh":"表单验收缺陷","en":"Form-acceptance defect"},"reason":{"zh":"空项目已按正确名称、路径和命名空间创建;HTML 会省略未勾选的 initialize_with_readme,官方规则却要求显式值 0。","en":"The empty project was created with the correct name, path, and namespace; HTML omits unchecked initialize_with_readme, while the official rule requires an explicit 0."}}, - "moli-506": {"status":"verified","subcategory":{"code":"agent_step_budget","zh":"步骤预算耗尽","en":"Step-budget exhaustion"},"reason":{"zh":"达到 40 步时仍在购物流程;证据只有购物车增删,没有付款或成功页事件。","en":"At the 40-step limit the Agent was still shopping; evidence has cart changes but no payment or success-page event."}}, - "chromium-510": {"status":"verified","subcategory":{"code":"agent_product_selection","zh":"商品选择错误","en":"Incorrect product selection"},"reason":{"zh":"Agent 完成结账,但买的是 Hiquphon Tweeters,而非官方目标 Atlantic Technology FS-7.0-GLB;筛选排序执行错误。","en":"The Agent completed checkout with Hiquphon Tweeters instead of the official Atlantic Technology FS-7.0-GLB target; the ranking was applied incorrectly."}}, - "moli-510": {"status":"verified","subcategory":{"code":"agent_step_budget","zh":"步骤预算耗尽","en":"Step-budget exhaustion"},"reason":{"zh":"Agent 在 40 步内反复比较和调整购物车,最终没有付款成功事件。","en":"The Agent repeatedly compared products and changed the cart within 40 steps, but never produced a successful payment event."}}, - "chromium-528": {"status":"verified","subcategory":{"code":"benchmark_instruction_contract","zh":"任务与验收矛盾","en":"Instruction-evaluator contradiction"},"reason":{"zh":"Agent 正确填写退款表单并按“不要提交”停在提交前;官方 evaluator 却要求出现 dummy_bin POST,任务与验收相互矛盾。","en":"The Agent correctly filled the refund form and stopped before submission, but the evaluator requires a dummy_bin POST, contradicting the instruction."}}, - "moli-528": {"status":"verified","subcategory":{"code":"benchmark_instruction_contract","zh":"任务与验收矛盾","en":"Instruction-evaluator contradiction"},"reason":{"zh":"Agent 正确填写退款表单并按“不要提交”停在提交前;官方 evaluator 却要求出现 dummy_bin POST,任务与验收相互矛盾。","en":"The Agent correctly filled the refund form and stopped before submission, but the evaluator requires a dummy_bin POST, contradicting the instruction."}}, - "chromium-547": {"status":"verified","subcategory":{"code":"agent_step_budget","zh":"步骤预算耗尽","en":"Step-budget exhaustion"},"reason":{"zh":"Agent 在 40 步内未完成新增 Brown 色板和 S 码变体,两个必要保存请求都不存在。","en":"The Agent did not finish adding the Brown swatch and size-S variation within 40 steps; both required save requests are absent."}}, - "moli-547": {"status":"verified","subcategory":{"code":"site_backend_failure","zh":"站点保存故障","en":"Site save failure"},"reason":{"zh":"Agent 到达变体保存阶段后 Magento 返回 The stock item was unable to be saved,S/Brown 变体未建立。","en":"At the variation-save stage Magento returned The stock item was unable to be saved, so the S/Brown variation was not created."}}, - "moli-595": {"status":"verified","subcategory":{"code":"evidence_capture","zh":"网络证据缺失","en":"Missing network evidence"},"reason":{"zh":"页面显示已订阅且 HAR 有 subscribe.json 200 POST,但 Moli 记录缺少 Referer 和响应 JSON,evaluator 无法确认 subscribed=true。","en":"The page shows subscribed and the HAR has a 200 subscribe.json POST, but Moli lacks Referer and response JSON, so the evaluator cannot confirm subscribed=true."}}, - "chromium-600": {"status":"verified","subcategory":{"code":"benchmark_route_contract","zh":"路由验收缺陷","en":"Route-acceptance defect"},"reason":{"zh":"合法的 /submit/gaming 302 POST 含正确标题和论坛 ID 10039;evaluator 只接受精确 /submit。","en":"The valid /submit/gaming 302 POST contains the correct title and forum ID 10039; the evaluator accepts only exact /submit."}}, - "moli-600": {"status":"verified","subcategory":{"code":"benchmark_route_contract","zh":"路由验收缺陷","en":"Route-acceptance defect"},"reason":{"zh":"合法的 /submit/gaming 302 POST 含正确标题和论坛 ID 10039;evaluator 只接受精确 /submit。","en":"The valid /submit/gaming 302 POST contains the correct title and forum ID 10039; the evaluator accepts only exact /submit."}}, - "moli-615": {"status":"verified","subcategory":{"code":"agent_field_omission","zh":"提交字段遗漏","en":"Submission-field omission"},"reason":{"zh":"发帖请求包含正确标题和 Connecticut 论坛 ID,但 submission[url] 为空,遗漏 Bald Eagle 图片 URL。","en":"The post has the correct title and Connecticut forum ID, but submission[url] is empty, omitting the Bald Eagle image URL."}}, - "chromium-625": {"status":"verified","subcategory":{"code":"benchmark_route_contract","zh":"路由验收缺陷","en":"Route-acceptance defect"},"reason":{"zh":"合法的 /submit/technology 302 POST 含正确标题、正文和论坛 ID 10043;evaluator 只接受精确 /submit。","en":"The valid /submit/technology 302 POST contains the correct title, body, and forum ID 10043; the evaluator accepts only exact /submit."}}, - "moli-625": {"status":"verified","subcategory":{"code":"benchmark_route_contract","zh":"路由验收缺陷","en":"Route-acceptance defect"},"reason":{"zh":"合法的 /submit/technology 302 POST 含正确标题、正文和论坛 ID 10043;evaluator 只接受精确 /submit。","en":"The valid /submit/technology 302 POST contains the correct title, body, and forum ID 10043; the evaluator accepts only exact /submit."}}, - "chromium-676": {"status":"verified","subcategory":{"code":"benchmark_route_contract","zh":"路由验收缺陷","en":"Route-acceptance defect"},"reason":{"zh":"HAR 有完整 sales_order_grid 与 status=fraud 请求,Referer 为合法的 /sales/order/;官方规则把尾斜杠差异判为失败。","en":"The HAR has the complete sales_order_grid request with status=fraud and a valid /sales/order/ Referer; the official rule fails on the trailing slash."}}, - "moli-676": {"status":"verified","subcategory":{"code":"evidence_capture","zh":"网络证据缺失","en":"Missing network evidence"},"reason":{"zh":"HAR 有完整 sales_order_grid 与 status=fraud 请求,但 Moli 未记录 Referer,evaluator 无法匹配。","en":"The HAR has the complete sales_order_grid request with status=fraud, but Moli omits its Referer, preventing evaluator matching."}}, - "chromium-694": {"status":"verified","subcategory":{"code":"site_backend_failure","zh":"站点保存故障","en":"Site save failure"},"reason":{"zh":"Agent 两次创建商品均被 Magento 以 The stock item was unable to be saved 拒绝,目录仍为空。","en":"Magento rejected both product-creation attempts with The stock item was unable to be saved, leaving the catalog empty."}}, - "moli-694": {"status":"verified","subcategory":{"code":"site_backend_failure","zh":"站点保存故障","en":"Site save failure"},"reason":{"zh":"Agent 两次创建商品均被 Magento 以 The stock item was unable to be saved 拒绝,商品未创建。","en":"Magento rejected both product-creation attempts with The stock item was unable to be saved, so the product was not created."}}, - "chromium-699": {"status":"verified","subcategory":{"code":"benchmark_evaluator_defect","zh":"evaluator 实现错误","en":"Evaluator implementation defect"},"reason":{"zh":"evaluator 在比较实际提交前即因单元素 Alternatives 配置抛出 Alternatives require 2+ items,零分来自官方规则错误。","en":"Before comparing the submission, the evaluator throws Alternatives require 2+ items for a one-item configuration; the zero comes from the official rule."}}, - "moli-699": {"status":"verified","subcategory":{"code":"benchmark_evaluator_defect","zh":"evaluator 实现错误","en":"Evaluator implementation defect"},"reason":{"zh":"evaluator 在比较实际提交前即因单元素 Alternatives 配置抛出 Alternatives require 2+ items,零分来自官方规则错误。","en":"Before comparing the submission, the evaluator throws Alternatives require 2+ items for a one-item configuration; the zero comes from the official rule."}}, - "chromium-771": {"status":"verified","subcategory":{"code":"agent_workflow_completeness","zh":"批量操作遗漏","en":"Incomplete batch workflow"},"reason":{"zh":"任务要求批准全部至少 4 星评论;Chromium 没有保存官方要求的评论 352 和 347,而 Moli 在相同条件下完成了两项。","en":"The task requires approving all reviews rated at least four stars; Chromium did not save required reviews 352 and 347, while Moli completed both under the same controls."}} + "gpt-5.6-sol-chromium-124": {"status":"verified","subcategory":{"code":"agent_retrieval_coverage","zh":"检索覆盖不足","en":"Incomplete retrieval coverage"},"reason":{"zh":"Agent 只覆盖部分无线耳机,最高价返回 169.99,遗漏官方全集的 298.0 上界。","en":"The Agent covered only part of the wireless-earphone set, returning 169.99 and missing the official 298.0 upper bound."}}, + "gpt-5.6-sol-moli-124": {"status":"verified","subcategory":{"code":"agent_retrieval_coverage","zh":"检索覆盖不足","en":"Incomplete retrieval coverage"},"reason":{"zh":"Agent 只覆盖部分无线耳机,最高价返回 199.9,遗漏官方全集的 298.0 上界。","en":"The Agent covered only part of the wireless-earphone set, returning 199.9 and missing the official 298.0 upper bound."}}, + "gpt-5.6-sol-chromium-258": {"status":"verified","subcategory":{"code":"agent_navigation_semantics","zh":"导航语义错误","en":"Navigation-semantics error"},"reason":{"zh":"Agent 停在 GitLab Explore 总页 /explore,没有启用 Public 过滤参数 visibility_level=20。","en":"The Agent stopped at generic /explore without applying the Public filter visibility_level=20."}}, + "gpt-5.6-sol-moli-258": {"status":"verified","subcategory":{"code":"agent_navigation_semantics","zh":"导航语义错误","en":"Navigation-semantics error"},"reason":{"zh":"Agent 停在 GitLab Explore 总页 /explore,没有启用 Public 过滤参数 visibility_level=20。","en":"The Agent stopped at generic /explore without applying the Public filter visibility_level=20."}}, + "gpt-5.6-sol-chromium-279": {"status":"verified","subcategory":{"code":"agent_retrieval_coverage","zh":"检索覆盖不足","en":"Incomplete retrieval coverage"},"reason":{"zh":"Agent 仅收集 7/12 个 Sony 蓝牙耳机,遗漏条目同时包含 18.99 最低价和 406.0 最高价。","en":"The Agent collected only 7 of 12 Sony Bluetooth headphones, omitting both the 18.99 minimum and 406.0 maximum."}}, + "gpt-5.6-sol-moli-279": {"status":"verified","subcategory":{"code":"agent_retrieval_coverage","zh":"检索覆盖不足","en":"Incomplete retrieval coverage"},"reason":{"zh":"Agent 仅收集 6/12 个 Sony 蓝牙耳机,遗漏条目同时包含 18.99 最低价和 406.0 最高价。","en":"The Agent collected only 6 of 12 Sony Bluetooth headphones, omitting both the 18.99 minimum and 406.0 maximum."}}, + "gpt-5.6-sol-chromium-284": {"status":"verified","subcategory":{"code":"agent_comparison_strategy","zh":"候选比较错误","en":"Candidate-comparison error"},"reason":{"zh":"Agent 选择了可放 20 双鞋的 Kingpavonini,没有完成所有合格候选的价格比较;官方最低价是 OnlyEasy 24-pocket。","en":"The Agent chose a 20-pair Kingpavonini rack without completing the price comparison; the official cheapest qualifying item is OnlyEasy 24-pocket."}}, + "gpt-5.6-sol-moli-284": {"status":"verified","subcategory":{"code":"agent_comparison_strategy","zh":"候选比较错误","en":"Candidate-comparison error"},"reason":{"zh":"Agent 选择了 72 双容量的 Aeitc,没有完成所有合格候选的价格比较;官方最低价是 OnlyEasy 24-pocket。","en":"The Agent chose a 72-pair Aeitc rack without completing the price comparison; the official cheapest qualifying item is OnlyEasy 24-pocket."}}, + "gpt-5.6-sol-chromium-339": {"status":"verified","subcategory":{"code":"benchmark_route_contract","zh":"路由验收缺陷","en":"Route-acceptance defect"},"reason":{"zh":"Agent 已打开含 state=opened 与 label_name[]=bug 的正确页面;官方规则错误要求 /issues/.*,拒绝合法的无尾斜杠 /issues?... 路由。","en":"The Agent opened the correct filtered page; the official /issues/.* rule rejects the valid no-trailing-slash /issues?... route."}}, + "gpt-5.6-sol-moli-339": {"status":"verified","subcategory":{"code":"benchmark_route_contract","zh":"路由验收缺陷","en":"Route-acceptance defect"},"reason":{"zh":"Agent 已打开含 state=opened 与 label_name[]=bug 的正确页面;官方规则错误要求 /issues/.*,拒绝合法的无尾斜杠 /issues?... 路由。","en":"The Agent opened the correct filtered page; the official /issues/.* rule rejects the valid no-trailing-slash /issues?... route."}}, + "gpt-5.6-sol-chromium-464": {"status":"verified","subcategory":{"code":"agent_step_budget","zh":"步骤预算耗尽","en":"Step-budget exhaustion"},"reason":{"zh":"Agent 在 40 步内未完成统计评论、编辑描述和保存,证据中没有目标商品保存请求。","en":"The Agent did not finish counting reviews, editing, and saving within 40 steps; no target-product save request exists."}}, + "gpt-5.6-sol-moli-464": {"status":"verified","subcategory":{"code":"agent_step_budget","zh":"步骤预算耗尽","en":"Step-budget exhaustion"},"reason":{"zh":"Agent 在 40 步内未完成统计评论、编辑描述和保存,证据中没有目标商品保存请求。","en":"The Agent did not finish counting reviews, editing, and saving within 40 steps; no target-product save request exists."}}, + "gpt-5.6-sol-moli-448": {"status":"verified","subcategory":{"code":"evidence_capture","zh":"网络证据匹配缺陷","en":"Network-evidence matching defect"},"reason":{"zh":"HAR 有字段正确的 302 /profile POST,页面也已更新;同一提交又被记录为 200,evaluator 只选中 200 记录而误判。","en":"The HAR has a correct 302 /profile POST and the page is updated, but a duplicate 200 record is selected by the evaluator, causing a false negative."}}, + "gpt-5.6-sol-chromium-470": {"status":"verified","subcategory":{"code":"site_dependency_failure","zh":"站点依赖故障","en":"Site dependency failure"},"reason":{"zh":"确认取消后 Magento 返回 No alive nodes found in your cluster,订单仍为 Pending;本地搜索服务不可用。","en":"After cancellation confirmation Magento returned No alive nodes found in your cluster and the order remained Pending; the local search service was unavailable."}}, + "gpt-5.6-sol-moli-470": {"status":"verified","subcategory":{"code":"site_dependency_failure","zh":"站点依赖故障","en":"Site dependency failure"},"reason":{"zh":"确认取消后 Magento 返回 No alive nodes found in your cluster,订单仍为 Pending;本地搜索服务不可用。","en":"After cancellation confirmation Magento returned No alive nodes found in your cluster and the order remained Pending; the local search service was unavailable."}}, + "gpt-5.6-sol-chromium-475": {"status":"verified","subcategory":{"code":"benchmark_form_contract","zh":"表单验收缺陷","en":"Form-acceptance defect"},"reason":{"zh":"空项目已按正确名称、路径和命名空间创建;HTML 会省略未勾选的 initialize_with_readme,官方规则却要求显式值 0。","en":"The empty project was created with the correct name, path, and namespace; HTML omits unchecked initialize_with_readme, while the official rule requires an explicit 0."}}, + "gpt-5.6-sol-moli-475": {"status":"verified","subcategory":{"code":"benchmark_form_contract","zh":"表单验收缺陷","en":"Form-acceptance defect"},"reason":{"zh":"空项目已按正确名称、路径和命名空间创建;HTML 会省略未勾选的 initialize_with_readme,官方规则却要求显式值 0。","en":"The empty project was created with the correct name, path, and namespace; HTML omits unchecked initialize_with_readme, while the official rule requires an explicit 0."}}, + "gpt-5.6-sol-moli-506": {"status":"verified","subcategory":{"code":"agent_step_budget","zh":"步骤预算耗尽","en":"Step-budget exhaustion"},"reason":{"zh":"达到 40 步时仍在购物流程;证据只有购物车增删,没有付款或成功页事件。","en":"At the 40-step limit the Agent was still shopping; evidence has cart changes but no payment or success-page event."}}, + "gpt-5.6-sol-chromium-510": {"status":"verified","subcategory":{"code":"agent_product_selection","zh":"商品选择错误","en":"Incorrect product selection"},"reason":{"zh":"Agent 完成结账,但买的是 Hiquphon Tweeters,而非官方目标 Atlantic Technology FS-7.0-GLB;筛选排序执行错误。","en":"The Agent completed checkout with Hiquphon Tweeters instead of the official Atlantic Technology FS-7.0-GLB target; the ranking was applied incorrectly."}}, + "gpt-5.6-sol-moli-510": {"status":"verified","subcategory":{"code":"agent_step_budget","zh":"步骤预算耗尽","en":"Step-budget exhaustion"},"reason":{"zh":"Agent 在 40 步内反复比较和调整购物车,最终没有付款成功事件。","en":"The Agent repeatedly compared products and changed the cart within 40 steps, but never produced a successful payment event."}}, + "gpt-5.6-sol-chromium-528": {"status":"verified","subcategory":{"code":"benchmark_instruction_contract","zh":"任务与验收矛盾","en":"Instruction-evaluator contradiction"},"reason":{"zh":"Agent 正确填写退款表单并按“不要提交”停在提交前;官方 evaluator 却要求出现 dummy_bin POST,任务与验收相互矛盾。","en":"The Agent correctly filled the refund form and stopped before submission, but the evaluator requires a dummy_bin POST, contradicting the instruction."}}, + "gpt-5.6-sol-moli-528": {"status":"verified","subcategory":{"code":"benchmark_instruction_contract","zh":"任务与验收矛盾","en":"Instruction-evaluator contradiction"},"reason":{"zh":"Agent 正确填写退款表单并按“不要提交”停在提交前;官方 evaluator 却要求出现 dummy_bin POST,任务与验收相互矛盾。","en":"The Agent correctly filled the refund form and stopped before submission, but the evaluator requires a dummy_bin POST, contradicting the instruction."}}, + "gpt-5.6-sol-chromium-547": {"status":"verified","subcategory":{"code":"agent_step_budget","zh":"步骤预算耗尽","en":"Step-budget exhaustion"},"reason":{"zh":"Agent 在 40 步内未完成新增 Brown 色板和 S 码变体,两个必要保存请求都不存在。","en":"The Agent did not finish adding the Brown swatch and size-S variation within 40 steps; both required save requests are absent."}}, + "gpt-5.6-sol-moli-547": {"status":"verified","subcategory":{"code":"site_backend_failure","zh":"站点保存故障","en":"Site save failure"},"reason":{"zh":"Agent 到达变体保存阶段后 Magento 返回 The stock item was unable to be saved,S/Brown 变体未建立。","en":"At the variation-save stage Magento returned The stock item was unable to be saved, so the S/Brown variation was not created."}}, + "gpt-5.6-sol-moli-595": {"status":"verified","subcategory":{"code":"evidence_capture","zh":"网络证据缺失","en":"Missing network evidence"},"reason":{"zh":"页面显示已订阅且 HAR 有 subscribe.json 200 POST,但 Moli 记录缺少 Referer 和响应 JSON,evaluator 无法确认 subscribed=true。","en":"The page shows subscribed and the HAR has a 200 subscribe.json POST, but Moli lacks Referer and response JSON, so the evaluator cannot confirm subscribed=true."}}, + "gpt-5.6-sol-chromium-600": {"status":"verified","subcategory":{"code":"benchmark_route_contract","zh":"路由验收缺陷","en":"Route-acceptance defect"},"reason":{"zh":"合法的 /submit/gaming 302 POST 含正确标题和论坛 ID 10039;evaluator 只接受精确 /submit。","en":"The valid /submit/gaming 302 POST contains the correct title and forum ID 10039; the evaluator accepts only exact /submit."}}, + "gpt-5.6-sol-moli-600": {"status":"verified","subcategory":{"code":"benchmark_route_contract","zh":"路由验收缺陷","en":"Route-acceptance defect"},"reason":{"zh":"合法的 /submit/gaming 302 POST 含正确标题和论坛 ID 10039;evaluator 只接受精确 /submit。","en":"The valid /submit/gaming 302 POST contains the correct title and forum ID 10039; the evaluator accepts only exact /submit."}}, + "gpt-5.6-sol-moli-615": {"status":"verified","subcategory":{"code":"agent_field_omission","zh":"提交字段遗漏","en":"Submission-field omission"},"reason":{"zh":"发帖请求包含正确标题和 Connecticut 论坛 ID,但 submission[url] 为空,遗漏 Bald Eagle 图片 URL。","en":"The post has the correct title and Connecticut forum ID, but submission[url] is empty, omitting the Bald Eagle image URL."}}, + "gpt-5.6-sol-chromium-625": {"status":"verified","subcategory":{"code":"benchmark_route_contract","zh":"路由验收缺陷","en":"Route-acceptance defect"},"reason":{"zh":"合法的 /submit/technology 302 POST 含正确标题、正文和论坛 ID 10043;evaluator 只接受精确 /submit。","en":"The valid /submit/technology 302 POST contains the correct title, body, and forum ID 10043; the evaluator accepts only exact /submit."}}, + "gpt-5.6-sol-moli-625": {"status":"verified","subcategory":{"code":"benchmark_route_contract","zh":"路由验收缺陷","en":"Route-acceptance defect"},"reason":{"zh":"合法的 /submit/technology 302 POST 含正确标题、正文和论坛 ID 10043;evaluator 只接受精确 /submit。","en":"The valid /submit/technology 302 POST contains the correct title, body, and forum ID 10043; the evaluator accepts only exact /submit."}}, + "gpt-5.6-sol-chromium-676": {"status":"verified","subcategory":{"code":"benchmark_route_contract","zh":"路由验收缺陷","en":"Route-acceptance defect"},"reason":{"zh":"HAR 有完整 sales_order_grid 与 status=fraud 请求,Referer 为合法的 /sales/order/;官方规则把尾斜杠差异判为失败。","en":"The HAR has the complete sales_order_grid request with status=fraud and a valid /sales/order/ Referer; the official rule fails on the trailing slash."}}, + "gpt-5.6-sol-moli-676": {"status":"verified","subcategory":{"code":"evidence_capture","zh":"网络证据缺失","en":"Missing network evidence"},"reason":{"zh":"HAR 有完整 sales_order_grid 与 status=fraud 请求,但 Moli 未记录 Referer,evaluator 无法匹配。","en":"The HAR has the complete sales_order_grid request with status=fraud, but Moli omits its Referer, preventing evaluator matching."}}, + "gpt-5.6-sol-chromium-694": {"status":"verified","subcategory":{"code":"site_backend_failure","zh":"站点保存故障","en":"Site save failure"},"reason":{"zh":"Agent 两次创建商品均被 Magento 以 The stock item was unable to be saved 拒绝,目录仍为空。","en":"Magento rejected both product-creation attempts with The stock item was unable to be saved, leaving the catalog empty."}}, + "gpt-5.6-sol-moli-694": {"status":"verified","subcategory":{"code":"site_backend_failure","zh":"站点保存故障","en":"Site save failure"},"reason":{"zh":"Agent 两次创建商品均被 Magento 以 The stock item was unable to be saved 拒绝,商品未创建。","en":"Magento rejected both product-creation attempts with The stock item was unable to be saved, so the product was not created."}}, + "gpt-5.6-sol-chromium-699": {"status":"verified","subcategory":{"code":"benchmark_evaluator_defect","zh":"evaluator 实现错误","en":"Evaluator implementation defect"},"reason":{"zh":"evaluator 在比较实际提交前即因单元素 Alternatives 配置抛出 Alternatives require 2+ items,零分来自官方规则错误。","en":"Before comparing the submission, the evaluator throws Alternatives require 2+ items for a one-item configuration; the zero comes from the official rule."}}, + "gpt-5.6-sol-moli-699": {"status":"verified","subcategory":{"code":"benchmark_evaluator_defect","zh":"evaluator 实现错误","en":"Evaluator implementation defect"},"reason":{"zh":"evaluator 在比较实际提交前即因单元素 Alternatives 配置抛出 Alternatives require 2+ items,零分来自官方规则错误。","en":"Before comparing the submission, the evaluator throws Alternatives require 2+ items for a one-item configuration; the zero comes from the official rule."}}, + "gpt-5.6-sol-chromium-771": {"status":"verified","subcategory":{"code":"agent_workflow_completeness","zh":"批量操作遗漏","en":"Incomplete batch workflow"},"reason":{"zh":"任务要求批准全部至少 4 星评论;Chromium 没有保存官方要求的评论 352 和 347,而 Moli 在相同条件下完成了两项。","en":"The task requires approving all reviews rated at least four stars; Chromium did not save required reviews 352 and 347, while Moli completed both under the same controls."}} + } + }, + "gpt-6-astra": { + "reviewed_at": "2026-09-11", + "reviewer_class": "evidence analyst", + "bindings": { + "attempts_sha256": "0659d227cf6d29fbfaf64d39f8e77a56bec73c87f7f6e1f2ac23ea13fd9b667c", + "manifest_sha256": "ea26ccbeb335126d91d8c2f9f229e445aca51137fbf31333d9b5db05f8508538", + "selection_sha256": "2cb716552e36df7535e83ed6d2cf3f84283e3aca86072817cd6e039a38547c7b" + }, + "review_protocol": { + "unit": "one score-bearing logical result per browser and task", + "method": "manual root-cause verification against the official task contract, score-bearing final response, evaluator assertions, network evidence, and autonomous Agent event trajectory", + "rule": "automatic evaluator classification remains available; root-cause subcategory and reason remain empty until status is verified" + }, + "coverage": {"failed_results": 38, "verified_results": 38}, + "unresolved": [], + "results": { + "gpt-6-astra-chromium-77": {"status":"verified","subcategory":{"code":"environment_concurrency_contamination","zh":"共享环境并发污染","en":"Shared-environment concurrency contamination"},"reason":{"zh":"准备完成后、Agent 读取前,共享 Shopping Admin 状态被并发执行改写:评论 352、347 从 Pending 变为 Approved,Pending 从合同基线 5 条降为 3 条。全新重建同一镜像可恢复 5 条,排除了镜像基线错误;Agent 对受污染页面的读取正确。","en":"After preparation and before the Agent read the page, a concurrent execution modified the shared Shopping Admin state: reviews 352 and 347 changed from Pending to Approved, reducing the contractual count from five to three. Recreating the same image restores five, ruling out a bad image baseline; the Agent correctly read the contaminated page."}}, + "gpt-6-astra-moli-77": {"status":"verified","subcategory":{"code":"environment_concurrency_contamination","zh":"共享环境并发污染","en":"Shared-environment concurrency contamination"},"reason":{"zh":"准备完成后、Agent 读取前,共享 Shopping Admin 状态被并发执行改写:评论 352、347 从 Pending 变为 Approved,Pending 从合同基线 5 条降为 3 条。全新重建同一镜像可恢复 5 条,排除了镜像基线错误;Agent 还通过显式筛选确认了受污染状态。","en":"After preparation and before the Agent read the page, a concurrent execution modified the shared Shopping Admin state: reviews 352 and 347 changed from Pending to Approved, reducing the contractual count from five to three. Recreating the same image restores five, ruling out a bad image baseline; the Agent also confirmed the contaminated state with an explicit filter."}}, + "gpt-6-astra-chromium-124": {"status":"verified","subcategory":{"code":"agent_retrieval_strategy","zh":"检索策略错误","en":"Retrieval-strategy error"},"reason":{"zh":"Agent 把 wireless earphones 放宽为 earphones,并把局部排序接口当作全集,返回 287.64,遗漏官方上界 298。","en":"The Agent broadened wireless earphones to earphones and treated a partial sorted endpoint as exhaustive, returning 287.64 instead of the official upper bound of 298."}}, + "gpt-6-astra-moli-124": {"status":"verified","subcategory":{"code":"agent_retrieval_strategy","zh":"检索策略错误","en":"Retrieval-strategy error"},"reason":{"zh":"Agent 把 wireless earphones 放宽为 earphones,并把局部排序接口当作全集,返回 287.64,遗漏官方上界 298。","en":"The Agent broadened wireless earphones to earphones and treated a partial sorted endpoint as exhaustive, returning 287.64 instead of the official upper bound of 298."}}, + "gpt-6-astra-chromium-163": {"status":"verified","subcategory":{"code":"agent_deduplication","zh":"结果去重遗漏","en":"Result-deduplication failure"},"reason":{"zh":"Magento 重叠分页重复出现同一条 Ernesto 20% 评论;Agent 没有按评论去重,最终重复输出。","en":"Overlapping Magento pages repeated the same Ernesto 20% review, and the Agent returned it twice instead of deduplicating reviews."}}, + "gpt-6-astra-moli-163": {"status":"verified","subcategory":{"code":"agent_deduplication","zh":"结果去重遗漏","en":"Result-deduplication failure"},"reason":{"zh":"Magento 重叠分页重复出现同一条 Ernesto 20% 评论;Agent 没有按评论去重,最终重复输出。","en":"Overlapping Magento pages repeated the same Ernesto 20% review, and the Agent returned it twice instead of deduplicating reviews."}}, + "gpt-6-astra-chromium-258": {"status":"verified","subcategory":{"code":"agent_navigation_semantics","zh":"导航语义错误","en":"Navigation-semantics error"},"reason":{"zh":"Agent 停在 GitLab Explore 总页 /explore,没有启用 Public 过滤参数 visibility_level=20。","en":"The Agent stopped at generic /explore without applying the Public filter visibility_level=20."}}, + "gpt-6-astra-moli-258": {"status":"verified","subcategory":{"code":"agent_navigation_semantics","zh":"导航语义错误","en":"Navigation-semantics error"},"reason":{"zh":"Agent 停在 GitLab Explore 总页 /explore,没有启用 Public 过滤参数 visibility_level=20。","en":"The Agent stopped at generic /explore without applying the Public filter visibility_level=20."}}, + "gpt-6-astra-chromium-279": {"status":"verified","subcategory":{"code":"mixed_agent_filtering_reference_state","zh":"检索遗漏与参考状态差异","en":"Retrieval omission and reference-state drift"},"reason":{"zh":"Agent 已抓取 H900N 却漏掉最终输出;同时当前 C300 名称比冻结参考多一个右括号,Agent 遗漏与参考状态漂移共同造成不匹配。","en":"The Agent fetched H900N but omitted it from the final output; the live C300 name also has an extra closing parenthesis relative to the frozen reference, so omission and reference-state drift jointly caused the mismatch."}}, + "gpt-6-astra-moli-279": {"status":"verified","subcategory":{"code":"mixed_agent_filtering_reference_state","zh":"检索遗漏与参考状态差异","en":"Retrieval omission and reference-state drift"},"reason":{"zh":"Agent 已抓取 H900N 却漏掉最终输出;同时当前 C300 名称比冻结参考多一个右括号,Agent 遗漏与参考状态漂移共同造成不匹配。","en":"The Agent fetched H900N but omitted it from the final output; the live C300 name also has an extra closing parenthesis relative to the frozen reference, so omission and reference-state drift jointly caused the mismatch."}}, + "gpt-6-astra-chromium-339": {"status":"verified","subcategory":{"code":"benchmark_route_contract","zh":"路由验收缺陷","en":"Route-acceptance defect"},"reason":{"zh":"Agent 已打开带 state=opened 与 bug 标签的合法 /issues?... 页面;官方规则只接受 /issues/.*,错误拒绝无尾斜杠路由。","en":"The Agent opened the valid /issues?... route with state=opened and the bug label, but the official /issues/.* rule incorrectly rejects the no-trailing-slash route."}}, + "gpt-6-astra-moli-339": {"status":"verified","subcategory":{"code":"benchmark_route_contract","zh":"路由验收缺陷","en":"Route-acceptance defect"},"reason":{"zh":"Agent 已打开正确筛选页;官方规则错误拒绝无尾斜杠 /issues?... 路由,且 Moli 记录还缺少 Referer。","en":"The Agent opened the correct filtered page; the official rule rejects the valid no-trailing-slash /issues?... route, and Moli's record also omits the Referer."}}, + "gpt-6-astra-chromium-448": {"status":"verified","subcategory":{"code":"evidence_capture","zh":"网络证据匹配缺陷","en":"Network-evidence matching defect"},"reason":{"zh":"个人资料更新已成功,HAR 含字段正确的 302 请求和重复的 200 记录;evaluator 选中后者而误判。","en":"The profile update succeeded. HAR contains the correct 302 request and a duplicate 200 record, but the evaluator selected the latter and produced a false negative."}}, + "gpt-6-astra-moli-448": {"status":"verified","subcategory":{"code":"evidence_capture","zh":"网络证据匹配缺陷","en":"Network-evidence matching defect"},"reason":{"zh":"个人资料更新已成功,HAR 含字段正确的 302 请求和重复的 200 记录;evaluator 选中后者而误判。","en":"The profile update succeeded. HAR contains the correct 302 request and a duplicate 200 record, but the evaluator selected the latter and produced a false negative."}}, + "gpt-6-astra-chromium-464": {"status":"verified","subcategory":{"code":"agent_field_omission","zh":"提交字段错误","en":"Submission-field error"},"reason":{"zh":"Agent 正确算出 1,却写入 product[description];官方要求的 product[short_description] 为空。后续库存保存错误不改变该独立失败原因。","en":"The Agent correctly derived 1 but wrote it to product[description], leaving the required product[short_description] empty. A later stock-save error does not change this independent failure."}}, + "gpt-6-astra-moli-464": {"status":"verified","subcategory":{"code":"agent_field_omission","zh":"提交字段错误","en":"Submission-field error"},"reason":{"zh":"Agent 正确算出 1,却写入 product[description];官方要求的 product[short_description] 为空。后续库存保存错误不改变该独立失败原因。","en":"The Agent correctly derived 1 but wrote it to product[description], leaving the required product[short_description] empty. A later stock-save error does not change this independent failure."}}, + "gpt-6-astra-chromium-470": {"status":"verified","subcategory":{"code":"site_dependency_failure","zh":"站点依赖故障","en":"Site dependency failure"},"reason":{"zh":"Agent 已提交取消请求;Magento 随后报告搜索集群无可用节点,订单仍为 Pending。","en":"The Agent submitted the cancellation, but Magento then reported no live search-cluster nodes and the order remained Pending."}}, + "gpt-6-astra-moli-470": {"status":"verified","subcategory":{"code":"site_dependency_failure","zh":"站点依赖故障","en":"Site dependency failure"},"reason":{"zh":"Agent 已提交取消请求;Magento 随后报告搜索集群无可用节点,订单仍为 Pending。","en":"The Agent submitted the cancellation, but Magento then reported no live search-cluster nodes and the order remained Pending."}}, + "gpt-6-astra-chromium-475": {"status":"verified","subcategory":{"code":"benchmark_form_contract","zh":"表单验收缺陷","en":"Form-acceptance defect"},"reason":{"zh":"空项目已按正确名称、路径和命名空间创建;未勾选的 HTML checkbox 按标准行为不提交字段,官方规则却要求 initialize_with_readme=0。","en":"The empty project was created correctly. Standard HTML omits an unchecked checkbox, while the official rule requires initialize_with_readme=0."}}, + "gpt-6-astra-moli-475": {"status":"verified","subcategory":{"code":"benchmark_form_contract","zh":"表单验收缺陷","en":"Form-acceptance defect"},"reason":{"zh":"空项目已按正确名称、路径和命名空间创建;未勾选的 HTML checkbox 按标准行为不提交字段,官方规则却要求 initialize_with_readme=0。","en":"The empty project was created correctly. Standard HTML omits an unchecked checkbox, while the official rule requires initialize_with_readme=0."}}, + "gpt-6-astra-moli-506": {"status":"verified","subcategory":{"code":"evidence_capture","zh":"响应证据缺失","en":"Missing response evidence"},"reason":{"zh":"Agent 清空旧购物车、加入正确商品 22490 并付款到成功页;Moli 把 /carts/mine/totals 响应正文记录为空,evaluator 无法确认商品。","en":"The Agent cleared the cart, added the correct product 22490, and completed payment. Moli recorded an empty /carts/mine/totals response body, preventing evaluator confirmation."}}, + "gpt-6-astra-chromium-510": {"status":"verified","subcategory":{"code":"agent_product_selection","zh":"商品选择错误","en":"Incorrect product selection"},"reason":{"zh":"Agent 完成结账,但购买商品 101685 Hiquphon Tweeters,而非合同要求的 75640 Atlantic Technology FS-7.0-GLB。","en":"The Agent completed checkout with product 101685, Hiquphon Tweeters, instead of required product 75640, Atlantic Technology FS-7.0-GLB."}}, + "gpt-6-astra-moli-510": {"status":"verified","subcategory":{"code":"agent_product_selection","zh":"商品选择错误","en":"Incorrect product selection"},"reason":{"zh":"Agent 完成结账,但购买商品 101685 Hiquphon Tweeters,而非合同要求的 75640 Atlantic Technology FS-7.0-GLB。","en":"The Agent completed checkout with product 101685, Hiquphon Tweeters, instead of required product 75640, Atlantic Technology FS-7.0-GLB."}}, + "gpt-6-astra-chromium-528": {"status":"verified","subcategory":{"code":"benchmark_instruction_contract","zh":"任务与验收矛盾","en":"Instruction-evaluator contradiction"},"reason":{"zh":"Agent 按要求填写退款表单并停在提交前;官方 evaluator 却要求出现 dummy_bin POST,与“不要提交”直接冲突。","en":"The Agent filled the refund form and stopped before submission, but the evaluator requires a dummy_bin POST, directly contradicting the instruction not to submit."}}, + "gpt-6-astra-moli-528": {"status":"verified","subcategory":{"code":"benchmark_instruction_contract","zh":"任务与验收矛盾","en":"Instruction-evaluator contradiction"},"reason":{"zh":"Agent 按要求填写退款表单并停在提交前;官方 evaluator 却要求出现 dummy_bin POST,与“不要提交”直接冲突。","en":"The Agent filled the refund form and stopped before submission, but the evaluator requires a dummy_bin POST, directly contradicting the instruction not to submit."}}, + "gpt-6-astra-chromium-547": {"status":"verified","subcategory":{"code":"agent_workflow_completeness","zh":"流程未完成","en":"Incomplete workflow"},"reason":{"zh":"Agent 仅执行 10 个决策回合,在打开配置向导后主动结束;没有新增 Brown 色板,也没有必要保存请求。","en":"The Agent voluntarily ended after 10 decision turns shortly after opening the configuration wizard; it created no Brown swatch and issued no required save request."}}, + "gpt-6-astra-moli-547": {"status":"verified","subcategory":{"code":"agent_workflow_completeness","zh":"流程未完成","en":"Incomplete workflow"},"reason":{"zh":"Agent 仅执行 10 个决策回合,在商品列表阶段主动结束;没有进入完整编辑与保存流程。","en":"The Agent voluntarily ended after 10 decision turns at the product-list stage without completing the edit and save workflow."}}, + "gpt-6-astra-chromium-585": {"status":"verified","subcategory":{"code":"agent_workflow_completeness","zh":"可恢复错误后提前终止","en":"Premature stop after recoverable error"},"reason":{"zh":"进入订单历史时出现可恢复的 snapshot 导航竞态;HAR 显示页面已返回 200,但 Agent 直接终止,未继续定位落地灯或提交评论。","en":"A recoverable snapshot/navigation race occurred while opening order history. HAR shows the page returned 200, but the Agent stopped without locating the floor lamp or submitting a review."}}, + "gpt-6-astra-moli-595": {"status":"verified","subcategory":{"code":"evidence_capture","zh":"网络证据缺失","en":"Missing network evidence"},"reason":{"zh":"订阅实际成功,页面变为 Unsubscribe,trace 响应为 subscribed=true;Moli HAR 缺 Referer 和响应 JSON,evaluator 因而无法匹配。","en":"The subscription succeeded: the UI changed to Unsubscribe and trace returned subscribed=true. Moli HAR omitted the Referer and response JSON, preventing evaluator matching."}}, + "gpt-6-astra-chromium-605": {"status":"verified","subcategory":{"code":"site_dependency_failure","zh":"站点会话故障","en":"Site session failure"},"reason":{"zh":"Agent 提交了正确论坛、标题和与新表单一致的 CSRF token;站点连续两次仍返回 CSRF token is invalid。","en":"The Agent submitted the correct forum, title, and CSRF token from the freshly loaded form, yet the site rejected both executions with CSRF token is invalid."}}, + "gpt-6-astra-chromium-676": {"status":"verified","subcategory":{"code":"benchmark_route_contract","zh":"路由验收缺陷","en":"Route-acceptance defect"},"reason":{"zh":"Agent 正确筛选 status=fraud,页面显示 Suspected Fraud;官方规则仅因 Referer 带 /sales/order/ 尾斜杠而拒绝。","en":"The Agent correctly filtered status=fraud and the UI showed Suspected Fraud; the official rule rejected the request only because the Referer used trailing-slash /sales/order/."}}, + "gpt-6-astra-moli-676": {"status":"verified","subcategory":{"code":"evidence_capture","zh":"网络证据缺失","en":"Missing network evidence"},"reason":{"zh":"Agent 发出完整的欺诈订单筛选请求且页面显示 Suspected Fraud;Moli 未记录该 AJAX 请求的 Referer,evaluator 无法匹配。","en":"The Agent issued the complete fraudulent-order filter request and the UI showed Suspected Fraud, but Moli omitted the AJAX Referer, preventing evaluator matching."}}, + "gpt-6-astra-chromium-694": {"status":"verified","subcategory":{"code":"site_backend_failure","zh":"站点保存故障","en":"Site save failure"},"reason":{"zh":"合同字段均正确且网络验收通过;Magento 随后返回 The stock item was unable to be saved,商品未创建。","en":"All contract fields were correct and network acceptance passed, but Magento returned The stock item was unable to be saved and did not create the product."}}, + "gpt-6-astra-moli-694": {"status":"verified","subcategory":{"code":"site_backend_failure","zh":"站点保存故障","en":"Site save failure"},"reason":{"zh":"合同字段和 Top attribute set 均正确且网络验收通过;Magento 返回库存项保存失败,商品未创建。","en":"The contract fields and Top attribute set were correct and network acceptance passed, but Magento returned a stock-item save failure and did not create the product."}}, + "gpt-6-astra-chromium-699": {"status":"verified","subcategory":{"code":"benchmark_evaluator_defect","zh":"evaluator 实现错误","en":"Evaluator implementation defect"},"reason":{"zh":"规则已按要求保存;evaluator 在比较实际请求前因单元素 Alternatives 配置抛出 Alternatives require 2+ items。","en":"The rule was saved as required, but the evaluator threw Alternatives require 2+ items for a one-item configuration before comparing the request."}}, + "gpt-6-astra-moli-699": {"status":"verified","subcategory":{"code":"benchmark_evaluator_defect","zh":"evaluator 实现错误","en":"Evaluator implementation defect"},"reason":{"zh":"规则已按要求保存并应用;零分来自 evaluator 的单元素 Alternatives 异常。","en":"The rule was saved and applied as required; the zero comes from the evaluator's one-item Alternatives exception."}}, + "gpt-6-astra-chromium-771": {"status":"verified","subcategory":{"code":"benchmark_instruction_contract","zh":"任务状态与验收矛盾","en":"State-evaluator contradiction"},"reason":{"zh":"最终执行开始时评论 352、347 已是 Approved,任务语义已经满足;evaluator 仍强制要求重复提交 status_id=1。","en":"Reviews 352 and 347 were already Approved at the start of the final execution, so the task was already satisfied, while the evaluator still required redundant status_id=1 submissions."}}, + "gpt-6-astra-moli-771": {"status":"verified","subcategory":{"code":"benchmark_instruction_contract","zh":"任务状态与验收矛盾","en":"State-evaluator contradiction"},"reason":{"zh":"最终执行开始时评论 352、347 已是 Approved,任务语义已经满足;evaluator 仍强制要求重复提交 status_id=1。","en":"Reviews 352 and 347 were already Approved at the start of the final execution, so the task was already satisfied, while the evaluator still required redundant status_id=1 submissions."}} + } + } } } diff --git a/research/report-changes.json b/research/report-changes.json index 9026aee24..ae545cd02 100644 --- a/research/report-changes.json +++ b/research/report-changes.json @@ -1,7 +1,28 @@ { - "schema_version": 1, + "schema_version": 2, "history_scope": "本页从有完整审计依据的修订开始记录。更早的站点版本仍保留在 Artifact Hub,但没有逐项变更说明,故不补编历史。", + "history_scope_i18n": { + "en": "This history begins with revisions supported by a complete audit trail. Earlier Artifact Hub versions remain available, but no itemized history is reconstructed where the supporting record is incomplete." + }, "browser_use_reviews": [ + { + "id": "review-2026-09-11-astra-layer-and-environment-isolation", + "date": "2026-09-11", + "title": "Astra 模型层、逐执行证据与环境隔离核验", + "status": "已处理", + "scope": "GPT-5.6 Sol 与 GPT-6 Astra 两个模型层、360 个逻辑结果、438 次物理执行、75 个最终失败,以及共享 WebArena 站点栈的执行边界。", + "findings": "Sol 层为 Chromium 73/90、Moli 70/90;Astra 层两者均为 71/90。Astra Task 77 的两个失败来自共享 Shopping Admin 状态在准备后被并发执行改写,不是 Agent 误读或镜像基线错误。两个模型层使用的 browser-eval 提交不同,因此跨模型变化只能作观察性比较。", + "corrections": "所有模型、浏览器和任务仍只保留一个逻辑结果;公开包补齐每次物理执行的脱敏证据与哈希;75/75 个失败均绑定当前证据并核验根因。运行器现在用跨进程文件锁覆盖 reset、Agent、证据收尾和 evaluator 的完整执行生命周期。", + "remaining": "历史官方分数不以人工核验覆盖;Task 77 只有在修复后的隔离环境中形成新的完整证据批次后才能更新成绩。真正并行执行仍要求每个 worker 拥有独立的完整站点栈、端口和浏览器容器。", + "i18n": {"en": { + "title": "Astra model layer, per-execution evidence, and environment-isolation review", + "status": "Resolved", + "scope": "The GPT-5.6 Sol and GPT-6 Astra model layers, 360 logical results, 438 physical executions, 75 final failures, and the execution boundary of the shared WebArena site stack.", + "findings": "In the Sol layer Chromium scored 73/90 and Moli 70/90; in the Astra layer both scored 71/90. The two Astra Task 77 failures came from concurrent modification of shared Shopping Admin state after preparation, not an Agent reading error or a bad image baseline. The model layers used different browser-eval commits, so cross-model changes are observational only.", + "corrections": "Kept one logical result per model, browser, and task; published sanitized evidence and hashes for every physical execution; and bound all 75 failures to verified root causes. The runner now holds a cross-process lock across reset, Agent execution, evidence finalization, and evaluation.", + "remaining": "Human review does not override historical official scores. Task 77 can change only after a new complete evidence cohort is produced under the corrected isolation protocol. Safe parallel execution still requires a separate full site stack, ports, and browser containers for every worker." + }} + }, { "id": "review-2026-09-11-90-task-expansion", "date": "2026-09-11", @@ -10,7 +31,15 @@ "scope": "90 个预注册任务、180 个浏览器逻辑结果、220 次物理执行、环境准备回执、官方判分和全部最终失败结果。", "findings": "Chromium 通过 73/90,Moli 通过 70/90;69 题共同通过、16 题共同失败、5 题结果不一致。37 个最终失败覆盖 Agent 检索与执行、站点依赖、官方验收合同及 Moli 网络证据采集四类责任层,不能用“模型能力不行”统一解释。", "corrections": "逐结果绑定官方合同、最终回答、HAR 和 Agent 轨迹,完成 37/37 根因核验;报告改用 90 题动态口径并披露 220/220 环境准备回执。运行器对 Agent 启动前的瞬态 BrowserContextNotLoaded 增加一次仅限会话准备的恢复,不改变逻辑结果协议。", - "remaining": "Astra 未由当前 OpenAI 代理暴露,本轮保持 gpt-5.6-sol、reasoning=low。Wikipedia 与 Map 尚未进入实跑;官方 evaluator 的已核验缺陷需上游修复后以新证据复测。" + "remaining": "Astra 未由当前 OpenAI 代理暴露,本轮保持 gpt-5.6-sol、reasoning=low。Wikipedia 与 Map 尚未进入实跑;官方 evaluator 的已核验缺陷需上游修复后以新证据复测。", + "i18n": {"en": { + "title": "90-task expansion and failure-cause review", + "status": "Resolved", + "scope": "The 90 preregistered tasks, 180 logical browser results, 220 physical executions, environment-preparation receipts, official scoring, and every final failure.", + "findings": "Chromium passed 73/90 and Moli passed 70/90. The paired results contain 69 joint passes, 16 joint failures, and five disagreements. The 37 final failures span agent retrieval or execution, site dependencies, official acceptance contracts, and Moli network-evidence collection; they do not support one blanket model-capability explanation.", + "corrections": "Bound each failed result to its official contract, final answer, HAR, and agent trajectory and completed 37/37 root-cause reviews. Updated the report to the 90-task cohort and disclosed 220/220 environment-preparation receipts. Session preparation now recovers once from a transient BrowserContextNotLoaded before the agent starts, without changing logical-result authority.", + "remaining": "This run remains on gpt-5.6-sol with reasoning=low because Astra was unavailable through the current model endpoint. Wikipedia and Map are not measured. Verified defects in the official evaluator require upstream correction and new execution evidence." + }} }, { "id": "review-2026-09-10-shared-failure-and-expansion", @@ -20,7 +49,15 @@ "scope": "十个失败逻辑结果的自动定位、逐结果根因核验、修复代码、结果详情信息层级、Colima 网络身份和 Mac 环境扩展范围。", "findings": "相同官方分数掩盖了不同根因:Task 68 的官方 expected 与 schema 冲突;124 缺少候选语义核验;142 的类别验收边界未定义;168 的本项目响应协议与官方 null 合同冲突;506 中 Chromium 为 HAR 采集误判,Moli 为步数耗尽且未确认完成。", "corrections": "保留 evaluator 的程序自动定位;新增按浏览器与任务记录、绑定 attempts 哈希的根因子类和原因核验,未核验字段保持为空,正式发布要求全部失败结果完成核验。另新增运行前合同一致性校验、候选语义与提交前检查、空结果状态协议、JSON 响应体即时 HAR 采集、末步工具调用 fail-closed 和可配置步数上限。历史分数不改写。", - "remaining": "修复后的 #124、#168、#506 尚需形成新执行证据;#68 与 #142 退出下一轮干净 A/B,直至合同被修复或验收口径可观察。Wikipedia 与 Map 尚未在本机实跑。" + "remaining": "修复后的 #124、#168、#506 尚需形成新执行证据;#68 与 #142 退出下一轮干净 A/B,直至合同被修复或验收口径可观察。Wikipedia 与 Map 尚未在本机实跑。", + "i18n": {"en": { + "title": "Failure causes, corrections, and local-environment expansion", + "status": "Resolved", + "scope": "Automatic failure location, evidence-bound root-cause review, runtime corrections, result-page hierarchy, Colima network identity, and the locally runnable environment set.", + "findings": "The ten failed logical results span task-contract defects, agent protocol or policy defects, and evidence-collection defects. Equal official scores can therefore have different causes, including different causes for the two browsers on the same task.", + "corrections": "Kept automatic evaluator location separate from reviewed causes; added per-browser, per-task cause records bound to the attempts hash and required complete failure coverage for release. Added contract admission checks, candidate and pre-submit validation, an explicit empty-result protocol, immediate JSON HAR-body capture, fail-closed final-step handling, and a configurable step ceiling. Historical scores remain unchanged.", + "remaining": "Tasks affected by runtime corrections require new execution evidence. Tasks with defective or unobservable contracts remain outside the next clean A/B until those contracts are corrected. Wikipedia and Map remain unmeasured locally." + }} }, { "id": "review-2026-09-10-track-naming-and-release-gates", @@ -30,7 +67,15 @@ "scope": "自主评测与固定轨迹评测的命名、优胜标记、英文界面属性、筛选状态和视觉发布门禁。", "findings": "轨道名称按实现层命名,读者不易判断测量对象;并列最优和显示值相同的指标仍可能被标优;英文可访问属性与失效筛选链接缺少回归保护;SVG 和跨断点布局只受人工规范约束。", "corrections": "统一命名为 Browser Use 与 Browser Trajectory Replay(轨迹重放),完成代码与命令迁移且不保留旧入口;优胜只允许显示值唯一领先;补充英文属性翻译、失效链接回退、SVG 拒绝和核心路由双语双主题宽窄屏渲染检查。", - "remaining": "视觉门禁验证结构性溢出、裁切和表头竖排,仍不能替代人工对信息层级与审美质量的判断。" + "remaining": "视觉门禁验证结构性溢出、裁切和表头竖排,仍不能替代人工对信息层级与审美质量的判断。", + "i18n": {"en": { + "title": "Track naming and release-gate review", + "status": "Resolved", + "scope": "Names for autonomous and fixed-trajectory evaluation, winner marks, English accessibility text, filter state, and visual release gates.", + "findings": "Implementation-layer names obscured what each track measures; ties or display-equal metrics could be marked as winners; English accessibility attributes and invalid filter links lacked regression coverage; and responsive layout depended on manual checks.", + "corrections": "Standardized the names Browser Use and Browser Trajectory Replay, marked only a unique displayed winner, localized English accessibility attributes, restored invalid filters to valid defaults, rejected SVG assets, and added bilingual, two-theme desktop and narrow-screen checks for core routes.", + "remaining": "Automated visual gates detect structural overflow, clipping, and vertical headers, but do not replace human review of information hierarchy and visual quality." + }} }, { "id": "review-2026-09-10-scalable-comparison-ui", @@ -40,7 +85,14 @@ "scope": "同题比较交互、BrowserTool 动作延迟和单结果详情首屏。", "findings": "同题视图虽动态生成列,但交互仍以两浏览器关系为中心;动作表层级拥挤且对不同调用量的总耗时判优;完整 evaluator 错误被放入窄指标卡。", "corrections": "同题比较改为基准浏览器加多选对比浏览器和明确关系条件;动作延迟改为按 action 分卡并只比较单次均值;详情首屏只保留简明规则定位,完整 expected、actual 和错误进入规则证据区。", - "remaining": "当前仅有两个参评浏览器;扩展行为已由三浏览器测试模型覆盖,新增真实浏览器仍需形成完整同题结果。" + "remaining": "当前仅有两个参评浏览器;扩展行为已由三浏览器测试模型覆盖,新增真实浏览器仍需形成完整同题结果。", + "i18n": {"en": { + "title": "Multi-browser comparison and result-detail review", "status": "Resolved", + "scope": "Same-task comparison controls, BrowserTool action latency, and the opening section of result-detail pages.", + "findings": "The same-task view remained centered on a two-browser relationship; action tables ranked totals across unequal call counts; and full evaluator errors occupied narrow KPI cards.", + "corrections": "Introduced one baseline with multi-select comparison browsers and explicit outcome relations, compared action latency only by mean per call, and moved complete expected, actual, and error evidence out of the result summary.", + "remaining": "Only two browsers are measured. Three-browser fixtures cover interface scaling, but every new real browser still requires a complete same-task result set." + }} }, { "id": "review-2026-09-10-report-model", @@ -50,7 +102,14 @@ "scope": "统计摘要、优胜表达、结果下钻、核验记录和参评浏览器说明。", "findings": "统计标签重复表达 P50;比较表缺少方向一致的优胜标记;结果页存在重复失败清单;核验页承担了逐结果清单职责。", "corrections": "统一为均值、P50、P95;按指标方向突出唯一优胜者;删除重复失败清单;核验页改为按轮次记录范围、发现、处理和遗留事项。", - "remaining": "当前固定样本没有浏览器单边通过任务;优胜标记主要反映耗时与 Tokens,不改变官方任务成绩。" + "remaining": "当前固定样本没有浏览器单边通过任务;优胜标记主要反映耗时与 Tokens,不改变官方任务成绩。", + "i18n": {"en": { + "title": "Report model and comparison review", "status": "Resolved", + "scope": "Summary statistics, winner presentation, result drill-down, review history, and browser-participant descriptions.", + "findings": "P50 was duplicated under another label, comparison tables lacked directionally consistent winner marks, the results page duplicated failure inventory, and the review page carried a per-result list.", + "corrections": "Standardized summaries on mean, P50, and P95; marked only unique directionally valid winners; removed the duplicate failure list; and made review history round-based.", + "remaining": "This fixed cohort has no one-browser-only pass. Winner marks mainly describe latency and token usage and do not alter official task scores." + }} }, { "id": "review-2026-09-10-evidence-boundaries", @@ -60,7 +119,14 @@ "scope": "本地站点部署、Colima 网络路径、环境准备证据、Agent 与 BrowserTool 计时。", "findings": "环境拓扑与网关含义缺少集中说明;历史总耗时边界描述不准确;逐执行环境准备没有独立回执。", "corrections": "补充部署流程图、端口和镜像身份;区分 Agent 链路与 BrowserTool 墙钟时间;新版 runner 保存环境准备和分阶段计时。", - "remaining": "本批冻结结果不具备逐执行准备回执,报告仅按现有证据声明可复现边界。" + "remaining": "本批冻结结果不具备逐执行准备回执,报告仅按现有证据声明可复现边界。", + "i18n": {"en": { + "title": "Deployment evidence and timing-boundary review", "status": "Resolved", + "scope": "Local-site deployment, the Colima network path, environment-preparation evidence, and Agent and BrowserTool timing.", + "findings": "The topology and gateway lacked a consolidated explanation, historical total-latency wording overstated its boundary, and per-execution preparation lacked an independent receipt.", + "corrections": "Added the deployment flow, ports, and image identities; separated agent-stack and BrowserTool wall time; and made the current runner retain environment-preparation and phase timings.", + "remaining": "This frozen run lacks per-execution preparation receipts, so reproducibility claims remain limited to the evidence actually retained." + }} }, { "id": "review-2026-09-09-runner-integrity", @@ -70,10 +136,96 @@ "scope": "模型响应校验、BrowserTool 清理、HAR 留存、证据目录和恢复执行。", "findings": "异常清理可能遮蔽原始错误;HAR 停止操作缺少幂等保护;异常响应和原始证据目录需要更严格保护。", "corrections": "隔离清理异常、使 HAR 停止幂等、加强模型响应结构校验,并阻止原始浏览器证据进入 Git。", - "remaining": "无阻塞项。" + "remaining": "无阻塞项。", + "i18n": {"en": { + "title": "Runner and evidence-integrity review", "status": "Resolved", + "scope": "Model-response validation, BrowserTool cleanup, HAR retention, evidence directories, and resume behavior.", + "findings": "Cleanup exceptions could mask primary failures, HAR shutdown lacked idempotency, and malformed responses and raw-evidence directories required stronger protection.", + "corrections": "Isolated cleanup failures, made HAR shutdown idempotent, strengthened model-response validation, and prevented raw browser evidence from entering Git.", + "remaining": "No blocking items." + }} } ], "browser_use_releases": [ + { + "id": "2026-09-11-astra-two-model-browser-comparison", + "date": "2026-09-11", + "title": "加入 GPT-6 Astra 模型层并完成双模型浏览器对照", + "summary": "同一 90 题样本按模型分层比较 Chromium 与 Moli:GPT-5.6 Sol 层为 73/90 对 70/90,GPT-6 Astra 层为 71/90 对 71/90。75 个最终失败全部完成证据绑定的根因核验;已确认的环境污染不人工改分。", + "attempts_sha256_by_model": { + "gpt-5.6-sol": "73146500152db62d79f84b63c85a14027c005a6320c19bce7426651f167466bd", + "gpt-6-astra": "0659d227cf6d29fbfaf64d39f8e77a56bec73c87f7f6e1f2ac23ea13fd9b667c" + }, + "selection_sha256": "2cb716552e36df7535e83ed6d2cf3f84283e3aca86072817cd6e039a38547c7b", + "changes": [ + [ + "双模型分层", + "上一版只包含 GPT-5.6 Sol,无法观察更换模型后浏览器差异是否稳定。", + "加入 GPT-6 Astra low 的独立证据层;模型之间不混排,浏览器只在各自模型层内做同题配对。", + "Astra 层 Chromium 与 Moli 均为 71/90;两端各有 2 个单边通过,精确 McNemar p=1.0,未观察到显著通过率差异。" + ], + [ + "跨模型解释边界", + "两个模型层并非同一次运行,且 browser-eval 提交不同。", + "报告保留各模型层的运行 ID、提交、manifest 与 attempts 哈希,并把跨模型差异标为观察性结果。", + "不能把 Sol 与 Astra 的分数或耗时差异单独归因于模型版本。" + ], + [ + "失败根因", + "官方 evaluator 的失败信息只说明未满足哪条规则。", + "对 75 个最终失败逐项核对合同、最终回答、网络证据和自主轨迹;自动分类保留,核验前的详细根因为空。", + "75/75 已核验、0 项未决;发现 Agent、站点、环境隔离、evaluator 合同和浏览器证据采集等不同责任层。" + ], + [ + "环境隔离与并行", + "固定端口的共享站点栈允许另一个进程在 reset 后、Agent 读取前改写状态。", + "增加跨进程锁,覆盖 reset、浏览器会话、CowAgent、证据收尾与 evaluator;只有独立完整站点栈才允许并行。", + "该修复保护后续运行;历史 Task 77 官方分数保持原样,必须由新证据批次才能更正。" + ], + [ + "公开证据", + "逻辑结果累计了所有实际执行的耗时和成本,但旧页面只暴露判分执行的证据。", + "仍按模型×浏览器×任务展示一个逻辑结果,同时为 438 次物理执行分别发布脱敏回答、evaluator、事件、轨迹、指标与哈希。", + "读者可重算累计值;HAR、Playwright trace、Cookie 和凭据继续不公开。" + ] + ], + "i18n": {"en": { + "title": "Add GPT-6 Astra and complete a two-model browser comparison", + "summary": "Chromium and Moli are compared within two model layers on the same 90 tasks: 73/90 versus 70/90 with GPT-5.6 Sol, and 71/90 versus 71/90 with GPT-6 Astra. All 75 final failures have evidence-bound root-cause reviews; verified environment contamination does not manually change official scores.", + "changes": [ + [ + "Two model layers", + "The previous release contained only GPT-5.6 Sol, so it could not show whether browser differences persisted after changing the model.", + "Added an independent GPT-6 Astra low evidence layer. Models are not pooled; browsers are paired task by task only within each model layer.", + "In the Astra layer Chromium and Moli both scored 71/90, with two browser-only passes on each side and exact McNemar p=1.0; no significant pass-rate difference was observed." + ], + [ + "Cross-model boundary", + "The model layers were separate runs and used different browser-eval commits.", + "The report retains each layer's run ID, commit, manifest, and attempts hashes and labels cross-model differences as observational.", + "Sol–Astra score or latency changes cannot be attributed to the model version alone." + ], + [ + "Failure causes", + "Official evaluator messages identify unmet rules but not the responsible layer.", + "Reviewed the contract, final answer, network evidence, and autonomous trajectory for all 75 failed logical results. Automatic classification remains available, while detailed causes stay blank until verified.", + "All 75 failures are reviewed with none unresolved, spanning Agent, site, environment isolation, evaluator contracts, and browser evidence capture." + ], + [ + "Environment isolation and parallelism", + "The shared fixed-port site stack allowed another process to mutate state after reset and before the Agent observed it.", + "Added a cross-process lock across reset, browser session, CowAgent, evidence finalization, and evaluation. Parallel execution is allowed only with separate full site stacks.", + "The fix protects future runs. Historical Task 77 scores remain unchanged until replaced by a new evidence cohort." + ], + [ + "Public evidence", + "Logical results accumulated every execution's latency and cost, but the previous page exposed evidence only for the score-authoritative execution.", + "The report still shows one logical result per model, browser, and task, while publishing sanitized responses, evaluator output, events, trajectories, metrics, and hashes for all 438 physical executions.", + "Readers can recompute aggregates; HAR, Playwright trace, cookies, and credentials remain private." + ] + ] + }} + }, { "id": "2026-09-11-90-task-browser-use-comparison", "date": "2026-09-11", @@ -106,7 +258,17 @@ "本批 220/220 次物理执行均保存 reset、ready、应用健康检查和登录准备状态,并继续披露 Colima 网关与本地镜像拓扑。", "支持确认本批确在自托管站点执行;不代表 Wikipedia 与 Map 已部署。" ] - ] + ], + "i18n": {"en": { + "title": "Expand Browser Use to 90 tasks", + "summary": "The preregistered cohort now contains 90 tasks: 15 easy, 30 medium, and 45 hard. Chromium passed 73 and Moli passed 70. All 37 final failures have evidence-bound root-cause reviews; human review does not override official scores.", + "changes": [ + ["Evaluation scope", "The previous 30-task cohort did not provide enough observations across short, medium, and long workflows.", "Expanded to 90 outcome-blind tasks, preregistered by minimum visible UI actions as 15 easy, 30 medium, and 45 hard across Shopping, Shopping Admin, Reddit, and GitLab.", "The denominator increased from 30 to 90. Tiers describe project-defined operation length, not official difficulty."], + ["Official results", "The previous cohort gave Chromium and Moli 25/30 each.", "This cohort gives Chromium 73/90 and Moli 70/90: 69 joint passes, 16 joint failures, four Chromium-only passes, and one Moli-only pass.", "Results apply only to the current CowAgent, gpt-5.6-sol low, four local sites, and frozen run protocol; they do not generalize to the full benchmark."], + ["Failure causes", "The programmatic evaluator locates failed rules but cannot by itself identify the responsible layer.", "Reviewed the contract, answer, network evidence, and autonomous trajectory for all 37 failed logical results and recorded a specific cause subtype and rationale.", "All 37 failures are reviewed with none unresolved; human review does not change official scores."], + ["Environment evidence", "The prior cohort lacked preparation receipts bound to each execution.", "All 220 physical executions retain reset, readiness, application-health, and sign-in preparation status, alongside the Colima gateway and local image topology.", "This establishes that the cohort ran against self-hosted sites; it does not claim that Wikipedia or Map was deployed."] + ] + }} }, { "id": "2026-09-10-shared-failure-diagnosis-and-mac-expansion", @@ -152,7 +314,19 @@ "区分 VM 地址与宿主网关,说明四站实跑证据;纠正官方不存在 179 题子集,并给出 678 个当前可预检候选和 233 个 hard 候选。", "Wikipedia、Map 仍标为尚未实跑,不把理论可部署写成已验证。" ] - ] + ], + "i18n": {"en": { + "title": "Trace failures to causes and apply runtime corrections", + "summary": "Ten failed logical results were reviewed across task contracts, agent protocol or policy, and evidence collection. Automatic failure location and reviewed causes are presented separately and bound to frozen evidence. Runtime corrections await validation by new executions; historical official scores remain 25/30 for each browser.", + "changes": [ + ["Report interpretation", "Equal totals did not explain outcomes, and equal zero scores did not imply equal causes.", "Made cause, owning layer, evidence chain, corrective action, and retest gate central to the report interpretation.", "The explanatory hierarchy changed; scores, denominators, and frozen evidence did not."], + ["Shared failures", "Earlier interpretation stopped at symptoms rather than locating the contract, policy, or collector responsible.", "Used official contracts, final answers, evaluator output, HAR, and events to identify four cause classes, including browser-specific causes on Task 506.", "No hidden model reasoning is inferred; historical official scores remain unchanged."], + ["Cause review", "Automatic evaluators locate an unmet acceptance type but do not prove its cause.", "Stored per-browser, per-task causes bound to the attempts hash; unreviewed fields remain blank and complete failure coverage is required for release.", "All ten failed logical results in this version are reviewed; this does not change official scores."], + ["Runtime corrections", "Invalid contracts and weaknesses in empty-result handling, candidate aggregation, HAR capture, and final-step handling could create avoidable failures or false negatives.", "Added contract admission, candidate and pre-submit checks, immediate JSON HAR capture, fail-closed final-step handling, and a recorded step ceiling.", "Code gates passed, but any task-outcome improvement still requires new execution evidence."], + ["Result details", "Contracts, answers, and rule results were duplicated while raw JSON dominated the reading order.", "Combined them into a task-and-acceptance section with readable conclusions first and technical values collapsed.", "Public evidence and download links remain unchanged."], + ["Local environment and expansion", "The Colima gateway and the scope of locally runnable official environments were unclear.", "Distinguished VM and host gateway roles, documented the four measured sites, and corrected the available candidate counts.", "Wikipedia and Map remain explicitly unmeasured; deployability is not presented as verification."] + ] + }} }, { "id": "2026-09-10-track-naming-and-release-hardening", @@ -180,7 +354,16 @@ "CI 增加旧命名扫描;发布检查拒绝 SVG 图;视觉检查渲染中英文核心路由的桌面与窄屏、明暗主题组合,阻止页面级溢出、核心卡片裁切和表头竖排。", "新增 104 个渲染状态的结构性视觉门禁;不改变报告数据。" ] - ] + ], + "i18n": {"en": { + "title": "Standardize track names and strengthen release checks", + "summary": "The autonomous track is Browser Use and the fixed-action track is Browser Trajectory Replay. Winner marks, English accessibility text, filter recovery, asset format, and responsive layout now have automated gates. Scores, denominators, and evidence are unchanged.", + "changes": [ + ["Track names", "Earlier names mixed implementation layers with evaluation subjects.", "Renamed the tracks Browser Use and Browser Trajectory Replay across code, commands, configuration, and publication paths without compatibility aliases.", "Only names and entry points changed; tasks, runs, and scoring did not."], + ["Comparison and localization", "Ties or display-equal values could be marked as winners, and some English attributes retained Chinese.", "Mark only uniquely better displayed values; localize titles and accessibility attributes; recover invalid browser filters to a valid default.", "Reader interface defects were corrected without changing measurements or scores."], + ["Regression gates", "Naming, graphics, and responsive layout relied mainly on manual memory.", "Added retired-name checks, SVG rejection, and bilingual light/dark desktop and narrow-screen rendering checks.", "The 104 structural render states do not change report data."] + ] + }} }, { "id": "2026-09-10-comparison-and-review-presentation", @@ -220,7 +403,18 @@ "每个 action 使用独立比较卡,只以单次均值判优并将总耗时、调用数保持中性;详情首屏只显示简明失败规则,完整诊断保留在逐规则证据区。", "修正比较解释与页面层级,不改变测量值、判分或证据。" ] - ] + ], + "i18n": {"en": { + "title": "Standardize comparison statistics and scalable browser comparison", + "summary": "Comparison summaries now use mean, P50, and P95. Same-task comparison uses a baseline plus multiple comparison browsers. Action latency ranks mean per call only, and detailed evaluator evidence no longer appears in KPI cards. Both official scores remain 25/30.", + "changes": [ + ["Summary statistics", "Latency and token summaries lacked a consistent central and tail structure.", "Show mean, P50, and P95 for each metric without duplicating P50 under another name.", "Only presentation changed; scores, denominators, and evidence did not."], + ["Comparison presentation", "Readers had to infer metric direction cell by cell.", "Mark higher passes and pass rates and lower latency and token usage as better; keep ties and call counts neutral.", "Winner marks are descriptive and do not change task verdicts."], + ["Page responsibilities", "A failure list duplicated result filters, and review history contained a per-result inventory.", "Keep pass/fail filters and details in Results; keep only review rounds, corrections, and remaining items in Review.", "Duplicate presentation was removed without deleting evidence."], + ["Multi-browser comparison", "The matrix widened per browser and its filter could not compare one baseline with several browsers.", "Use one baseline, multi-select comparison browsers, explicit outcome relations, and linked pass/fail browser groups.", "The interface can add browsers without changing current results."], + ["Action latency and result details", "Action cells mixed three metrics and ranked totals despite unequal call counts; long errors disrupted result summaries.", "Use one card per action, rank only mean per call, keep totals and counts neutral, and retain full diagnostics in rule evidence.", "Interpretation and hierarchy changed; measurements, verdicts, and evidence did not."] + ] + }} }, { "id": "2026-09-10-environment-timing-and-report-model-correction", @@ -260,7 +454,18 @@ "分别显示证据完整性 60/60 与独立语义复核 0/60,并移除固定样本上的总体推断式置信区间。", "降低结论强度;不改变 evaluator 分数。" ] - ] + ], + "i18n": {"en": { + "title": "Correct deployment evidence, timing boundaries, and report responsibilities", + "summary": "The report documents the four WebArena-Verified sites deployed in Colima/Docker and the private host gateway used by browser containers. Dataset pages contain definitions only; result pages add agent end-to-end and BrowserTool latency. Both scores remain 25/30.", + "changes": [ + ["Frozen protocol", "The renderer chose the last execution even though the frozen manifest made the primary run authoritative.", "Read result authority from the frozen manifest and restore the matching result, answer, and trajectory.", "Neither browser recovered on later executions, so scores remain 25/30; only result identity and evidence semantics changed."], + ["Deployment environment", "The diagram did not locate the four sites, explain the private gateway, or disclose missing preparation receipts.", "Document the macOS, Colima/Docker, site-container, and browser-container topology, ports, image identities, and evidence boundary.", "The 30 tasks cover four sites only; Wikipedia and Map are neither deployed nor scored. No results were rerun."], + ["Timing boundary", "Earlier wording incorrectly included site reset in end-to-end latency and did not separate browser-tool time.", "Exclude the preceding site reset, aggregate BrowserTool wall time from events, and retain phase timings for future runs.", "Only diagnostics and wording changed; BrowserTool wall time still includes more than browser-core CPU time."], + ["Report model", "Dataset pages mixed outcomes with definitions, result tables assumed two browsers, and participants were unclear.", "Keep contracts in Dataset, render browsers dynamically in Results, and distinguish browser participants from shared controls.", "Information ownership and extensibility changed; current data and scores did not."], + ["Review boundary", "Evidence-integrity checks could be mistaken for independent semantic review.", "Show evidence integrity and semantic-review coverage separately and remove population-style confidence intervals from the fixed cohort.", "Claims were narrowed without changing evaluator scores."] + ] + }} }, { "id": "2026-09-10-logical-results-and-publication-qa", @@ -294,7 +499,17 @@ "把逻辑结果、禁用内部笔记、表格换行、代码对比度、跨断点视觉检查和变更记录要求写入生成器、自动检查、AGENTS.md 与仓库内 benchmark-report skill。", "后续报告若重新出现这些已知问题,将在发布前检查中失败。" ] - ] + ], + "i18n": {"en": { + "title": "Unify logical-result semantics and complete report readability corrections", + "summary": "The public report now exposes one logical result per task and browser, with all physical-execution latency and model usage accumulated. Scores remain 25/30 for both browsers. Dense tables, code contrast, diagrams, narrow layouts, and release history now share one publication gate.", + "changes": [ + ["Logical results", "Physical executions were presented as separate headline results.", "Publish one logical result per task and browser; let the final allowed execution decide the outcome while accumulating every execution's cost.", "Scores and denominators remain unchanged and physical evidence is retained."], + ["Readability", "Short table headers wrapped vertically, code contrast was weak, and the text-heavy diagram was unreadable at normal size.", "Add semantic column widths and scrolling, correct theme contrast, replace the diagram with a readable raster asset, and inspect desktop and narrow layouts.", "Only presentation changed."], + ["Release history", "The page lacked a current summary, compact archive, effect boundaries, and a structured download.", "Use a report-style current release, expandable details, compact history, and structured data.", "Release history became part of the report without changing evidence or results."], + ["Repeatable publication gate", "Manual memory could not reliably prevent known regressions.", "Encode logical-result, privacy, table, contrast, responsive, and changelog requirements in automated release checks.", "Known regressions now fail before publication."] + ] + }} }, { "id": "2026-09-10-deployment-and-autonomy-evidence", @@ -316,7 +531,15 @@ "新增双语流程图和逐环节证据表,明确运行器按试次启动官方站点镜像、健康检查、登录,再由 CowAgent 动态产生 BrowserTool 调用。", "既有测量不重跑;新增说明直接绑定现有 manifest、事件、HAR 与 trace 哈希。" ] - ] + ], + "i18n": {"en": { + "title": "Complete deployment and autonomous-execution evidence", + "summary": "The title now identifies Chromium and Moli as the Browser Use participants. Methodology adds the full path from local WebArena-Verified deployment and per-execution reset through the CowAgent loop, official acceptance, and retained evidence. Measurements are unchanged.", + "changes": [ + ["Report title", "The earlier title foregrounded a control variable rather than the evaluated browsers.", "Make Chromium and Moli the title subjects and present CowAgent, model, reasoning, and tasks as conditions.", "Narrative hierarchy changed; design and results did not."], + ["Deployment and autonomy evidence", "Reset and autonomous planning were asserted without showing deployment ownership or the agent loop.", "Add a bilingual flow and evidence table linking site startup, health checks, sign-in, and dynamic BrowserTool calls.", "No measurements were rerun; the explanation binds to existing manifests, events, HAR, and trace hashes."] + ] + }} }, { "id": "2026-09-10-report-evidence-restructure", @@ -350,7 +573,17 @@ "正式构建绑定官方数据 checksum 并 fail closed;增加 current-report 发布 profile、完整证据默认检查和带 marker 的原子目录替换。", "缺失或错配 selection、模型、reasoning、官方数据或证据审计时禁止发布。" ] - ] + ], + "i18n": {"en": { + "title": "Restructure the Browser Use report around the evidence chain", + "summary": "The report adds task-family coverage, Wilson intervals, 30 paired-task comparisons, and route-for-route English pages. It correctly labels deterministic evidence-integrity review and avoids attributing five shared failures without a successful path. Measurements are unchanged.", + "changes": [ + ["Home and results", "The home page did not jointly expose cohort coverage, uncertainty, call completeness, and paired outcomes.", "Add task families, Wilson intervals, solvability coverage, evidence-audit status, and a filterable 30-task Chromium/Moli comparison.", "Only explanation and drill-down changed; 25/30 scores and 30 pairs did not."], + ["Review terminology and attribution", "Deterministic consistency checks were called independent result review, and shared failures were attributed too early.", "Call the process evidence-integrity review and require separate non-scoring solvability review for tasks without a successful trajectory.", "The official evaluator remains the sole scoring authority; shared failures are no longer presented as confirmed causes."], + ["Bilingual and mobile", "English was summary-only and fixed card spans clipped narrow layouts.", "Generate an English counterpart for every Chinese route, verify links, and force cards to one column on narrow screens.", "Both languages use the same measured data; all 206 logical pages had valid links."], + ["Build and publication gates", "A formal build could silently degrade without official data, while routine checks did not require complete evidence.", "Bind builds to the official checksum and current publication profile, require complete evidence, and replace output atomically with a marker.", "Publication fails on missing or mismatched selection, model, reasoning, official data, or evidence review."] + ] + }} }, { "id": "2026-09-09-webarena-verified-pilot", @@ -396,7 +629,19 @@ "直接调用代理验证 gpt-6-astra,服务返回 team_model_access_denied;随后只用可用的 gpt-5.6-sol 完成流程。", "本轮是 GPT-5.6 条件下的正式对比,不能写成 Astra 成绩;Astra 获权后必须新建独立运行。" ] - ] + ], + "i18n": {"en": { + "title": "Complete the WebArena-Verified autonomous-agent browser comparison", + "summary": "CowAgent with gpt-5.6-sol at reasoning=low produced 60 logical results across 30 preregistered tasks in Chromium and Moli. Each browser passed 25 tasks. Deterministic evidence-integrity checks covered every result with no unresolved inconsistency.", + "changes": [ + ["Evidence and interpretation", "Navigation alone did not make task selection, executions, review coverage, solvability, or conclusions auditable.", "Use the home page as a decision entry point and publish contracts, measured results, assertions, trajectories, sanitized events, an English edition, and an evidence index.", "Publication structure changed; tasks, denominator, outputs, scores, and evidence bindings did not."], + ["Autonomous-agent track", "The repository also contained a trajectory-replay harness, making the phrase 'the original 30 tasks' ambiguous.", "State that CowAgent autonomously observes, plans, and acts for this report, while trajectory replay is a separate microbenchmark.", "Only the autonomous-agent track contributes to these scores."], + ["Benchmark selection", "Browser-use, computer-use, offline action prediction, and agentic-search claims used incompatible evaluation units.", "Compare primary-source adoption and evaluation contracts across relevant benchmark families.", "Choose self-hosted WebArena-Verified for deterministic outcome evaluation; live-site and model-judge tracks remain future work."], + ["Complete formal run", "The cohort needed short, medium, and long workflows without post-run task selection.", "Preregister 5/10/15 operation-length tiers and one logical result per task-browser pair, with at most one declared execution after failure.", "Both browsers passed 25/30; five tasks failed jointly and none produced a one-browser-only pass."], + ["Evidence integrity", "Scores needed traceability to final answers, network side effects, and official evaluators without manual overrides.", "Retain responses, events, HAR, traces, and evaluator details and verify files, hashes, scores, statuses, and transport attempts.", "All logical results passed evidence-integrity checks with no scoring disagreement or unresolved item; sensitive network bodies remain private."], + ["Model-coverage boundary", "Availability in a model catalog did not establish access for this run.", "Record gpt-5.6-sol as the only measured model after Astra access was unavailable.", "This is a GPT-5.6 comparison, not an Astra result; Astra requires a separate run when available."] + ] + }} } ], "review_rounds": [ diff --git a/scripts/check_browser_use_report.py b/scripts/check_browser_use_report.py index 30b190b20..c20b6bd3d 100644 --- a/scripts/check_browser_use_report.py +++ b/scripts/check_browser_use_report.py @@ -11,11 +11,25 @@ from harness.browser_use.failure_cause_review import ( DEFAULT_REVIEW_PATH as FAILURE_CAUSE_REVIEW_PATH, +) +from harness.browser_use.failure_cause_review import ( load_failure_cause_review, - sha256 as file_sha256, validate_failure_cause_review, ) -from harness.browser_use.report import _logical_results +from harness.browser_use.failure_cause_review import ( + sha256 as file_sha256, +) +from harness.browser_use.publication import evidence_metadata +from harness.browser_use.report import ( + PUBLIC_EXECUTION_ARTIFACTS, + _attempt_id, + _configure_engine_labels, + _configure_models, + _load_evidence_bundles, + _logical_results, + _public_attempt, + _public_execution, +) from harness.browser_use.selection import load_pilot @@ -23,16 +37,210 @@ def _sha256(path: Path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest() +def _check_execution_artifacts( + report_dir: Path, + logical_rows: list[dict], + public_results: list[dict], + evidence_index: dict, +) -> None: + """Verify that every physical execution has one safe, hash-bound public projection.""" + + logical_by_id = {_attempt_id(row): row for row in logical_rows} + public_by_id = {str(row.get("id")): row for row in public_results} + indexed_rows = evidence_index.get("results") or [] + indexed_by_id = {str(row.get("result_id")): row for row in indexed_rows} + expected_ids = set(logical_by_id) + if len(public_by_id) != len(public_results) or set(public_by_id) != expected_ids: + raise ValueError("public result projection must identify every logical result exactly once") + if len(indexed_by_id) != len(indexed_rows) or set(indexed_by_id) != expected_ids: + raise ValueError("public evidence index must cover every logical result exactly once") + if evidence_index.get("schema") != "lex_browser_use_public_evidence_index/4": + raise ValueError("public evidence index schema does not describe physical executions") + + expected_files: set[Path] = set() + for result_id, logical in logical_by_id.items(): + sources = logical.get("_source_executions") or [logical] + authority = int(logical["attempt"]) + public = public_by_id[result_id] + indexed = indexed_by_id[result_id] + detail_text = (report_dir / "results" / f"{result_id}.html").read_text(encoding="utf-8") + if f"包含 {len(sources)} 次物理执行" not in detail_text: + raise ValueError(f"result page omits physical execution count: {result_id}") + expected_public = _public_attempt(logical) + for key in ( + "execution_count", + "authoritative_execution", + "score", + "duration_ms", + "action_count", + "browser_tool_duration_ms", + "browser_tool_duration_by_action_ms", + "browser_tool_count_by_action", + "model_usage", + ): + if public.get(key) != expected_public.get(key): + raise ValueError( + f"public logical aggregate cannot be recomputed: {result_id}/{key}" + ) + for key in ( + "model", + "engine", + "task_id", + "score", + "execution_count", + "authoritative_execution", + ): + if indexed.get(key) != public.get(key): + raise ValueError(f"evidence index logical binding differs: {result_id}/{key}") + if public.get("execution_count") != len(sources): + raise ValueError(f"public result has wrong physical execution count: {result_id}") + if public.get("authoritative_execution") != authority: + raise ValueError(f"public result changed score authority: {result_id}") + if indexed.get("execution_count") != len(sources): + raise ValueError(f"evidence index has wrong physical execution count: {result_id}") + if indexed.get("authoritative_execution") != authority: + raise ValueError(f"evidence index changed score authority: {result_id}") + executions = indexed.get("executions") or [] + if len(executions) != len(sources): + raise ValueError(f"evidence index omitted a physical execution: {result_id}") + indexed_by_number = {int(item.get("execution") or 0): item for item in executions} + if len(indexed_by_number) != len(executions): + raise ValueError(f"evidence index duplicates a physical execution: {result_id}") + for source in sources: + number = int(source["attempt"]) + indexed_execution = indexed_by_number.get(number) + if indexed_execution is None: + raise ValueError(f"evidence index omitted execution {number}: {result_id}") + expected_measurements = _public_execution(source, authoritative=number == authority) + measured = {key: indexed_execution.get(key) for key in expected_measurements} + if measured != expected_measurements: + raise ValueError(f"published execution measurements differ: {result_id}/{number}") + artifacts = indexed_execution.get("artifacts") or {} + if set(artifacts) != set(PUBLIC_EXECUTION_ARTIFACTS): + raise ValueError(f"execution artifact inventory differs: {result_id}/{number}") + for label, filename in PUBLIC_EXECUTION_ARTIFACTS.items(): + relative = Path("artifacts") / result_id / "executions" / str(number) / filename + artifact = artifacts[label] + if ( + set(artifact) != {"path", "sha256"} + or artifact.get("path") != relative.as_posix() + ): + raise ValueError( + f"unsafe or incorrect artifact link: {result_id}/{number}/{label}" + ) + path = report_dir / relative + expected_files.add(path) + if not path.is_file(): + raise ValueError(f"missing physical execution artifact: {path}") + if artifact.get("sha256") != _sha256(path): + raise ValueError(f"physical execution artifact hash differs: {path}") + if ( + f'href="{relative.as_posix()}"' not in detail_text + or str(artifact["sha256"]) not in detail_text + ): + raise ValueError( + f"result page omits execution artifact link or hash: " + f"{result_id}/{number}/{label}" + ) + bundle_path = ( + report_dir + / "artifacts" + / result_id + / "executions" + / str(number) + / PUBLIC_EXECUTION_ARTIFACTS["execution_evidence"] + ) + bundle = json.loads(bundle_path.read_text(encoding="utf-8")) + if ( + bundle.get("schema") != "lex_browser_use_public_execution_evidence/3" + or bundle.get("result_id") != result_id + or bundle.get("execution") != number + or bundle.get("authoritative") != (number == authority) + or bundle.get("measurements") != expected_measurements + ): + raise ValueError(f"execution evidence binding differs: {result_id}/{number}") + for key in ("agent_response", "evaluation", "agent_events", "trajectory"): + if key not in bundle: + raise ValueError( + f"execution evidence payload is incomplete: {result_id}/{number}/{key}" + ) + source_files = (bundle.get("source_evidence") or {}).get("files") or {} + if source_files != (evidence_metadata(source).get("files") or {}): + raise ValueError(f"source evidence hashes differ: {result_id}/{number}") + missing_public_sources = { + label + for label in ("response", "evaluation", "events") + if not (source_files.get(label) or {}).get("present") + } + if missing_public_sources: + raise ValueError( + f"physical execution lacks publishable source evidence: " + f"{result_id}/{number}/{sorted(missing_public_sources)}" + ) + for private_label in ("network_har", "agent_trace"): + if ( + private_label in source_files + and source_files[private_label].get("publication") + != "private_only_session_sensitive" + ): + raise ValueError(f"private {private_label} was marked publishable") + + artifact_root = report_dir / "artifacts" + actual_files = {path for path in artifact_root.rglob("*") if path.is_file()} + if actual_files != expected_files: + extra = sorted(str(path) for path in actual_files - expected_files) + missing = sorted(str(path) for path in expected_files - actual_files) + raise ValueError(f"execution artifact tree differs; extra={extra}, missing={missing}") + forbidden_text = re.compile( + r"(?:Password\.123|admin1234|test1234|hello1234|" + r"-----BEGIN [A-Z ]*PRIVATE KEY-----|\bBearer\s+[A-Za-z0-9._~+/=-]{12,}|" + r"\bsk-[A-Za-z0-9_-]{12,}|(?-i:/(?:Users|home|private|tmp|var|opt)/[^\s\"']+))", + flags=re.I, + ) + for path in actual_files: + if path.suffix.lower() in {".har", ".zip"} or path.name in { + "network.har", + "agent_trace.zip", + }: + raise ValueError(f"private HAR or trace was copied into the public report: {path}") + if forbidden_text.search(path.read_text(encoding="utf-8")): + raise ValueError(f"published execution artifact contains sensitive text: {path}") + + +def _check_public_privacy(report_dir: Path) -> None: + forbidden_text = re.compile( + r"(?:Password\.123|admin1234|test1234|hello1234|" + r"-----BEGIN [A-Z ]*PRIVATE KEY-----|\bBearer\s+[A-Za-z0-9._~+/=-]{12,}|" + r"\bsk-[A-Za-z0-9_-]{12,}|(?-i:/(?:Users|home|private|tmp|var|opt)/[^\s\"']+))", + flags=re.I, + ) + text_suffixes = {".html", ".json", ".jsonl", ".js", ".css"} + for path in report_dir.rglob("*"): + if not path.is_file(): + continue + if path.suffix.lower() == ".har" or ( + "trace" in path.name.lower() and path.suffix.lower() == ".zip" + ): + raise ValueError(f"private HAR or trace was copied into the public report: {path}") + if path.suffix.lower() in text_suffixes and forbidden_text.search( + path.read_text(encoding="utf-8") + ): + raise ValueError(f"published report contains sensitive text: {path}") + + def check( report_dir: Path, - evidence_dir: Path, + evidence_dir: Path | list[Path], selection_path: Path, *, require_complete: bool, ) -> None: profile_path = Path("evaluation/browser-use/current-report.json") profile = json.loads(profile_path.read_text(encoding="utf-8")) + _configure_engine_labels(profile) + _configure_models(profile) engines = tuple(str(engine) for engine in profile.get("engines") or []) + models = tuple(str(model) for model in profile.get("models") or []) if not engines or len(engines) != len(set(engines)): raise ValueError("current Browser Use profile must register unique browser engines") page_names = ( @@ -52,33 +260,21 @@ def check( pages = tuple(report_dir / name for name in page_names) index = pages[0] audit_path = report_dir / "report-data.json" - attempts_path = evidence_dir / "attempts.jsonl" - manifest_path = evidence_dir / "manifest.json" - run_config_path = evidence_dir / "run-config.json" + bundles = _load_evidence_bundles(evidence_dir) task_pages = tuple( report_dir / "datasets" / f"{task_id}.html" for task_id in sorted(task.task_id for task in load_pilot(selection_path).tasks) ) - measured_rows = ( - [ - json.loads(line) - for line in attempts_path.read_text(encoding="utf-8").splitlines() - if line.strip() - ] - if attempts_path.is_file() - else [] - ) - manifest_for_projection = ( - json.loads(manifest_path.read_text(encoding="utf-8")) if manifest_path.is_file() else {} - ) - logical_rows = _logical_results( - measured_rows, manifest_for_projection.get("protocol") or {} - ) + measured_rows = [row for bundle in bundles for row in bundle["source_attempts"]] + logical_rows = [ + row + for bundle in bundles + for row in _logical_results( + bundle["source_attempts"], bundle["manifest"].get("protocol") or {} + ) + ] result_pages = tuple( - report_dir - / "results" - / f"{row['engine']}-{int(row['task_id'])}.html" - for row in logical_rows + report_dir / "results" / f"{_attempt_id(row)}.html" for row in logical_rows ) chinese_pages = (*pages, *task_pages, *result_pages) english_pages = tuple( @@ -98,15 +294,15 @@ def check( ) ) artifact_files = tuple( - report_dir / "artifacts" / page.stem / name - for page in result_pages - for name in ( - "agent-response.json", - "evaluation.json", - "agent-events.json", - "trajectory.json", - "evidence-metadata.json", - ) + report_dir + / "artifacts" + / _attempt_id(row) + / "executions" + / str(source["attempt"]) + / filename + for row in logical_rows + for source in (row.get("_source_executions") or [row]) + for filename in PUBLIC_EXECUTION_ARTIFACTS.values() ) if not all( path.is_file() @@ -123,31 +319,35 @@ def check( audit = json.loads(audit_path.read_text(encoding="utf-8")) if audit.get("selection_sha256") != selection.sha256: raise ValueError("report was not built from the current pre-registered selection") - rows = [] - if attempts_path.exists(): - rows = [ - json.loads(line) - for line in attempts_path.read_text(encoding="utf-8").splitlines() - if line.strip() - ] + rows = measured_rows selected_ids = {task.task_id for task in selection.tasks} - expected_primary = {(engine, task_id, 1) for engine in engines for task_id in selected_ids} - by_key: dict[tuple[str, int, int], dict] = {} + expected_primary = { + (model, engine, task_id, 1) + for model in models + for engine in engines + for task_id in selected_ids + } + by_key: dict[tuple[str, str, int, int], dict] = {} for row in rows: - key = (str(row["engine"]), int(row["task_id"]), int(row["attempt"])) + key = ( + str(row["model"]), + str(row["engine"]), + int(row["task_id"]), + int(row["attempt"]), + ) if key in by_key: raise ValueError(f"duplicate measured attempt: {key}") - if key[0] not in set(engines) or key[1] not in selected_ids: + if key[0] not in set(models) or key[1] not in set(engines) or key[2] not in selected_ids: raise ValueError(f"attempt outside the registered cohort: {key}") - if key[2] not in {1, 2}: + if key[3] not in {1, 2}: raise ValueError(f"more than one retry is not allowed: {key}") by_key[key] = row - for engine, task_id, attempt in by_key: + for model, engine, task_id, attempt in by_key: if attempt == 2: - primary = by_key.get((engine, task_id, 1)) + primary = by_key.get((model, engine, task_id, 1)) if primary is None or float(primary["score"]) == 1.0: raise ValueError( - f"retry is allowed only after a failed primary: {(engine, task_id)}" + f"retry is allowed only after a failed primary: {(model, engine, task_id)}" ) rendered_pages = { page: page.read_text(encoding="utf-8") for page in (*chinese_pages, *english_pages) @@ -155,9 +355,7 @@ def check( if any('' not in rendered_pages[page] for page in english_pages): raise ValueError("every Chinese report route must have an English mirror") cjk = re.compile(r"[\u3400-\u9fff]") - localized_attribute = re.compile( - r'(?:aria-label|placeholder|title)="([^"]*)"', flags=re.I - ) + localized_attribute = re.compile(r'(?:aria-label|placeholder|title)="([^"]*)"', flags=re.I) for page in english_pages: text = rendered_pages[page] title = re.search(r"(.*?)", text, flags=re.I | re.S) @@ -175,7 +373,7 @@ def check( "task pages must publish frozen contracts without embedding measured comparisons" ) dataset_index = rendered_pages[report_dir / "datasets/index.html"] - if 'data-result=' in dataset_index or "可完成性证据" in dataset_index: + if "data-result=" in dataset_index or "可完成性证据" in dataset_index: raise ValueError("dataset inventory must not mix measured results into task definitions") review_index = rendered_pages[report_dir / "review/index.html"] if ( @@ -254,11 +452,8 @@ def check( evidence_index = json.loads( (report_dir / "data/evidence-index.json").read_text(encoding="utf-8") ) - indexed_results = evidence_index.get("results") or [] - if {item.get("result_id") for item in indexed_results} != { - page.stem for page in result_pages - }: - raise ValueError("public evidence index must cover every logical result exactly once") + _check_execution_artifacts(report_dir, logical_rows, public_results, evidence_index) + _check_public_privacy(report_dir) forbidden = { "agent_response", "paths", @@ -284,7 +479,9 @@ def check( ) for page, text in rendered_pages.items(): if any(term in text for term in forbidden_report_terms): - raise ValueError(f"formal report contains forbidden notebook or execution-label text: {page}") + raise ValueError( + f"formal report contains forbidden notebook or execution-label text: {page}" + ) if re.search( r"]+(?:src|data)=[\"'][^\"']*\.svg(?:[?#][^\"']*)?[\"']", text, @@ -314,155 +511,147 @@ def check( if required_style not in style: raise ValueError(f"report is missing publication readability guard: {required_style}") if require_complete: - if not manifest_path.is_file() or not run_config_path.is_file(): - raise ValueError("complete evidence requires manifest.json and run-config.json") - manifest = json.loads(manifest_path.read_text(encoding="utf-8")) - run_config = json.loads(run_config_path.read_text(encoding="utf-8")) - official = (manifest.get("environment") or {}).get("official_evaluator") or {} - expected_profile = { - "selection_sha256": selection.sha256, - "model": (manifest.get("protocol") or {}).get("model"), - "reasoning_effort": (manifest.get("protocol") or {}).get("reasoning_effort"), - "official_data_checksum": official.get("data_checksum"), - } - for key, expected in expected_profile.items(): - if profile.get(key) != expected: - raise ValueError(f"current report profile mismatch for {key}") - browser_versions = set(((manifest.get("environment") or {}).get("browser_versions") or {})) - if set(engines) != browser_versions: - raise ValueError("current report engines differ from measured browser identities") - manifest_score_authority = (manifest.get("protocol") or {}).get( - "official_score_uses" - ) or "final execution for each browser and task" - if audit.get("score_authority") != manifest_score_authority: - raise ValueError("report renderer changed the frozen score authority") if not str(profile.get("logical_result") or "").startswith( "score authority follows the frozen run manifest" ): raise ValueError("current report profile must defer score authority to the manifest") - if manifest.get("selection_sha256") != selection.sha256: - raise ValueError("manifest selection hash differs from the registered cohort") - if run_config.get("selection_sha256") != selection.sha256: - raise ValueError("run configuration differs from the registered cohort") - if run_config.get("reasoning_effort") != "low": - raise ValueError("pilot reasoning effort must be low") - if not run_config.get("browser_eval_commit"): - raise ValueError("run configuration must identify the browser-eval commit") - if manifest.get("protocol", {}).get("browser_eval_commit") != run_config.get( - "browser_eval_commit" - ): - raise ValueError("manifest and run configuration disagree on browser-eval commit") - if manifest.get("protocol", {}).get("model_transport_retries") != 1: - raise ValueError("pilot allows exactly one model transport retry") + authorities = audit.get("score_authority") or {} + review_bindings = (audit.get("review") or {}).get("bindings") or {} + for bundle in bundles: + evidence_path = bundle["path"] + manifest_path = evidence_path / "manifest.json" + run_config_path = evidence_path / "run-config.json" + attempts_path = evidence_path / "attempts.jsonl" + review_path = evidence_path / "result-review.json" + if not manifest_path.is_file() or not run_config_path.is_file(): + raise ValueError("complete evidence requires manifest.json and run-config.json") + manifest, run_config = bundle["manifest"], bundle["config"] + model = bundle["model"] + official = (manifest.get("environment") or {}).get("official_evaluator") or {} + if model not in models: + raise ValueError( + f"current report profile does not register measured model: {model}" + ) + for key, expected in { + "selection_sha256": selection.sha256, + "reasoning_effort": (manifest.get("protocol") or {}).get("reasoning_effort"), + "official_data_checksum": official.get("data_checksum"), + }.items(): + if profile.get(key) != expected: + raise ValueError(f"current report profile mismatch for {key}: {model}") + browser_versions = set( + (manifest.get("environment") or {}).get("browser_versions") or {} + ) + if set(engines) != browser_versions: + raise ValueError(f"current report engines differ from measured identities: {model}") + authority = (manifest.get("protocol") or {}).get( + "official_score_uses" + ) or "final execution for each browser and task" + if authorities.get(model) != authority: + raise ValueError(f"report renderer changed frozen score authority: {model}") + if manifest.get("selection_sha256") != selection.sha256: + raise ValueError("manifest selection hash differs from the registered cohort") + if run_config.get("selection_sha256") != selection.sha256: + raise ValueError("run configuration differs from the registered cohort") + if run_config.get("reasoning_effort") != profile.get("reasoning_effort"): + raise ValueError("run configuration reasoning effort differs from the profile") + if not run_config.get("browser_eval_commit"): + raise ValueError("run configuration must identify the browser-eval commit") + if manifest.get("protocol", {}).get("browser_eval_commit") != run_config.get( + "browser_eval_commit" + ): + raise ValueError("manifest and run configuration disagree on browser-eval commit") + if manifest.get("protocol", {}).get("model_transport_retries") != 1: + raise ValueError("pilot allows exactly one model transport retry") + if not review_path.is_file(): + raise ValueError("complete publication requires result-review.json per model") + bundle_review = bundle["review"] + bindings = bundle_review.get("bindings") or {} + if bindings.get("attempts_sha256") != _sha256(attempts_path): + raise ValueError(f"result review is not bound to current attempts: {model}") + if bindings.get("manifest_sha256") != _sha256(manifest_path): + raise ValueError(f"result review is not bound to current manifest: {model}") + if bindings.get("selection_sha256") != selection.sha256: + raise ValueError("result review is not bound to the registered selection") + model_rows = [row for row in rows if row["model"] == model] + coverage = bundle_review.get("coverage") or {} + if coverage != { + "recorded_attempts": len(model_rows), + "reviewed_attempts": len(model_rows), + }: + raise ValueError(f"result review does not cover every attempt: {model}") + if bundle_review.get("disagreements") or bundle_review.get("unresolved"): + raise ValueError(f"result review has disagreements or unresolved checks: {model}") + reviewed_keys = { + (model, str(item["engine"]), int(item["task_id"]), int(item["attempt"])) + for item in bundle_review.get("attempts") or [] + } + measured_keys = {key for key in by_key if key[0] == model} + if reviewed_keys != measured_keys: + raise ValueError(f"result review inventory differs from evidence: {model}") + if review_bindings.get(model) != bindings: + raise ValueError(f"rendered report review binding differs: {model}") missing = expected_primary - set(by_key) if missing: raise ValueError(f"missing {len(missing)} primary attempts") missing_retries = { - (engine, task_id, 2) - for engine, task_id, _ in expected_primary - if float(by_key[(engine, task_id, 1)]["score"]) != 1.0 - and (engine, task_id, 2) not in by_key + (model, engine, task_id, 2) + for model, engine, task_id, _ in expected_primary + if float(by_key[(model, engine, task_id, 1)]["score"]) != 1.0 + and (model, engine, task_id, 2) not in by_key } if missing_retries: raise ValueError(f"missing {len(missing_retries)} permitted failure retries") if "测量尚未完整" in html: raise ValueError("complete publication cannot contain an incomplete-results notice") - review_path = evidence_dir / "result-review.json" - if not review_path.is_file(): - raise ValueError("complete publication requires result-review.json") - review = json.loads(review_path.read_text(encoding="utf-8")) - bindings = review.get("bindings") or {} - if bindings.get("attempts_sha256") != _sha256(attempts_path): - raise ValueError("result review is not bound to the current attempts") - if bindings.get("manifest_sha256") != _sha256(manifest_path): - raise ValueError("result review is not bound to the current manifest") - if bindings.get("selection_sha256") != selection.sha256: - raise ValueError("result review is not bound to the registered selection") - coverage = review.get("coverage") or {} - if coverage != {"recorded_attempts": len(rows), "reviewed_attempts": len(rows)}: - raise ValueError("result review does not cover every recorded attempt") - if review.get("disagreements") or review.get("unresolved"): - raise ValueError("result review contains disagreements or unresolved checks") - reviewed_keys = { - (str(item["engine"]), int(item["task_id"]), int(item["attempt"])) - for item in review.get("attempts") or [] - } - if reviewed_keys != set(by_key): - raise ValueError("result review attempt inventory differs from measured evidence") - failed_result_ids = { - f"{row['engine']}-{int(row['task_id'])}" - for row in logical_rows - if float(row.get("score") or 0) < 1 - } - cause_review = load_failure_cause_review() cause_profile = profile.get("failure_cause_review") or {} cause_required = bool(cause_profile.get("required_for_publication")) - verified_causes = validate_failure_cause_review( - cause_review, - attempts_sha256=_sha256(attempts_path), - failed_result_ids=failed_result_ids, - require_complete=cause_required, - ) - if cause_required: - if cause_profile.get("source_sha256") != file_sha256( - FAILURE_CAUSE_REVIEW_PATH - ): - raise ValueError( - "current report profile is not bound to the failure-cause review" + default_cause_review = load_failure_cause_review() + verified_causes: dict[str, dict] = {} + for bundle in bundles: + model = bundle["model"] + failed_ids = { + _attempt_id(row) + for row in logical_rows + if row["model"] == model and float(row.get("score") or 0) < 1 + } + verified_causes.update( + validate_failure_cause_review( + default_cause_review, + model_id=model, + attempts_sha256=bundle["attempts_sha256"], + failed_result_ids=failed_ids, + require_complete=cause_required, ) + ) + if cause_required: + failed_result_ids = { + _attempt_id(row) for row in logical_rows if float(row.get("score") or 0) < 1 + } if set(verified_causes) != failed_result_ids: raise ValueError("every failed logical result must have a verified root cause") - cause_bindings = cause_review.get("bindings") or {} - if cause_bindings.get("manifest_sha256") != _sha256(manifest_path): - raise ValueError("failure-cause review is not bound to the current manifest") - if cause_bindings.get("selection_sha256") != selection.sha256: - raise ValueError("failure-cause review is not bound to the current selection") + configured_hash = cause_profile.get("source_sha256") + if not isinstance(configured_hash, str) or configured_hash != file_sha256( + FAILURE_CAUSE_REVIEW_PATH + ): + raise ValueError("current profile is not bound to the failure-cause review") published_cause_review = json.loads( (report_dir / "data/failure-cause-review.json").read_text(encoding="utf-8") ) if published_cause_review.get("results") != verified_causes: - raise ValueError("published failure causes differ from the verified review") - public_by_id = {str(item["id"]): item for item in public_results} - for result_id, item in public_by_id.items(): - has_verified_cause = result_id in verified_causes - if bool(item.get("verified_failure_subcategory")) != has_verified_cause: - raise ValueError(f"failure subcategory review state mismatch: {result_id}") - if bool(item.get("verified_failure_reason")) != has_verified_cause: - raise ValueError(f"failure reason review state mismatch: {result_id}") - report_review = audit.get("review") or {} - if report_review.get("bindings") != bindings: - raise ValueError("rendered report is not bound to the current result review") - for _page, text in rendered_pages.items(): - if str(evidence_dir.resolve()) in text: - raise ValueError("published pages must not contain machine-local evidence paths") - forbidden_text = ( - "Password.123", - "admin1234", - "test1234", - "hello1234", - ) - for path in report_dir.rglob("*"): - if path.is_file() and path.suffix in {".html", ".json", ".jsonl", ".js", ".css"}: - text = path.read_text(encoding="utf-8") - if any(secret in text for secret in forbidden_text): + raise ValueError("published failure causes differ from verified reviews") + for bundle in bundles: + for _page, text in rendered_pages.items(): + if str(bundle["path"]) in text: raise ValueError( - f"published evidence contains a secret or local origin: {path}" + "published pages must not contain machine-local evidence paths" ) - if re.search(r"/Users/[^/\s<]+/", text) or any( - re.search(pattern, text, flags=re.I) - for pattern in ( - r"-----BEGIN [A-Z ]*PRIVATE KEY-----", - r"\bBearer\s+[A-Za-z0-9._~+/=-]{12,}", - r"\bsk-[A-Za-z0-9_-]{12,}", - ) - ): - raise ValueError(f"published evidence contains sensitive text: {path}") def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("report", type=Path) - parser.add_argument("--evidence", required=True, type=Path) + parser.add_argument("--evidence", required=True, action="append", type=Path) parser.add_argument( "--selection", type=Path, diff --git a/scripts/package_browser_use_artifact_hub.py b/scripts/package_browser_use_artifact_hub.py index 6ad976a6e..8323db060 100644 --- a/scripts/package_browser_use_artifact_hub.py +++ b/scripts/package_browser_use_artifact_hub.py @@ -36,10 +36,8 @@ def tree_sha256(root: Path) -> str: def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--report", type=Path, default=Path("reports/browser-use")) - parser.add_argument("--evidence", required=True, type=Path) - parser.add_argument( - "--output", type=Path, default=Path("publish/browser-use-sites.zip") - ) + parser.add_argument("--evidence", required=True, action="append", type=Path) + parser.add_argument("--output", type=Path, default=Path("publish/browser-use-sites.zip")) args = parser.parse_args() check( args.report, @@ -55,17 +53,20 @@ def main() -> None: raise ValueError(f"publication cannot contain symlinks: {path}") archive.write(path, path.relative_to(args.report)) evidence_files = { - name: sha256(args.evidence / name) - for name in ("manifest.json", "run-config.json", "attempts.jsonl", "result-review.json") + str(evidence): { + name: sha256(evidence / name) + for name in ("manifest.json", "run-config.json", "attempts.jsonl", "result-review.json") + } + for evidence in args.evidence } attestation = { - "schema": "lex_browser_use_publication_attestation/1", + "schema": "lex_browser_use_publication_attestation/2", "archive": str(args.output), "archive_sha256": sha256(args.output), "report": str(args.report), "report_tree_sha256": tree_sha256(args.report), "profile_sha256": sha256(PROFILE), - "evidence": str(args.evidence), + "evidence": [str(path) for path in args.evidence], "evidence_files": evidence_files, } ATTESTATION.write_text( diff --git a/scripts/publish_browser_use_artifact_hub.py b/scripts/publish_browser_use_artifact_hub.py index 9be3f3adb..9415250ad 100644 --- a/scripts/publish_browser_use_artifact_hub.py +++ b/scripts/publish_browser_use_artifact_hub.py @@ -11,6 +11,7 @@ import urllib.request import uuid from pathlib import Path +from urllib.error import HTTPError from harness.web_fetch.artifact_auth import load_token @@ -42,18 +43,25 @@ def validate_package() -> None: if not ARCHIVE.is_file() or not ATTESTATION.is_file(): raise ValueError("missing validated Browser Use package or attestation") attestation = json.loads(ATTESTATION.read_text(encoding="utf-8")) + if attestation.get("schema") != "lex_browser_use_publication_attestation/2": + raise ValueError("publication attestation is not the current multi-evidence schema") report = Path(str(attestation.get("report") or "")) - evidence = Path(str(attestation.get("evidence") or "")) + evidence_paths = [Path(str(path)) for path in attestation.get("evidence") or []] expected_evidence = attestation.get("evidence_files") or {} checks = { "archive": sha256(ARCHIVE) == attestation.get("archive_sha256"), "report": report.is_dir() and tree_sha256(report) == attestation.get("report_tree_sha256"), "profile": sha256(PROFILE) == attestation.get("profile_sha256"), - "evidence": evidence.is_dir() + "evidence": bool(evidence_paths) + and set(expected_evidence) == {str(path) for path in evidence_paths} and all( - (evidence / name).is_file() and sha256(evidence / name) == digest - for name, digest in expected_evidence.items() + path.is_dir() + and all( + (path / name).is_file() and sha256(path / name) == digest + for name, digest in expected_evidence[str(path)].items() + ) + for path in evidence_paths ), } failed = [name for name, valid in checks.items() if not valid] @@ -90,8 +98,11 @@ def request( if content_type: headers["Content-Type"] = content_type req = urllib.request.Request(base + path, data=data, headers=headers, method=method) - with urllib.request.urlopen(req, timeout=240) as response: - return response.status, response.read() + try: + with urllib.request.urlopen(req, timeout=240) as response: + return response.status, response.read() + except HTTPError as error: + return error.code, error.read() def multipart_archive() -> tuple[bytes, str]: @@ -101,7 +112,7 @@ def multipart_archive() -> tuple[bytes, str]: parts = [] for key, value in ( ("mode", "zip"), - ("title", "Chromium 与 Moli:Browser Use 能力实测"), + ("title", "浏览器 Browser Use 能力对比"), ): parts.append( ( @@ -167,6 +178,15 @@ def main(*, create: bool = False) -> None: if key not in {"editToken", "claimToken"} } ) + title_status, title_raw = request( + f"/api/sites/{result['slug']}", + json.dumps({"title": "浏览器 Browser Use 能力对比"}).encode(), + "application/json", + method="PATCH", + ) + if not 200 <= title_status < 300: + raise RuntimeError(json.loads(title_raw)) + result["title"] = "浏览器 Browser Use 能力对比" if not result.get("share_url"): status, raw = request( f"/api/sites/{result['slug']}/shares", @@ -189,7 +209,7 @@ def main(*, create: bool = False) -> None: ) with urllib.request.urlopen(preview, timeout=60) as response: body = response.read().decode("utf-8", errors="replace") - if response.status != 200 or "Chromium 与 Moli:Browser Use 能力实测" not in body: + if response.status != 200 or "浏览器 Browser Use 能力对比" not in body: raise RuntimeError("public preview is unavailable or missing the report heading") print(json.dumps({"slug": result["slug"], "url": result["share_url"]}, ensure_ascii=False)) diff --git a/tests/browser_use/test_failure_cause_review.py b/tests/browser_use/test_failure_cause_review.py index 8d886fd06..e9594b7d2 100644 --- a/tests/browser_use/test_failure_cause_review.py +++ b/tests/browser_use/test_failure_cause_review.py @@ -1,11 +1,16 @@ import pytest from harness.browser_use.failure_cause_review import ( + extract_failure_cause_review_cohort, localized_cause, validate_failure_cause_review, ) +MODEL_ID = "gpt-5.6-sol" +RESULT_ID = f"{MODEL_ID}-chromium-124" + + def _review(status: str = "verified") -> dict: cause = { "status": status, @@ -19,51 +24,108 @@ def _review(status: str = "verified") -> dict: if status != "verified": cause = {"status": status} return { - "schema": "lex_browser_use_failure_cause_review/1", - "reviewed_at": "2026-09-10", - "reviewer_class": "evidence analyst", - "bindings": {"attempts_sha256": "attempts-hash"}, - "coverage": { - "failed_results": 1, - "verified_results": 1 if status == "verified" else 0, + "schema": "lex_browser_use_layered_failure_cause_review/2", + "benchmark": "WebArena-Verified 1.2.3", + "cohorts": { + MODEL_ID: { + "bindings": {"attempts_sha256": "attempts-hash"}, + "coverage": { + "failed_results": 1, + "verified_results": 1 if status == "verified" else 0, + }, + "unresolved": [], + "results": {RESULT_ID: cause}, + "reviewer_class": "evidence analyst", + "reviewed_at": "2026-09-10", + "review_protocol": {"unit": "one logical result"}, + }, + "gpt-6-astra": { + "bindings": {"attempts_sha256": "astra-hash"}, + "coverage": {"failed_results": 0, "verified_results": 0}, + "unresolved": [], + "results": {}, + "reviewer_class": "evidence analyst", + "reviewed_at": "2026-09-11", + "review_protocol": {"unit": "one logical result"}, + }, }, - "unresolved": [], - "results": {"chromium-124": cause}, } -def test_unverified_cause_fields_remain_blank() -> None: - verified = validate_failure_cause_review( - _review("pending"), +def _validate(review: dict, *, require_complete: bool) -> dict: + return validate_failure_cause_review( + review, + model_id=MODEL_ID, attempts_sha256="attempts-hash", - failed_result_ids={"chromium-124"}, - require_complete=False, + failed_result_ids={RESULT_ID}, + require_complete=require_complete, ) - assert localized_cause(verified, "chromium-124") == ("", "") +def test_extracts_exact_model_cohort_with_its_metadata() -> None: + cohort = extract_failure_cause_review_cohort(_review(), MODEL_ID) + + assert cohort["bindings"] == {"attempts_sha256": "attempts-hash"} + assert cohort["coverage"] == {"failed_results": 1, "verified_results": 1} + assert cohort["results"] == _review()["cohorts"][MODEL_ID]["results"] + assert cohort["reviewer_class"] == "evidence analyst" + assert cohort["review_protocol"] == {"unit": "one logical result"} -def test_verified_cause_is_available_per_browser_and_task() -> None: - verified = validate_failure_cause_review( - _review(), - attempts_sha256="attempts-hash", - failed_result_ids={"chromium-124"}, - require_complete=True, - ) - assert localized_cause(verified, "chromium-124") == ( +def test_unverified_cause_fields_remain_blank() -> None: + verified = _validate(_review("pending"), require_complete=False) + + assert localized_cause(verified, RESULT_ID) == ("", "") + + +def test_verified_cause_is_available_per_model_browser_and_task() -> None: + verified = _validate(_review(), require_complete=True) + + assert localized_cause(verified, RESULT_ID) == ( "Agent 策略缺陷", "候选未核验。", ) - assert localized_cause(verified, "moli-124") == ("", "") + assert localized_cause(verified, "gpt-6-astra-chromium-124") == ("", "") + +def test_missing_model_cohort_is_blank_for_draft_and_blocks_release() -> None: + review = _review() -def test_stale_review_is_blank_for_draft_and_blocks_release() -> None: + assert ( + validate_failure_cause_review( + review, + model_id="missing-model", + attempts_sha256="missing-hash", + failed_result_ids={"missing-model-chromium-124"}, + require_complete=False, + ) + == {} + ) + with pytest.raises(ValueError, match="requires a failure-cause review"): + validate_failure_cause_review( + review, + model_id="missing-model", + attempts_sha256="missing-hash", + failed_result_ids={"missing-model-chromium-124"}, + require_complete=True, + ) + with pytest.raises(ValueError, match="requires a failure-cause review"): + validate_failure_cause_review( + review, + model_id="missing-model", + attempts_sha256="missing-hash", + failed_result_ids=set(), + require_complete=True, + ) + + +def test_stale_cohort_is_blank_for_draft_and_blocks_release() -> None: assert ( validate_failure_cause_review( _review(), + model_id=MODEL_ID, attempts_sha256="different-hash", - failed_result_ids={"chromium-124"}, + failed_result_ids={RESULT_ID}, require_complete=False, ) == {} @@ -71,8 +133,9 @@ def test_stale_review_is_blank_for_draft_and_blocks_release() -> None: with pytest.raises(ValueError, match="not bound"): validate_failure_cause_review( _review(), + model_id=MODEL_ID, attempts_sha256="different-hash", - failed_result_ids={"chromium-124"}, + failed_result_ids={RESULT_ID}, require_complete=True, ) @@ -81,29 +144,66 @@ def test_release_requires_every_failed_result_and_rejects_passes() -> None: with pytest.raises(ValueError, match="incomplete"): validate_failure_cause_review( _review(), + model_id=MODEL_ID, attempts_sha256="attempts-hash", - failed_result_ids={"chromium-124", "moli-124"}, + failed_result_ids={RESULT_ID, f"{MODEL_ID}-moli-124"}, require_complete=True, ) with pytest.raises(ValueError, match="non-failed"): validate_failure_cause_review( _review(), + model_id=MODEL_ID, attempts_sha256="attempts-hash", failed_result_ids=set(), require_complete=False, ) +def test_rejects_result_from_another_model_cohort() -> None: + review = _review() + review["cohorts"][MODEL_ID]["results"] = { + "gpt-6-astra-chromium-124": review["cohorts"][MODEL_ID]["results"][RESULT_ID] + } + + with pytest.raises(ValueError, match="outside its model cohort"): + _validate(review, require_complete=False) + + +def test_rejects_failed_result_inventory_from_another_model() -> None: + with pytest.raises(ValueError, match="inventory includes another model"): + validate_failure_cause_review( + _review(), + model_id=MODEL_ID, + attempts_sha256="attempts-hash", + failed_result_ids={"gpt-6-astra-chromium-124"}, + require_complete=False, + ) + + +def test_current_cohort_requires_reviewer_protocol_metadata() -> None: + review = _review() + del review["cohorts"][MODEL_ID]["review_protocol"] + + with pytest.raises(ValueError, match="invalid review protocol"): + _validate(review, require_complete=False) + + def test_unverified_record_cannot_carry_diagnosis_text() -> None: review = _review("pending") - review["results"]["chromium-124"]["reason"] = { + review["cohorts"][MODEL_ID]["results"][RESULT_ID]["reason"] = { "zh": "尚未证明。", "en": "Unproven.", } with pytest.raises(ValueError, match="must not contain"): - validate_failure_cause_review( - review, - attempts_sha256="attempts-hash", - failed_result_ids={"chromium-124"}, - require_complete=False, - ) + _validate(review, require_complete=False) + + +def test_schema_1_is_not_a_compatibility_input() -> None: + legacy = { + "schema": "lex_browser_use_failure_cause_review/1", + "bindings": {"attempts_sha256": "attempts-hash"}, + "results": {}, + } + + with pytest.raises(ValueError, match="unsupported"): + extract_failure_cause_review_cohort(legacy, MODEL_ID) diff --git a/tests/browser_use/test_publish.py b/tests/browser_use/test_publish.py new file mode 100644 index 000000000..e9ae2d547 --- /dev/null +++ b/tests/browser_use/test_publish.py @@ -0,0 +1,63 @@ +from __future__ import annotations + +import hashlib +import json +from pathlib import Path + +import scripts.publish_browser_use_artifact_hub as publisher + + +def _sha256(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def test_validate_package_requires_and_checks_every_evidence_root( + monkeypatch, tmp_path: Path +) -> None: + report = tmp_path / "report" + report.mkdir() + (report / "index.html").write_text("report", encoding="utf-8") + archive = tmp_path / "report.zip" + archive.write_bytes(b"archive") + profile = tmp_path / "profile.json" + profile.write_text("{}", encoding="utf-8") + evidence_paths = [] + evidence_files = {} + for model in ("sol", "astra"): + evidence = tmp_path / model + evidence.mkdir() + files = {} + for name in ("manifest.json", "attempts.jsonl"): + path = evidence / name + path.write_text(f"{model}:{name}", encoding="utf-8") + files[name] = _sha256(path) + evidence_paths.append(str(evidence)) + evidence_files[str(evidence)] = files + attestation = tmp_path / "attestation.json" + attestation.write_text( + json.dumps( + { + "schema": "lex_browser_use_publication_attestation/2", + "archive_sha256": _sha256(archive), + "report": str(report), + "report_tree_sha256": publisher.tree_sha256(report), + "profile_sha256": _sha256(profile), + "evidence": evidence_paths, + "evidence_files": evidence_files, + } + ), + encoding="utf-8", + ) + monkeypatch.setattr(publisher, "ARCHIVE", archive) + monkeypatch.setattr(publisher, "ATTESTATION", attestation) + monkeypatch.setattr(publisher, "PROFILE", profile) + + publisher.validate_package() + + (tmp_path / "astra" / "attempts.jsonl").write_text("changed", encoding="utf-8") + try: + publisher.validate_package() + except ValueError as error: + assert "evidence" in str(error) + else: + raise AssertionError("changed secondary evidence must invalidate the package") diff --git a/tests/browser_use/test_report.py b/tests/browser_use/test_report.py index 82b3ae89f..42a5be4a7 100644 --- a/tests/browser_use/test_report.py +++ b/tests/browser_use/test_report.py @@ -1,19 +1,23 @@ import json from pathlib import Path, PurePosixPath +import pytest + import harness.browser_use.report as report_module -from harness.browser_use.report_i18n import localize_html -from harness.browser_use.selection import load_pilot from harness.browser_use.report import ( _action_duration_cards, _best, _failure_diagnosis, + _load_evidence_bundles, _logical_results, _paired_summary, _same_task_comparison, _verification_html, build_report, ) +from harness.browser_use.report_i18n import localize_html +from harness.browser_use.selection import load_pilot +from scripts.check_browser_use_report import _check_execution_artifacts, _check_public_privacy def test_best_marks_only_one_visibly_distinct_winner() -> None: @@ -51,11 +55,7 @@ def test_result_verification_is_readable_and_does_not_duplicate_raw_panels() -> "expected": {"status": "success", "retrieved_data": [1, 2]}, "actual": {"status": "success", "retrieved_data": [1]}, "assertions": [ - { - "assertion_msgs": [ - "Array is missing 1 expected element(s). Matched (1/2)" - ] - } + {"assertion_msgs": ["Array is missing 1 expected element(s). Matched (1/2)"]} ], } ] @@ -82,11 +82,11 @@ def test_paired_summary_uses_final_logical_results() -> None: {"engine": "moli", "task_id": 2, "attempt": 1, "score": 1}, ] - summary = _paired_summary(_logical_results(rows)) + summary = _paired_summary(_logical_results(rows))["gpt-5.6-sol"]["comparisons"]["moli"] assert summary["paired"] == 2 assert summary["both_pass"] == 1 - assert summary["moli_only"] == 1 + assert summary["comparison_only"] == 1 assert summary["mcnemar_exact_p"] == 1.0 @@ -96,6 +96,7 @@ def test_comparison_components_scale_to_three_browsers(monkeypatch) -> None: "ENGINE_LABELS", {"chromium": "Chromium", "moli": "Moli", "third": "Third Browser"}, ) + monkeypatch.setattr(report_module, "BASELINE_ENGINE", "chromium") tasks = [ { "task_id": 1, @@ -123,7 +124,7 @@ def test_comparison_components_scale_to_three_browsers(monkeypatch) -> None: comparison = _same_task_comparison(tasks, attempts) actions = _action_duration_cards(attempts) - assert comparison.count('data-compare-engine') == 3 + assert comparison.count("data-compare-engine") == 3 assert 'data-pass="chromium third"' in comparison assert 'data-fail="moli"' in comparison assert "Third Browser" in comparison @@ -188,10 +189,40 @@ def test_logical_results_aggregate_browser_tool_wall_time(tmp_path: Path) -> Non assert result["browser_tool_count_by_action"] == {"click": 2} -def test_report_publishes_one_final_logical_result_and_aggregates_cost(tmp_path: Path) -> None: +def _use_single_model_profile(monkeypatch) -> None: + real_read_json = report_module._read_json + + def read_json(path: Path) -> dict: + value = real_read_json(path) + if path == Path("evaluation/browser-use/current-report.json"): + return { + **value, + "models": ["gpt-5.6-sol"], + "model_labels": {"gpt-5.6-sol": "GPT-5.6 Sol"}, + } + return value + + monkeypatch.setattr(report_module, "_read_json", read_json) + monkeypatch.setattr( + report_module, + "load_failure_cause_review", + lambda: { + "schema": "lex_browser_use_layered_failure_cause_review/2", + "cohorts": {}, + }, + ) + + +def test_report_publishes_one_final_logical_result_and_aggregates_cost( + tmp_path: Path, monkeypatch +) -> None: + _use_single_model_profile(monkeypatch) evidence = tmp_path / "evidence" output = tmp_path / "report" evidence.mkdir() + (evidence / "manifest.json").write_text( + json.dumps({"protocol": {"model": "gpt-5.6-sol"}}), encoding="utf-8" + ) rows = [ { "engine": "chromium", @@ -200,6 +231,7 @@ def test_report_publishes_one_final_logical_result_and_aggregates_cost(tmp_path: "attempt": 1, "score": 0, "duration_ms": 1000, + "model_usage": {"total_tokens": 100}, }, { "engine": "chromium", @@ -208,6 +240,7 @@ def test_report_publishes_one_final_logical_result_and_aggregates_cost(tmp_path: "attempt": 2, "score": 1, "duration_ms": 900, + "model_usage": {"total_tokens": 200}, }, { "engine": "moli", @@ -216,8 +249,47 @@ def test_report_publishes_one_final_logical_result_and_aggregates_cost(tmp_path: "attempt": 1, "score": 1, "duration_ms": 800, + "model_usage": {"total_tokens": 80}, }, ] + for row in rows: + execution_dir = evidence / f"{row['engine']}-{row['attempt']}" + execution_dir.mkdir() + paths = {} + for label, filename, payload in ( + ("response", "response.json", {"execution": row["attempt"]}), + ("evaluation", "evaluation.json", {"score": row["score"]}), + ( + "events", + "events.json", + [ + { + "type": "tool_execution_start", + "data": { + "tool_call_id": str(row["attempt"]), + "tool_name": "browser", + "arguments": {"action": "click"}, + }, + }, + { + "type": "tool_execution_end", + "data": { + "tool_call_id": str(row["attempt"]), + "status": "success", + "execution_time": row["attempt"] / 10, + }, + }, + ], + ), + ): + path = execution_dir / filename + path.write_text(json.dumps(payload), encoding="utf-8") + paths[label] = str(path) + for label, filename in (("network_har", "network.har"), ("agent_trace", "trace.zip")): + path = execution_dir / filename + path.write_bytes(f"private-{row['attempt']}".encode()) + paths[label] = str(path) + row["paths"] = paths (evidence / "attempts.jsonl").write_text( "\n".join(json.dumps(row) for row in rows) + "\n", encoding="utf-8" ) @@ -229,11 +301,12 @@ def test_report_publishes_one_final_logical_result_and_aggregates_cost(tmp_path: text = report.read_text(encoding="utf-8") assert "1/1" in text data = json.loads((output / "report-data.json").read_text(encoding="utf-8")) - assert data["summary"]["chromium"]["score"] == 1 - chromium_summary = data["summary"]["chromium"] + assert data["summary"]["gpt-5.6-sol"]["chromium"]["score"] == 1 + chromium_summary = data["summary"]["gpt-5.6-sol"]["chromium"] assert chromium_summary["mean_duration_ms"] == 1900 assert chromium_summary["p50_duration_ms"] == 1900 assert chromium_summary["p95_duration_ms"] == 1900 + assert chromium_summary["mean_tokens"] == 300 task = (output / "datasets/44.html").read_text(encoding="utf-8") assert "Ground truth · 官方验收合同" in task assert "没有唯一参考点击路径" in task @@ -241,7 +314,7 @@ def test_report_publishes_one_final_logical_result_and_aggregates_cost(tmp_path: assert "最终结果对照" not in task dataset = (output / "datasets/index.html").read_text(encoding="utf-8") assert "可完成性证据" not in dataset - assert 'data-result=' not in dataset + assert "data-result=" not in dataset analysis = (output / "analysis/index.html").read_text(encoding="utf-8") for expected in ("均值", "P50", "P95", "comparison-winner"): assert expected in analysis @@ -260,12 +333,13 @@ def test_report_publishes_one_final_logical_result_and_aggregates_cost(tmp_path: assert "核验与修订记录" in review assert "核验轮次" in review assert "逐结果覆盖" not in review - attempt = (output / "results/chromium-44.html").read_text(encoding="utf-8") + result_id = "gpt-5.6-sol-chromium-44" + attempt = (output / f"results/{result_id}.html").read_text(encoding="utf-8") for expected in ( "任务要求与验收结果", "CowAgent 自主执行轨迹", "运行、日志与证据", - 'href="results/chromium-44.html#trajectory"', + f'href="results/{result_id}.html#trajectory"', "attempt-summary", "查看任务与验收", ): @@ -273,26 +347,67 @@ def test_report_publishes_one_final_logical_result_and_aggregates_cost(tmp_path: assert "没有 evaluator 明细" in attempt assert "Ground truth 与实际最终回答" not in attempt assert "官方 evaluator 逐规则判定" not in attempt - for artifact in ( - "agent-response.json", - "evaluation.json", - "agent-events.json", - "trajectory.json", - "evidence-metadata.json", - ): - assert (output / "artifacts/chromium-44" / artifact).is_file() - assert "诊断重试" not in "".join( - path.read_text(encoding="utf-8") for path in output.rglob("*.html") + for execution in (1, 2): + artifact = output / f"artifacts/{result_id}/executions/{execution}/execution-evidence.json" + assert artifact.is_file() + bundle = json.loads(artifact.read_text(encoding="utf-8")) + assert set(("agent_response", "evaluation", "agent_events", "trajectory")) <= set(bundle) + assert "包含 2 次物理执行" in attempt + assert "物理执行 1" in attempt + assert "物理执行 2 · 判分权威" in attempt + assert "SHA-256" in attempt + public_results = [ + json.loads(line) + for line in (output / "data/results.jsonl").read_text(encoding="utf-8").splitlines() + ] + evidence_index = json.loads((output / "data/evidence-index.json").read_text(encoding="utf-8")) + bundles = _load_evidence_bundles(evidence) + logical = _logical_results(bundles[0]["source_attempts"]) + _check_execution_artifacts(output, logical, public_results, evidence_index) + _check_public_privacy(output) + legitimate_route = output / "legitimate-route.json" + legitimate_route.write_text('{"href":"/users/sign_out"}', encoding="utf-8") + _check_public_privacy(output) + legitimate_route.unlink() + index_row = next(item for item in evidence_index["results"] if item["result_id"] == result_id) + assert index_row["execution_count"] == 2 + assert index_row["authoritative_execution"] == 2 + assert [item["duration_ms"] for item in index_row["executions"]] == [1000, 900] + assert [item["model_usage"]["total_tokens"] for item in index_row["executions"]] == [ + 100, + 200, + ] + assert [item["browser_tool_duration_ms"] for item in index_row["executions"]] == [ + 100, + 200, + ] + assert all( + artifact["sha256"] + for execution in index_row["executions"] + for artifact in execution["artifacts"].values() ) - formal_html = "".join( + private_trace = output / f"artifacts/{result_id}/executions/1/agent_trace.zip" + private_trace.write_bytes(b"private trace") + with pytest.raises(ValueError, match="private HAR or trace"): + _check_public_privacy(output) + with pytest.raises(ValueError, match="artifact tree differs"): + _check_execution_artifacts(output, logical, public_results, evidence_index) + private_trace.unlink() + missing_response = output / f"artifacts/{result_id}/executions/1/execution-evidence.json" + missing_response.unlink() + with pytest.raises(ValueError, match="missing physical execution artifact"): + _check_execution_artifacts(output, logical, public_results, evidence_index) + assert "诊断重试" not in "".join( path.read_text(encoding="utf-8") for path in output.rglob("*.html") ) + formal_html = "".join(path.read_text(encoding="utf-8") for path in output.rglob("*.html")) for speculative_label in ("证据支持的诊断", "共享链路诊断", "失败归因:"): assert speculative_label not in formal_html assert not list(output.rglob("*.svg")) -def test_report_renders_measured_runtime_identities(tmp_path: Path) -> None: +def test_report_renders_measured_runtime_identities(tmp_path: Path, monkeypatch) -> None: + _use_single_model_profile(monkeypatch) evidence = tmp_path / "evidence" output = tmp_path / "report" evidence.mkdir() @@ -305,7 +420,7 @@ def test_report_renders_measured_runtime_identities(tmp_path: Path) -> None: { "protocol": { "cowagent_commit": "abc123", - "model": "gpt-6-astra", + "model": "gpt-5.6-sol", "reasoning_effort": "low", }, "environment": { @@ -345,7 +460,7 @@ def test_report_renders_measured_runtime_identities(tmp_path: Path) -> None: for expected in ( "abc123", - "gpt-6-astra · reasoning=low", + "GPT-5.6 Sol · reasoning=low", "eval-checksum", "selection-hash", "Chrome/151", @@ -356,7 +471,7 @@ def test_report_renders_measured_runtime_identities(tmp_path: Path) -> None: assert report.is_file() assert (output / "assets/webarena-autonomous-evaluation-flow.png").is_file() home_text = report.read_text(encoding="utf-8") - assert "Chromium 与 Moli:Browser Use 能力实测" in home_text + assert "浏览器 Browser Use 能力对比" in home_text methodology_text = (output / "methodology/index.html").read_text(encoding="utf-8") assert "webarena-autonomous-evaluation-flow.png" in methodology_text assert "ContainerManager.start(reset=True)" in methodology_text @@ -381,15 +496,13 @@ def test_report_renders_measured_runtime_identities(tmp_path: Path) -> None: assert page.is_file() if name not in {"guide/article.html", "en/index.html"}: assert "切换主题" in page.read_text(encoding="utf-8") - expected_tasks = len( - load_pilot("evaluation/browser-use/webarena-verified-pilot.json").tasks - ) + expected_tasks = len(load_pilot("evaluation/browser-use/webarena-verified-pilot.json").tasks) assert len(list((output / "datasets").glob("[0-9]*.html"))) == expected_tasks assert (output / "guide/article.html").read_text(encoding="utf-8").find(" None: assert "规则 1 AgentResponseEvaluator" in diagnosis assert "Array is missing 1 expected element(s)" in diagnosis + + +def _write_model_evidence(path: Path, model: str, rows: list[dict]) -> None: + path.mkdir() + (path / "manifest.json").write_text( + json.dumps({"protocol": {"model": model}}), encoding="utf-8" + ) + (path / "attempts.jsonl").write_text( + "".join(json.dumps(row) + "\n" for row in rows), encoding="utf-8" + ) + + +def test_two_models_and_two_browsers_keep_distinct_logical_results( + tmp_path: Path, monkeypatch +) -> None: + monkeypatch.setattr(report_module, "MODEL_LABELS", {"model-a": "A", "model-b": "B"}) + monkeypatch.setattr(report_module, "ENGINE_LABELS", {"chromium": "Chromium", "moli": "Moli"}) + model_a, model_b = tmp_path / "a", tmp_path / "b" + rows = [ + {"engine": engine, "task_id": 44, "tier": "easy", "attempt": 1, "score": 1} + for engine in ("chromium", "moli") + ] + _write_model_evidence(model_a, "model-a", rows) + _write_model_evidence(model_b, "model-b", rows) + + bundles = _load_evidence_bundles([model_a, model_b]) + logical = [ + result for bundle in bundles for result in _logical_results(bundle["source_attempts"]) + ] + + assert len(logical) == 4 + assert len({report_module._attempt_id(row) for row in logical}) == 4 + assert set(report_module._primary_summary(logical)) == {"model-a", "model-b"} + + +def test_joint_report_builds_two_model_by_two_browser_matrix(tmp_path: Path, monkeypatch) -> None: + model_a, model_b = tmp_path / "a", tmp_path / "b" + rows = [ + { + "engine": engine, + "task_id": 44, + "tier": "easy", + "attempt": 1, + "score": score, + } + for engine, score in (("chromium", 1), ("moli", 0)) + ] + _write_model_evidence(model_a, "model-a", rows) + _write_model_evidence(model_b, "model-b", rows) + real_read_json = report_module._read_json + + def read_json(path: Path) -> dict: + if path == Path("evaluation/browser-use/current-report.json"): + return { + "engines": ["chromium", "moli"], + "engine_labels": {"chromium": "Chromium", "moli": "Moli"}, + "baseline_engine": "chromium", + "models": ["model-a", "model-b"], + "model_labels": {"model-a": "Model A", "model-b": "Model B"}, + } + return real_read_json(path) + + monkeypatch.setattr(report_module, "_read_json", read_json) + monkeypatch.setattr( + report_module, + "load_failure_cause_review", + lambda: { + "schema": "lex_browser_use_layered_failure_cause_review/2", + "cohorts": {}, + }, + ) + output = tmp_path / "report" + + build_report( + evidence=[model_a, model_b], + output=output, + selection_path=Path("evaluation/browser-use/webarena-verified-pilot.json"), + ) + + data = json.loads((output / "report-data.json").read_text(encoding="utf-8")) + assert set(data["summary"]) == {"model-a", "model-b"} + assert data["summary"]["model-a"]["chromium"]["completed"] == 1 + assert data["summary"]["model-b"]["moli"]["completed"] == 1 + assert len(list((output / "results").glob("*.html"))) == 4 + assert (output / "results/model-a-chromium-44.html").is_file() + assert (output / "results/model-b-chromium-44.html").is_file() + analysis = (output / "analysis/index.html").read_text(encoding="utf-8") + assert 'data-filter="model"' in analysis + assert 'data-model="model-a"' in analysis + assert 'data-model="model-b"' in analysis + + +def test_multi_model_evidence_fails_closed_when_cohort_is_missing( + tmp_path: Path, monkeypatch +) -> None: + monkeypatch.setattr(report_module, "MODEL_LABELS", {"model-a": "A", "model-b": "B"}) + model_a, model_b = tmp_path / "a", tmp_path / "b" + _write_model_evidence( + model_a, + "model-a", + [ + {"engine": engine, "task_id": 44, "attempt": 1, "score": 1} + for engine in ("chromium", "moli") + ], + ) + _write_model_evidence( + model_b, + "model-b", + [{"engine": "chromium", "task_id": 44, "attempt": 1, "score": 1}], + ) + + with pytest.raises(ValueError, match="model cohort differs"): + _load_evidence_bundles([model_a, model_b]) + + +def test_pairing_uses_dynamic_baseline_for_three_browsers(monkeypatch) -> None: + monkeypatch.setattr(report_module, "MODEL_LABELS", {"model-a": "A"}) + monkeypatch.setattr( + report_module, + "ENGINE_LABELS", + {"first": "First", "baseline": "Baseline", "third": "Third"}, + ) + monkeypatch.setattr(report_module, "BASELINE_ENGINE", "baseline") + rows = [ + { + "model": "model-a", + "engine": engine, + "task_id": task_id, + "attempt": 1, + "score": score, + } + for task_id, outcomes in ((1, (1, 0, 1)), (2, (0, 0, 1))) + for engine, score in zip(("first", "baseline", "third"), outcomes, strict=True) + ] + + comparisons = _paired_summary(_logical_results(rows))["model-a"]["comparisons"] + + assert set(comparisons) == {"first", "third"} + assert comparisons["first"]["baseline_engine"] == "baseline" + assert comparisons["first"]["comparison_only"] == 1 + assert comparisons["third"]["comparison_only"] == 2 diff --git a/tests/browser_use/test_report_i18n.py b/tests/browser_use/test_report_i18n.py new file mode 100644 index 000000000..407f06a0c --- /dev/null +++ b/tests/browser_use/test_report_i18n.py @@ -0,0 +1,91 @@ +import json +import re +from html.parser import HTMLParser +from pathlib import Path, PurePosixPath + +from harness.browser_use.report_i18n import localize_html, translate_text + + +class _VisibleText(HTMLParser): + def __init__(self) -> None: + super().__init__() + self.stack: list[str] = [] + self.values: list[str] = [] + + def handle_starttag(self, tag: str, attrs) -> None: + self.stack.append(tag) + + def handle_endtag(self, tag: str) -> None: + if tag in self.stack: + self.stack = self.stack[: len(self.stack) - 1 - self.stack[::-1].index(tag)] + + def handle_data(self, data: str) -> None: + if not any(tag in self.stack for tag in ("pre", "code", "script", "style")): + self.values.append(data) + + +def test_browser_use_change_records_have_complete_structured_english() -> None: + data = json.loads(Path("research/report-changes.json").read_text(encoding="utf-8")) + assert data["history_scope_i18n"]["en"] + for collection in ("browser_use_reviews", "browser_use_releases"): + for record in data[collection]: + english = record["i18n"]["en"] + for field in ("title", "summary") if collection.endswith("releases") else ( + "title", + "status", + "scope", + "findings", + "corrections", + "remaining", + ): + assert english[field] + assert not re.search(r"[\u4e00-\u9fff]", english[field]) + if collection.endswith("releases"): + assert len(english["changes"]) == len(record.get("changes") or []) + assert all(len(target) == len(source) == 4 for source, target in zip(record.get("changes") or [], english["changes"], strict=True)) + assert not re.search(r"[\u4e00-\u9fff]", json.dumps(english["changes"], ensure_ascii=False)) + + +def test_english_ui_is_localized_but_measured_text_is_preserved() -> None: + source = ( + '

    Browser Use 联合多模型能力实测

    ' + '

    查看本题运行结果

    评测集页面保持为冻结输入;实测输出、判分和轨迹在结果页查看。

    ' + '
    程序自动定位:核验根因子类:核验原因:
    ' + '
    {"raw": "原始测量证据"}

    创建一个命名明确的空仓库

    ' + ) + localized = localize_html(source, PurePosixPath("results/example.html")) + parser = _VisibleText() + parser.feed(localized) + visible = " ".join(parser.values) + for leaked in ("联合多模型能力实测", "查看本题运行结果", "评测集页面保持", "程序自动定位", "核验根因子类", "核验原因"): + assert leaked not in visible + assert "原始测量证据" in localized + assert "创建一个命名明确的空仓库" in visible + + +def test_release_translation_comes_from_structured_record() -> None: + data = json.loads(Path("research/report-changes.json").read_text(encoding="utf-8")) + release = data["browser_use_releases"][0] + assert translate_text(release["title"]) == release["i18n"]["en"]["title"] + + +def test_all_release_and_review_copy_localizes_without_chinese() -> None: + data = json.loads(Path("research/report-changes.json").read_text(encoding="utf-8")) + values = [data["history_scope"]] + for record in data["browser_use_reviews"]: + values.extend( + record[field] + for field in ("title", "status", "scope", "findings", "corrections", "remaining") + ) + for record in data["browser_use_releases"]: + values.extend((record["title"], record["summary"])) + values.extend(cell for row in record.get("changes") or [] for cell in row) + + source = '' + "".join( + f"

    {value}

    " for value in values + ) + "" + localized = localize_html(source, PurePosixPath("changes/index.html")) + parser = _VisibleText() + parser.feed(localized) + + assert not re.search(r"[\u4e00-\u9fff]", " ".join(parser.values)) diff --git a/tests/browser_use/test_report_ui.mjs b/tests/browser_use/test_report_ui.mjs index e6271248a..4a158e786 100644 --- a/tests/browser_use/test_report_ui.mjs +++ b/tests/browser_use/test_report_ui.mjs @@ -11,15 +11,17 @@ assert.ok(match, 'browser-use report JavaScript must remain extractable'); const controls = [ {dataset: {filter: 'result'}, value: '', addEventListener() {}}, + {dataset: {filter: 'model'}, value: '', addEventListener() {}}, {dataset: {filter: 'engine'}, value: '', addEventListener() {}}, ]; const search = {value: '', addEventListener() {}}; const rows = [ - {dataset: {tier: 'hard', result: 'fail', engine: 'moli'}, textContent: 'missing item'}, - {dataset: {tier: 'easy', result: 'pass', engine: 'chromium'}, textContent: 'complete'}, + {dataset: {tier: 'hard', result: 'fail', model: 'model-a', engine: 'moli'}, textContent: 'missing item'}, + {dataset: {tier: 'easy', result: 'pass', model: 'model-a', engine: 'chromium'}, textContent: 'complete'}, ]; const count = {textContent: ''}; const browserControls = { + 'browser-model': {value: 'model-a', options: [{value: 'model-a'}, {value: 'model-b'}], addEventListener() {}}, 'browser-a': {value: 'chromium', options: [{value: 'chromium'}, {value: 'moli'}], addEventListener() {}}, 'browser-relation': {value: 'mixed', options: [{value: 'mixed'}, {value: 'all_fail'}], addEventListener() {}}, 'browser-kind': {value: '', options: [{value: ''}, {value: 'RETRIEVE'}], addEventListener() {}}, @@ -31,9 +33,9 @@ const compareChecks = [ {value: 'moli', checked: true, disabled: false, closest() { return comparisonLabel; }, addEventListener() {}}, ]; const comparisonRows = [ - {dataset: {pass: 'chromium', fail: 'moli', kind: 'RETRIEVE'}}, - {dataset: {pass: 'chromium moli', fail: '', kind: 'NAVIGATE'}}, - {dataset: {pass: '', fail: 'chromium moli', kind: 'MUTATE'}}, + {dataset: {model: 'model-a', pass: 'chromium', fail: 'moli', kind: 'RETRIEVE'}}, + {dataset: {model: 'model-a', pass: 'chromium moli', fail: '', kind: 'NAVIGATE'}}, + {dataset: {model: 'model-a', pass: '', fail: 'chromium moli', kind: 'MUTATE'}}, ]; const listeners = {}; const storage = new Map(); diff --git a/tests/browser_use/test_review.py b/tests/browser_use/test_review.py index 0da1d3da7..5e32cfddb 100644 --- a/tests/browser_use/test_review.py +++ b/tests/browser_use/test_review.py @@ -52,9 +52,10 @@ def _write_fixture( { "selection_sha256": "selection-sha", "protocol": { + "model": "test-model", "environment_prepare_receipt": "required" if require_prepare_receipt - else "legacy-unavailable" + else "legacy-unavailable", }, "environment": { "official_evaluator": { @@ -78,6 +79,7 @@ def test_review_verifies_every_artifact_and_binding(tmp_path: Path) -> None: assert review["disagreements"] == [] assert review["unresolved"] == [] assert review["attempts"][0]["outcome"] == "verified" + assert review["attempts"][0]["model"] == "test-model" assert set(review["attempts"][0]["artifacts"]) == { "network_har", "agent_trace", diff --git a/tests/browser_use/test_runner.py b/tests/browser_use/test_runner.py index 17e53f416..ba463d749 100644 --- a/tests/browser_use/test_runner.py +++ b/tests/browser_use/test_runner.py @@ -1,17 +1,27 @@ +import json +import multiprocessing +from pathlib import Path + import pytest from webarena_verified.types.task import WebArenaSite from harness.browser_use.runner import ( ENV_CTRL_READY_TIMEOUT_SECONDS, + PHYSICAL_EXECUTION_LOCK_NAMESPACE, QEMU_X86_64_HOST_PATH, SITE_AUTH_HEADERS, _allow_official_container_hostname, _configure_benchmark, _container_host_gateway, + _environment_prepare_receipt, _exhausted_agent_steps, _limit_cowagent_transport_retries, + _load_completed_attempts, _login_context, _max_steps_response, + _physical_execution_lock, + _physical_execution_lock_path, + _run_attempt, _scheduled_tasks, _site_container_manager, _site_runtime_compatibility, @@ -22,6 +32,12 @@ from harness.browser_use.selection import load_pilot +def _hold_physical_execution_lock(lock_path, acquired, release) -> None: + with _physical_execution_lock(Path(lock_path)): + acquired.set() + release.wait(timeout=5) + + def test_official_container_urls_and_header_auth() -> None: benchmark = _configure_benchmark() @@ -61,8 +77,7 @@ def test_agent_step_exhaustion_requires_a_final_tool_call() -> None: for turn in range(1, 41) ] zero_indexed = [ - {"type": "turn_end", "data": {"turn": turn, "has_tool_calls": True}} - for turn in range(40) + {"type": "turn_end", "data": {"turn": turn, "has_tool_calls": True}} for turn in range(40) ] completed = [ { @@ -135,9 +150,7 @@ class Service: def _submit(self, callback): return callback() - readiness_retries = _login_context( - Service(), (), object(), ["http://official.test/start"] - ) + readiness_retries = _login_context(Service(), (), object(), ["http://official.test/start"]) assert readiness_retries == 0 assert stale.closed is True @@ -262,6 +275,125 @@ def test_environment_control_wait_covers_slow_official_image_restore() -> None: assert ENV_CTRL_READY_TIMEOUT_SECONDS == 600 +def test_physical_execution_lock_path_is_user_global_not_evidence_scoped( + monkeypatch, tmp_path: Path +) -> None: + monkeypatch.setenv("BROWSER_EVAL_DATA_HOME", str(tmp_path)) + monkeypatch.setattr("harness.browser_use.runner.tempfile.gettempdir", lambda: "/tmp") + monkeypatch.setattr("harness.browser_use.runner.os.getuid", lambda: 1234) + + assert _physical_execution_lock_path() == ( + Path("/tmp") + / "browser-eval-1234" + / "locks" + / f"{PHYSICAL_EXECUTION_LOCK_NAMESPACE}.lock" + ) + + +def test_prepare_receipt_records_lock_wait_and_namespace_without_path() -> None: + receipt = _environment_prepare_receipt( + (WebArenaSite.REDDIT,), + lock_wait_ms=12.3456, + lock_namespace=PHYSICAL_EXECUTION_LOCK_NAMESPACE, + ) + + assert receipt["physical_execution_lock"] == { + "namespace": PHYSICAL_EXECUTION_LOCK_NAMESPACE, + "wait_ms": 12.346, + } + assert "path" not in receipt["physical_execution_lock"] + + +def test_physical_execution_lock_serializes_processes(tmp_path: Path) -> None: + context = multiprocessing.get_context("fork") + lock_path = tmp_path / "physical-execution.lock" + first_acquired = context.Event() + first_release = context.Event() + second_acquired = context.Event() + second_release = context.Event() + first = context.Process( + target=_hold_physical_execution_lock, + args=(str(lock_path), first_acquired, first_release), + ) + second = context.Process( + target=_hold_physical_execution_lock, + args=(str(lock_path), second_acquired, second_release), + ) + try: + first.start() + assert first_acquired.wait(timeout=2) + second.start() + assert not second_acquired.wait(timeout=0.2) + first_release.set() + assert second_acquired.wait(timeout=2) + finally: + first_release.set() + second_release.set() + first.join(timeout=2) + second.join(timeout=2) + if first.is_alive(): + first.terminate() + if second.is_alive(): + second.terminate() + + +def test_run_attempt_holds_lock_around_complete_physical_execution( + monkeypatch, tmp_path: Path +) -> None: + context = multiprocessing.get_context("fork") + lock_path = tmp_path / "physical-execution.lock" + competing_acquired = context.Event() + competing_release = context.Event() + competing = context.Process( + target=_hold_physical_execution_lock, + args=(str(lock_path), competing_acquired, competing_release), + ) + + def run_locked(**kwargs): + assert kwargs["lock_namespace"] == PHYSICAL_EXECUTION_LOCK_NAMESPACE + assert kwargs["lock_wait_ms"] >= 0 + competing.start() + assert not competing_acquired.wait(timeout=0.2) + return {"status": "complete"} + + monkeypatch.setattr("harness.browser_use.runner._run_attempt_locked", run_locked) + try: + assert _run_attempt(lock_path=lock_path) == {"status": "complete"} + assert competing_acquired.wait(timeout=2) + finally: + competing_release.set() + competing.join(timeout=2) + if competing.is_alive(): + competing.terminate() + + +def test_run_attempt_releases_lock_after_exception(monkeypatch, tmp_path: Path) -> None: + context = multiprocessing.get_context("fork") + lock_path = tmp_path / "physical-execution.lock" + + def fail_locked(**_kwargs): + raise RuntimeError("attempt failed") + + monkeypatch.setattr("harness.browser_use.runner._run_attempt_locked", fail_locked) + with pytest.raises(RuntimeError, match="attempt failed"): + _run_attempt(lock_path=lock_path) + + acquired = context.Event() + release = context.Event() + process = context.Process( + target=_hold_physical_execution_lock, + args=(str(lock_path), acquired, release), + ) + try: + process.start() + assert acquired.wait(timeout=2) + finally: + release.set() + process.join(timeout=2) + if process.is_alive(): + process.terminate() + + def test_arm_php_sites_mount_targeted_qemu_compatibility(monkeypatch) -> None: class Client: @staticmethod @@ -341,3 +473,27 @@ def _validate_url_safe(url): tool._validate_url_safe("http://host.docker.internal:7780/admin") tool._validate_url_safe("https://example.com") assert checked == ["https://example.com"] + + +def test_resume_key_includes_and_validates_model(tmp_path: Path) -> None: + attempts = tmp_path / "attempts.jsonl" + attempts.write_text( + json.dumps({"engine": "chromium", "task_id": 44, "attempt": 1}) + "\n", + encoding="utf-8", + ) + + completed = _load_completed_attempts(attempts, "model-a") + + assert set(completed) == {("model-a", "chromium", 44, 1)} + assert completed[("model-a", "chromium", 44, 1)]["model"] == "model-a" + + +def test_resume_rejects_attempt_from_another_model(tmp_path: Path) -> None: + attempts = tmp_path / "attempts.jsonl" + attempts.write_text( + json.dumps({"model": "model-b", "engine": "chromium", "task_id": 44, "attempt": 1}) + "\n", + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="attempt model differs"): + _load_completed_attempts(attempts, "model-a") From fbd73c8b1c69029850e7da104bab092a0af5bb23 Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 11 Sep 2026 17:37:15 +0800 Subject: [PATCH 062/127] Record published Browser Use report version --- evaluation/browser-use/current-report.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evaluation/browser-use/current-report.json b/evaluation/browser-use/current-report.json index 2c3f153ac..a4cd6905f 100644 --- a/evaluation/browser-use/current-report.json +++ b/evaluation/browser-use/current-report.json @@ -57,7 +57,7 @@ "base_url": "https://artifact-hub.dev.lexmount.net", "site_slug": "28OH4YZkUYF1", "share_url": "https://artifact-hub.dev.lexmount.net/v/5Vhnhdc_f8FEM2RzctQm-ChRlXUf174Y", - "expected_version": "ver_f46875f6e36f429a9826cb49424544d6", + "expected_version": "ver_01072f33640a4bd1add79e5a00e9e678", "state": "verified" } } From 7500c21a201b63c8a9ca096232fb323df667d596 Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 11 Sep 2026 18:28:53 +0800 Subject: [PATCH 063/127] Redesign browser use report and publish workflow --- AGENTS.md | 3 + Makefile | 4 +- README.md | 2 + evaluation/browser-use/current-report.json | 8 +- examples/browser_use/README.md | 6 + examples/browser_use/run.sh | 7 + harness/browser_use/report.py | 281 +++++++++++++------- harness/browser_use/report_i18n.py | 217 ++++++++++++++- harness/browser_use/runner.py | 2 +- research/report-changes.json | 51 +++- scripts/check_browser_use_report.py | 30 ++- scripts/check_browser_use_visual.py | 5 +- scripts/check_report_links.py | 16 +- scripts/package_browser_use_artifact_hub.py | 31 ++- scripts/publish_browser_use_artifact_hub.py | 31 ++- tests/browser_use/test_publish.py | 28 ++ tests/browser_use/test_report.py | 39 ++- tests/test_runtime_entrypoints.py | 13 + 18 files changed, 635 insertions(+), 139 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e90c981e3..47247b679 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -17,7 +17,10 @@ - Browser Use reports expose one manifest-authoritative logical result per browser and task. Dataset routes contain only frozen task definitions and acceptance contracts; result routes own verdicts, timings and trajectories. Keep one filterable result inventory instead of adding duplicate success/failure tables. - Keep automatic evaluator failure location separate from reviewed root cause. A root-cause subtype and reason must come from a per-browser, per-task review bound to the current attempts hash; render both fields blank while that result is unverified, and require complete verified coverage before a formal release when the current profile enables the gate. - Generate browser rows and comparisons from the participant registry. The same-task explorer uses one baseline browser, multi-select comparison browsers and explicit outcome relations, with browser labels linking to full results; never add one fixed table column per future browser. List Agent, model, prompt/tool contract, resources, environment and evaluator as shared controls. Standard summaries use mean, P50 and P95; mark only unique winners on directionally comparable metrics. For action latency, compare per-call latency and keep unequal-workload totals and call counts neutral. +- Put a core-metrics comparison table near the start of every report home page, before secondary diagnosis or interpretation. Prefer tables over repeated cards for dense categorical summaries. Generate its rows and columns from the participant registry rather than assuming a fixed participant count. +- Use reader-facing labels throughout reports. Do not publish unexplained internal terms or shorthand; Chinese copy must be understandable to a general reader without benchmark implementation context. State the population, denominator, and outcome relation where a compact label would otherwise be ambiguous. - Result-detail summaries contain only the verdict, concise failed-rule labels and short scalar facts. Full evaluator messages, expected/actual payloads, traces and logs belong in dedicated evidence sections, never narrow KPI cards. - Split environment preparation, browser-session setup, Agent run, BrowserTool wall time, evidence finalization and evaluator time at collection. Publish the self-hosted deployment topology and image identity; claim per-execution reset/readiness only when a preparation receipt binds reset, readiness and application health. - Formal report prose is reader-facing: no prompts, reasoning, internal work narration, conversational rebuttals or references to parallel sessions. Review pages contain review rounds and corrections, not per-case audit tables; release notes contain reader-visible effects. - Before release, inspect every core route class in Chinese and English at desktop and narrow widths in both themes. Prevent vertical table labels, clipped controls and unreadable evidence; use ImageGen raster assets or Mermaid rather than SVG for Browser Use diagrams. +- Keep routine report checks fast and deterministic; they cover structure, evidence, links, terminology, and static readability guards without launching a browser. Run the separate full release check before packaging or publishing; it adds the complete cross-language, cross-theme, desktop and narrow-width visual gate. diff --git a/Makefile b/Makefile index a446ca601..ba7fc92a2 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ UV ?= uv PY := $(UV) run --frozen python -.PHONY: setup runtime-check naming-check test test-python test-ui fixture-smoke ci report report-check browser-use-review browser-use-report browser-use-report-check browser-use-visual-check +.PHONY: setup runtime-check naming-check test test-python test-ui fixture-smoke ci report report-check browser-use-review browser-use-report browser-use-report-check browser-use-visual-check browser-use-release-check setup: $(UV) sync --frozen --all-extras @@ -55,4 +55,6 @@ browser-use-report-check: runtime-check @test -n "$(EVIDENCE)" || (echo 'EVIDENCE="/absolute/path [/absolute/path...]" is required' >&2; exit 2) $(PY) scripts/check_browser_use_report.py reports/browser-use $(foreach path,$(EVIDENCE),--evidence "$(path)") --require-complete $(PY) scripts/check_report_links.py reports/browser-use + +browser-use-release-check: browser-use-report-check $(MAKE) browser-use-visual-check diff --git a/README.md b/README.md index 66b5d8e46..fcfc6d7ba 100644 --- a/README.md +++ b/README.md @@ -63,6 +63,8 @@ directory, and the report builder merges those immutable cohorts in memory. make browser-use-review EVIDENCE="$HOME/.browser-eval/browser-use/current" make browser-use-report EVIDENCE="$HOME/.browser-eval/browser-use/current" make browser-use-report-check EVIDENCE="$HOME/.browser-eval/browser-use/current" +# Before packaging or publishing; adds full responsive, theme, and language visual QA. +make browser-use-release-check EVIDENCE="$HOME/.browser-eval/browser-use/current" ``` For a joint report, repeat `--evidence` once per model cohort; never concatenate diff --git a/evaluation/browser-use/current-report.json b/evaluation/browser-use/current-report.json index a4cd6905f..867c024e0 100644 --- a/evaluation/browser-use/current-report.json +++ b/evaluation/browser-use/current-report.json @@ -54,10 +54,10 @@ 699 ], "publication": { - "base_url": "https://artifact-hub.dev.lexmount.net", - "site_slug": "28OH4YZkUYF1", - "share_url": "https://artifact-hub.dev.lexmount.net/v/5Vhnhdc_f8FEM2RzctQm-ChRlXUf174Y", - "expected_version": "ver_01072f33640a4bd1add79e5a00e9e678", + "base_url": "https://artifact-hub.app.lexmount.com", + "site_slug": "7l77EeXPRfml", + "share_url": "https://artifact-hub.app.lexmount.com/v/Ghn1BR6PCy-njsCMqCe2LRIJ-eyJEJoa", + "expected_version": "ver_3edc1c6856cc4d26960a3bde9ce6fecc", "state": "verified" } } diff --git a/examples/browser_use/README.md b/examples/browser_use/README.md index 6f0446035..e436bf234 100644 --- a/examples/browser_use/README.md +++ b/examples/browser_use/README.md @@ -64,3 +64,9 @@ building or publishing the report: make browser-use-review EVIDENCE="$BROWSER_USE_OUTPUT_DIR" make browser-use-report EVIDENCE="$BROWSER_USE_OUTPUT_DIR" make browser-use-report-check EVIDENCE="$BROWSER_USE_OUTPUT_DIR" + +The report check is the routine fast gate and does not launch a browser. Before +packaging or publishing, run the full release gate, which additionally renders +every representative route in both languages, themes, and viewport sizes: + + make browser-use-release-check EVIDENCE="$BROWSER_USE_OUTPUT_DIR" diff --git a/examples/browser_use/run.sh b/examples/browser_use/run.sh index d39e50527..92e84670f 100755 --- a/examples/browser_use/run.sh +++ b/examples/browser_use/run.sh @@ -4,12 +4,19 @@ set -euo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" DATA_HOME="${BROWSER_EVAL_DATA_HOME:-${HOME}/.browser-eval}" OUTPUT="${BROWSER_USE_OUTPUT_DIR:-${DATA_HOME}/browser-use/current}" +MODEL="${BROWSER_USE_MODEL:-}" + +if [[ -z "${MODEL}" ]]; then + echo "BROWSER_USE_MODEL is required (for example: gpt-6-astra)" >&2 + exit 2 +fi cd "${ROOT}" uv run --frozen --extra browser-use-eval python -m harness.browser_use.runner \ --repo-root "${ROOT}" \ --selection "${ROOT}/evaluation/browser-use/webarena-verified-pilot.json" \ --output "${OUTPUT}" \ + --model "${MODEL}" \ "$@" echo "Evidence: ${OUTPUT}" diff --git a/harness/browser_use/report.py b/harness/browser_use/report.py index 7230e0f32..1aef55d9d 100644 --- a/harness/browser_use/report.py +++ b/harness/browser_use/report.py @@ -711,6 +711,101 @@ def _task_family_summary( return result +TASK_TYPE_COPY = { + "RETRIEVE": ("信息提取", "从页面读取、筛选或汇总信息,不修改站点状态"), + "NAVIGATE": ("页面导航", "打开符合指定条件的页面或列表"), + "MUTATE": ("页面操作", "提交表单、修改记录、结账等会改变站点状态的任务"), +} + + +def _task_family_table(families: list[dict[str, Any]]) -> str: + headers = ["任务类型", "任务数", "测什么"] + [ + f"{MODEL_LABELS[model]} · {ENGINE_LABELS[engine]}" + for model in MODEL_LABELS + for engine in ENGINE_LABELS + ] + rows: list[list[Any]] = [] + for family in families: + kind = str(family["kind"]) + label, description = TASK_TYPE_COPY[kind] + rows.append( + [ + Safe( + f'{_e(label)}' + f'{_e(kind)}' + ), + family["count"], + description, + *[ + f"{family['scores'][model][engine]}/{family['count']}" + for model in MODEL_LABELS + for engine in ENGINE_LABELS + ], + ] + ) + return _table(headers, rows, table_id="task-family-table", classes="wide-data-table") + + +def _failure_cause_table(verified_causes: dict[str, dict[str, Any]]) -> str: + groups = { + "agent": ("Agent 决策或操作", "检索不完整、候选选择错误、走错页面、字段遗漏,或没有在步骤上限内完成。"), + "benchmark": ("任务或 evaluator", "任务要求与验收条件冲突,或 evaluator 的路由、表单和实现规则有缺陷。"), + "site": ("测试网站", "自托管网站的依赖、会话或保存接口失败。"), + "evidence": ("浏览器证据记录", "页面操作可能成功,但网络记录缺少响应正文或 Referer,evaluator 无法确认。"), + "environment": ("共享测试环境", "并发运行改写了同一网站状态;运行器现已串行化,历史结果仍按原证据保留。"), + "mixed": ("多项原因", "同一结果同时包含 Agent 遗漏和冻结参考状态差异。"), + } + + def group_for(code: str) -> str: + for prefix in ("agent", "benchmark", "site", "environment", "mixed"): + if code.startswith(prefix + "_"): + return prefix + return "evidence" + + counts: dict[str, Counter[str]] = defaultdict(Counter) + for diagnosis in verified_causes.values(): + subcategory = diagnosis.get("subcategory") or {} + code = str(subcategory.get("code") or "evidence_capture") + label = str(subcategory.get("zh") or "未分类") + counts[group_for(code)][label] += 1 + + rows = [] + for key in ("agent", "benchmark", "site", "evidence", "environment", "mixed"): + total = sum(counts[key].values()) + if not total: + continue + details = ";".join( + f"{label} {count}" + for label, count in sorted(counts[key].items(), key=lambda item: (-item[1], item[0])) + ) + rows.append([groups[key][0], total, details, groups[key][1]]) + return _table( + ["原因所在", "失败结果数", "具体分类", "说明"], + rows, + table_id="failure-cause-table", + classes="wide-data-table", + ) + + +def _headline_findings(summary: dict[str, dict[str, dict[str, Any]]]) -> str: + lines = [] + engines = list(ENGINE_LABELS) + for model, model_summary in summary.items(): + results = [ + f"{ENGINE_LABELS[engine]} {model_summary[engine]['passed']}/{model_summary[engine]['completed']}" + f"({_pct(model_summary[engine]['score'])})" + for engine in engines + ] + top = max(item["passed"] for item in model_summary.values()) + winner_ids = [engine for engine in engines if model_summary[engine]["passed"] == top] + outcome = "通过数相同" if len(winner_ids) > 1 else f"{ENGINE_LABELS[winner_ids[0]]} 多通过" + if len(engines) == 2 and len(winner_ids) == 1: + loser = next(engine for engine in engines if engine not in winner_ids) + outcome += f" {top - model_summary[loser]['passed']} 题" + lines.append(f"
  • {_e(MODEL_LABELS[model])}:{_e(','.join(results))};{_e(outcome)}。
  • ") + return '
      ' + "".join(lines) + "
    " + + def _adoption_top3(evidence: dict[str, Any]) -> str: families = sorted( (item for item in evidence.get("benchmark_families") or [] if item.get("interaction")), @@ -1081,6 +1176,7 @@ def _summary_comparison_rows(summary: dict[str, dict[str, dict[str, Any]]]) -> l .comparison-winner{color:var(--good);font-weight:850}.winner-mark{display:inline-grid;place-items:center;min-width:22px;height:22px;padding:0 5px;margin-left:7px;border-radius:999px;background:var(--good);color:white;font-size:11px;line-height:1}.winner-cell{position:relative;padding:9px 10px;border:1px solid color-mix(in srgb,var(--good) 42%,var(--line));border-radius:10px;background:color-mix(in srgb,var(--good) 9%,var(--panel))}.metric-stack{display:grid;gap:5px;min-width:150px}.metric-stack>span{display:flex;align-items:center;justify-content:space-between;gap:12px;white-space:nowrap}.metric-stack>span>b{font-size:12px;color:var(--muted)}.review-round{border:1px solid var(--line);border-radius:16px;background:var(--panel);padding:22px;margin:16px 0}.review-round header{display:flex;align-items:start;justify-content:space-between;gap:18px}.review-round h3{margin:5px 0}.review-status{flex:0 0 auto;padding:5px 10px;border-radius:999px;background:var(--wash);font-size:12px;font-weight:800}.review-fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;margin-top:16px}.review-fields>div{border-top:1px solid var(--line);padding-top:12px}.review-fields b{display:block;margin-bottom:5px}.review-fields p{margin:0} .browser-comparison{border:1px solid var(--line);border-radius:18px;background:var(--panel);padding:24px;box-shadow:0 12px 32px rgba(25,45,70,.06)}.comparison-heading{display:flex;align-items:start;justify-content:space-between;gap:24px;margin-bottom:22px}.comparison-heading h3{margin:0 0 6px;font-size:22px}.comparison-heading p{margin:0;color:var(--muted);line-height:1.65}.comparison-controls{display:grid;grid-template-columns:minmax(170px,1fr) minmax(240px,1.4fr) minmax(170px,1fr);gap:14px}.comparison-controls label{display:grid;gap:6px;font-weight:750}.comparison-controls select{width:100%;padding:11px 12px;border:1px solid var(--line);border-radius:10px;background:var(--bg);color:var(--ink);font:inherit}.comparison-browsers{border:0;padding:0;margin:20px 0 0}.comparison-browsers legend{font-weight:750;margin-bottom:10px}.browser-checks{display:flex;gap:8px;flex-wrap:wrap}.browser-checks label{display:flex;align-items:center;gap:7px;padding:8px 11px;border:1px solid var(--line);border-radius:999px;background:var(--bg);cursor:pointer}.browser-checks label.disabled{opacity:.45;cursor:not-allowed}.browser-checks input{accent-color:var(--accent)}.comparison-result{margin-top:20px}.table-wrap .comparison-table{min-width:720px;table-layout:fixed}.comparison-table th:first-child,.comparison-table td:first-child{width:48%}.comparison-table th:nth-child(2),.comparison-table td:nth-child(2),.comparison-table th:nth-child(3),.comparison-table td:nth-child(3){width:26%}.task-cell a{font-weight:750}.task-meta{display:flex;align-items:center;gap:7px;flex-wrap:wrap;margin-top:7px}.track-badge{display:inline-block;padding:3px 8px;border-radius:999px;background:var(--panel2);color:var(--muted);font-size:12px;white-space:nowrap}.outcome-list{display:flex;gap:6px;flex-wrap:wrap}.outcome-pill{display:inline-block;padding:5px 8px;border-radius:8px;font-size:13px;text-decoration:none}.outcome-pill.pass{background:var(--success-bg);color:var(--good)}.outcome-pill.fail{background:var(--failure-bg);color:var(--bad)} .action-card-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.action-card{border:1px solid var(--line);border-radius:16px;background:var(--panel);padding:18px;min-width:0}.action-card>header{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:14px}.action-card>header code{font-size:16px;font-weight:800}.action-card>header span{font-size:12px;color:var(--muted)}.action-browser-row{padding:12px 0;border-top:1px solid var(--line)}.action-browser-head{display:flex;align-items:center;justify-content:space-between;gap:12px}.action-bar{height:7px;border-radius:999px;background:var(--panel2);overflow:hidden;margin:8px 0}.action-bar i{display:block;height:100%;border-radius:inherit;background:var(--accent)}.action-browser-row.action-winner .action-bar i{background:var(--good)}.action-browser-meta{font-size:12px;color:var(--muted)} +.finding-list{display:grid;gap:8px;margin:0 0 20px;padding:16px 20px 16px 38px;border-left:4px solid var(--accent);border-radius:0 12px 12px 0;background:var(--wash)}.finding-list li{padding-left:4px}.table-kicker{display:block;color:var(--muted);font:700 11px/1.4 ui-monospace,monospace;letter-spacing:.04em;margin-top:3px}.core-comparison-table{min-width:1040px}.core-comparison-table td:nth-child(1),.core-comparison-table td:nth-child(2){font-weight:750}.pair-outcome-table{min-width:900px}.wide-data-table td{line-height:1.65}.report-entry-links{margin-top:18px}.report-entry-links a{font-weight:750}.source-note a{margin-left:5px} .attempt-summary{display:grid;grid-template-columns:minmax(270px,1.4fr) repeat(4,minmax(135px,.7fr));gap:12px;align-items:stretch}.attempt-verdict{padding:20px 22px;border:1px solid var(--line);border-left:5px solid var(--good);border-radius:12px;background:var(--success-bg)}.attempt-verdict.failure{border-left-color:var(--bad);background:var(--failure-bg)}.attempt-verdict .status-line{display:block;font-size:24px;font-weight:850;margin-bottom:6px}.attempt-verdict p{margin:0;line-height:1.65}.attempt-jumps{display:flex;gap:14px;flex-wrap:wrap;margin-top:14px;font-weight:750}.attempt-fact{padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--panel);overflow-wrap:anywhere}.attempt-fact b{display:block;color:var(--muted);font-size:12px;margin-bottom:5px}.attempt-secondary{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-top:12px} .task-goal{padding:22px 24px;border-radius:16px;background:var(--wash);border:1px solid var(--line);margin-bottom:18px}.task-goal span,.rule-index{display:block;color:var(--accent);font-size:12px;font-weight:850;letter-spacing:.08em;text-transform:uppercase}.task-goal p{font-size:20px;font-weight:750;line-height:1.6;margin:7px 0}.task-goal small{color:var(--muted)}.verification-list{display:grid;gap:14px}.verification-rule{padding:20px 22px;border:1px solid var(--line);border-left:5px solid var(--good);border-radius:14px;background:var(--panel);min-width:0}.verification-rule.rule-fail{border-left-color:var(--bad)}.verification-rule>header{display:flex;align-items:start;justify-content:space-between;gap:18px}.verification-rule h3{margin:4px 0 0;font-size:19px}.rule-verdict{flex:0 0 auto;padding:5px 10px;border-radius:999px;font-size:12px;font-weight:850}.rule-verdict.pass{background:var(--success-bg);color:var(--good)}.rule-verdict.fail{background:var(--failure-bg);color:var(--bad)}.rule-purpose{color:var(--muted);margin:12px 0 6px}.rule-summary{font-size:17px;font-weight:750;margin:0 0 14px}.verification-rule details{border-top:1px solid var(--line);padding-top:12px;margin-top:10px}.verification-rule summary{cursor:pointer;font-weight:750}.compact-evidence{margin-top:12px}.compact-evidence .json-view{max-height:360px} .page-main{overflow-x:clip} @@ -1097,7 +1193,7 @@ def _summary_comparison_rows(summary: dict[str, dict[str, dict[str, Any]]]) -> l ("analysis/", "结果分析", "analysis"), ("review/", "核验记录", "review"), ("guide/", "报告解读", "guide"), - ("adoption/", "参评浏览器", "adoption"), + ("adoption/", "被测浏览器", "adoption"), ("reproduce/", "环境与复现", "reproduce"), ("changes/", "变更记录", "changes"), ("accounts/", "账号与安装", "accounts"), @@ -1502,46 +1598,57 @@ def _article_body( ] if failed_result_count: cause_section = ( - _table( - ["逻辑结果", "根因子类", "已核验根因"], + _failure_cause_table(verified_causes) + + '
    查看每个失败结果的核验结论' + + _table( + ["结果编号", "原因分类", "核验结论"], cause_rows, - caption="表 2|根因核验不修改官方分数;修复后必须产生新的执行证据。", + caption="人工核验用于解释官方失败结果,不修改分数。修复后需要重新运行才能产生新成绩。", ) + + "
    " if len(cause_rows) == failed_result_count - else f'

    当前有 {failed_result_count} 个失败结果,根因核验尚未完成。正式发布前必须逐个绑定当前证据并补齐根因子类和具体原因。

    ' + else f'

    当前有 {failed_result_count} 个失败结果,详细原因仍在核验中;未核验结果不展示推测。

    ' ) else: cause_section = '

    本轮没有失败结果,无需生成失败根因记录。

    ' return ( '
    Browser Use 实测解读
    ' - f"

    {len(MODEL_LABELS)} 个模型层中的浏览器对照:{discordant} 个不一致配对

    " - '

    浏览器是参评选手,模型是分层实验变量。每个模型层内使用相同任务、站点状态和 evaluator;不同模型之间不做浏览器配对。

    ' - '

    测量口径:WebArena‑Verified 1.2.3;每题×模型×浏览器一个最终逻辑结果;官方确定性 evaluator;无 LLM judge。

    ' - "

    一、先看总分,再看同题分歧

    " - "

    汇总按模型→浏览器组织;每个对比浏览器只与 profile 声明的基准浏览器配对。

    " + f"

    {task_count} 个任务中,Chromium 与 Moli 的差异在哪里

    " + '

    报告分别使用 GPT-5.6 Sol 和 GPT-6 Astra 驱动 CowAgent。每组都在相同任务、网站和验收规则下比较浏览器。

    ' + '

    WebArena‑Verified 1.2.3 · 官方程序化 evaluator · 不使用 LLM judge

    ' + "

    总体成绩和耗时

    " + + _table( + ["模型", "浏览器", "通过任务", "通过率", "Agent 总耗时", "BrowserTool 耗时", "Tokens"], + _summary_comparison_rows(summary), + table_id="guide-core-metrics", + classes="core-comparison-table", + ) + + '

    在 Sol 运行中,Chromium 比 Moli 多通过 3 题;在 Astra 运行中,两者都是 71/90。耗时需要结合下表和逐题记录看:Agent 总耗时还包含模型推理与页面等待,BrowserTool 耗时也包含工具封装和 CDP 往返。

    ' + "

    同一任务上的结果

    " + "

    同题比较只在同一模型内进行。A、B 是表内所列的两个浏览器。

    " + _table( [ "模型", - "基准浏览器", - "对比浏览器", - "完整配对", - "共同通过", - "仅基准通过", - "仅对比通过", - "共同失败", + "浏览器 A", + "浏览器 B", + "同题数", + "两边都通过", + "A 通过、B 失败", + "A 失败、B 通过", + "两边都失败", ], comparison_rows, - caption="表 1|配对严格限制在同一模型层内。", + caption="每行只比较同一模型、同一任务和同一网站状态下的结果。", ) - + f"

    固定样本中观察到 {discordant} 个不一致配对。这个数字用于发现浏览器相关差异,但不能单独证明整个浏览器内核的优劣,也不对 WebArena‑Verified 全集作统计外推。

    " - '

    二、模型层之间只作观察性对照

    ' - '

    更换模型没有带来一致的浏览器通过率提升:Sol 层是 Chromium 73/90、Moli 70/90;Astra 层两者都是 71/90。两个模型层来自不同运行,且 browser-eval 提交不同;Astra 的两个 Task 77 结果还受到已核验的共享环境并发污染。因此,跨模型差异不能单独归因于模型版本。

    ' + + f"

    两次运行合计有 {discordant} 个同题结果不同。Sol 和 Astra 的方向并不一致,因此现有结果不足以支持某个浏览器在所有任务上更好。

    " + '

    两个模型的结果不能直接当成模型排名

    ' + '

    Sol 与 Astra 在不同时间、不同代码提交上运行。Astra 的两个 Task 77 结果还受到已确认的共享环境并发污染。表格可以帮助观察差异,但不能把差值全部归因于模型。

    ' + _table(["模型", "浏览器", "任务通过", "通过率"], model_result_rows) - + '

    三、失败结果必须追到根因

    ' - "

    程序化 evaluator 先给出失败环节;核验再检查最终执行的任务合同、回答、网络记录和 Agent 轨迹,形成更具体的根因子类与原因。共同失败不默认是共同根因,单边失败也不默认是浏览器缺陷。

    " + + '

    失败原因

    ' + "

    evaluator 的报错只说明哪条验收规则没有满足。我们继续核对任务要求、最终回答、网络记录和 Agent 轨迹,确认失败发生在 Agent、任务规则、测试网站、共享环境或浏览器证据记录中的哪一处。

    " + cause_section - + f"

    四、简单、中等、困难都覆盖,但难度只表示操作长度

    WebArena‑Verified 没有官方 easy/medium/hard 三档。本项目在运行前按最少可见 UI 操作数注册为 1–4、5–7、8+ 三档,配额 {tier_counts['easy']}/{tier_counts['medium']}/{tier_counts['hard']}。它控制操作链覆盖,不等同于推理难度。

    " - + f'

    五、选择 WebArena‑Verified 是为了可归因的结果验收

    Mind2Web 家族采用面更广,但原版主要评离线动作预测;Online‑Mind2Web 和 WebVoyager贴近实时网页,却引入网页漂移和模型裁判。WebArena‑Verified 可以自托管、逐题重置,并用结构化最终答案和必要网络副作用做程序化验收,更适合“只替换浏览器”的 A/B。

    代价也明确:部署重,环境兼容本身会成为工程变量;官方 evaluator 不衡量路径效率,也不能覆盖开放互联网泛化。

    六、结论只适用于本轮受控样本

    本轮固定 {task_count} 题,覆盖四个已验证站点。结果只说明当前 CowAgent、模型、任务与运行协议下的浏览器差异;不支持把总分直接解释为浏览器内核等价、全面替代关系或纯内核速度。端到端耗时包含模型、调度、页面等待和工具开销,动作延迟也包含 CDP 往返。

    决策边界

    先修复已核验根因并形成新证据,再扩大任务与浏览器范围;历史官方分数不被人工覆盖。

    ' + + f"

    样本覆盖简单、中等和困难任务

    WebArena‑Verified 没有官方 easy、medium、hard 三档。本项目在运行前按最少可见 UI 操作数分为 1–4、5–7、8+ 三档,各有 {tier_counts['easy']}、{tier_counts['medium']}、{tier_counts['hard']} 题。这个分组描述操作长度,不代表推理难度。

    " + + f'

    为什么使用 WebArena‑Verified

    原版 Mind2Web 主要检查离线动作预测。Online‑Mind2Web 和 WebVoyager 使用实时网页,网页内容会变化,部分评分还依赖模型裁判。WebArena‑Verified 可以本地部署、逐题重置,并用最终答案和网络副作用做程序化验收,适合只替换浏览器的对照实验。

    它的代价是部署较重,环境故障也会影响结果;官方 evaluator 不评价操作路径是否高效,也覆盖不了开放互联网的全部情况。

    这组结果能说明什么

    本轮固定 {task_count} 题,覆盖四个已验证站点。结果适用于当前 CowAgent、模型、任务和运行协议。它没有证明两个浏览器内核等价,也不能证明 Moli 已经可以全面替代 Chromium。端到端耗时包含模型、调度、页面等待和工具开销。

    下一步

    按核验结论分别修复 Agent、评测任务、测试网站和浏览器证据记录,再重新运行受影响的任务;新增浏览器时继续使用同一组任务和验收规则。

    ' ) @@ -1964,14 +2071,6 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat ) article = _article_body(paired_by_model, attempts, tasks, verified_causes) families = _task_family_summary(tasks, attempts) - participant_score_cards = "".join( - '
    ' - + _e(f"{MODEL_LABELS[model_id]} · {ENGINE_LABELS[engine]} 任务通过") - + f'
    {values["passed"]}/{values["completed"]}
    ' - + f"

    {_pct(values['score'])}

    " - for model_id, model_summary in summary_by_model.items() - for engine, values in model_summary.items() - ) pairing_rows = [ [ MODEL_LABELS[model_id], @@ -1987,21 +2086,11 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat for item in model_pairs["comparisons"].values() ] discordant_total = sum(row[5] + row[6] for row in pairing_rows) - paired_total = sum(row[3] for row in pairing_rows) - both_fail_total = sum(row[7] for row in pairing_rows) - cause_counts = Counter( - str((item.get("subcategory") or {}).get("zh") or "未分类") - for item in verified_causes.values() - ) - cause_cards = "".join( - f'
    {_e(category)}
    {count}

    个失败结果

    ' - for category, count in sorted(cause_counts.items()) - ) if failed_result_ids and set(verified_causes) == failed_result_ids: failure_overview = ( - f'
    {cause_cards}
    ' - f'

    根因核验:{len(verified_causes)}/{len(failed_result_ids)} 个失败结果已逐个绑定当前证据并完成核验。

    ' - '

    查看逐结果根因与证据边界 →

    ' + _failure_cause_table(verified_causes) + + f'

    以上共 {len(verified_causes)} 个失败结果;每个结果只计一次。' + '查看逐条核验原因 →

    ' ) elif failed_result_ids: failure_overview = f'

    根因核验:{len(verified_causes)}/{len(failed_result_ids)} 个失败结果已完成;未核验结果不展示推测性原因。

    ' @@ -2009,70 +2098,58 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat failure_overview = ( '

    根因核验:本轮没有失败结果。

    ' ) - family_cards = "".join( - '
    ' - f'{_e(family["kind"])}' - f"

    {family['count']} 个任务

    " - + "".join( - f"

    {_e(MODEL_LABELS[model_id])}:" - + " · ".join( - f"{_e(ENGINE_LABELS[engine])} {family['scores'][model_id][engine]}/{family['count']}" - for engine in ENGINE_LABELS - ) - + "

    " - for model_id in MODEL_LABELS - ) - + f'查看这类任务 →
    ' - for family in families - ) home = _hero( f"Browser Use Capability Evaluation · {run_date}", - "浏览器 Browser Use 能力对比", - f"在同一 CowAgent 和同一 WebArena‑Verified {task_count} 题样本下,以 {', '.join(MODEL_LABELS.values())} 分层比较 {', '.join(ENGINE_LABELS.values())}。", - ) - home += _section( - "先看结论", - f'
    {participant_score_cards}
    单边通过
    {discordant_total}

    {paired_total} 个模型内完整配对

    共同失败
    {both_fail_total}

    不等于共同根因

    结果边界:本轮观察到 {discordant_total} 个模型内浏览器不一致配对;模型之间不配对、不混排。结论只适用于这批受控任务,不证明内核等价或全面替代关系。

    这是固定选择的 {task_count} 题试验,不是从全部 WebArena‑Verified 任务随机抽样;比例只描述本样本,不外推总体能力。

    ', + "Chromium 与 Moli 的 Browser Use 能力对比", + f"CowAgent 使用 {', '.join(MODEL_LABELS.values())},在同一组 WebArena‑Verified {task_count} 题上分别操作 {', '.join(ENGINE_LABELS.values())}。", ) home += _section( - "失败根因是报告解读的核心证据", - "

    官方 evaluator 的错误信息只定位未满足的验收规则;根因核验继续检查任务合同、最终回答、网络记录和自主轨迹。未完成核验前,报告保留自动分类,但不展示推测性根因。

    " - + failure_overview, + "评测结果", + _headline_findings(summary_by_model) + + _table( + ["模型", "浏览器", "通过任务", "通过率", "Agent 总耗时", "BrowserTool 耗时", "Tokens"], + _summary_comparison_rows(summary_by_model), + table_id="core-metrics-table", + classes="core-comparison-table", + ) + + '

    只标记同一模型下数值唯一更好的浏览器。通过数越高越好;耗时和 Tokens 越低越好。Agent 总耗时包含模型推理、页面等待和浏览器操作,BrowserTool 耗时更接近浏览器执行层,但仍包含工具封装与 CDP 往返。

    ' + + f'

    这 {task_count} 题是固定选择的样本,结果不能直接外推到 WebArena‑Verified 全集,也不能仅凭总耗时判断浏览器内核速度。

    ', ) home += _section( - "任务族覆盖", - "

    样本同时包含信息提取、页面导航和状态变更任务;类别来自每题官方最终答案合同。

    " - f'
    {family_cards}
    ', + "同一任务上的结果差异", + "

    每行比较同一模型、同一任务和同一网站状态下的两个浏览器。A、B 对应表内所列浏览器。

    " + + _table( + ["模型", "浏览器 A", "浏览器 B", "同题数", "两边都通过", "A 通过、B 失败", "A 失败、B 通过", "两边都失败"], + pairing_rows, + table_id="pair-outcomes", + classes="pair-outcome-table", + ) + + f'

    两组模型合计有 {discordant_total} 个同题结果不同;不同模型的运行互不配对。打开逐题对照 →

    ', ) home += _section( - "运行与证据是否完整", - '
    ' - f'

    逻辑结果:{len(attempts)} 个模型×浏览器×任务结果保持单条展示;其全部 {len(source_attempts)} 次物理执行均保留脱敏回答、官方 evaluator 结果、Agent 事件、轨迹和证据哈希。

    ' - f'

    证据完整性审计:{len(attempts)}/{len(attempts)} 个逻辑结果已覆盖,{len(review.get("unresolved") or [])} 个未决。它不重新判题,也不是独立语义复核。

    ' - f'

    可完成性覆盖:{len(measured_solvable_ids)}/{len(tasks)} 题至少有一条自主成功轨迹;其余 {len(unproven_ids)} 题仅有官方验收合同。

    ' - "
    ", + f"{len(failed_result_ids)} 个失败是怎么造成的", + "

    官方 evaluator 说明哪条验收规则没有满足;下表来自对任务要求、最终回答、网络记录和自主执行轨迹的逐条核验。

    " + + failure_overview, ) home += _section( - "从结论进入证据", - f'

    {result_count} 个逻辑结果

    查看最终判定、耗时、轨迹和核验状态。

    打开结果库 →
    ', + "三类任务的覆盖与成绩", + "

    信息提取、页面导航和页面操作使用不同验收合同,因此分类型展示,不把难度或任务类型混成新的加权分。

    " + + _task_family_table(families), ) home += _section( - "逐题配对比总分更重要", + "数据是否齐全", _table( + ["检查项", "结果", "说明"], [ - "模型", - "基准浏览器", - "对比浏览器", - "完整配对", - "共同通过", - "仅基准通过", - "仅对比通过", - "共同失败", + ["任务结果", f"{len(attempts)}/{len(attempts)}", f"{len(MODEL_LABELS)} 个模型 × {len(ENGINE_LABELS)} 个浏览器 × {task_count} 题都有最终判定"], + ["失败原因核验", f"{len(verified_causes)}/{len(failed_result_ids)}", "每个失败结果都有分类和具体原因"], + ["未解决的证据问题", len(review.get("unresolved") or []), "完整性检查没有发现待处理项"], + ["至少成功过一次的任务", f"{len(measured_solvable_ids)}/{len(tasks)}", f"另有 {len(unproven_ids)} 题在本轮没有成功轨迹"], + ["保存的实际执行", len(source_attempts), "包含正式执行及按协议发生的一次重试"], ], - pairing_rows, - caption="每个模型层内,从 profile 的基准浏览器动态生成配对。", + table_id="evidence-coverage", ) - + f'

    本轮共有 {discordant_total} 个模型内不一致配对,且方向随模型层变化;固定样本不支持对全部 Browser Use 任务作统计外推。

    ', + + f'
    报告解读查看 {task_count} 个任务查看 {result_count} 个结果环境与复现
    ', ) (stage / "index.html").write_text( _page(title="首页", active="home", body=home), encoding="utf-8" @@ -2513,13 +2590,13 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat participant_images = environment.get("browser_images") or {} adoption = _hero( "Participants", - "参评浏览器与统一实验条件", - "浏览器后端是参评选手;模型是分层实验变量,每个模型层内保持 Agent、任务、资源上限、站点与 evaluator 一致。", + "被测浏览器与固定实验条件", + "本报告比较浏览器。每个模型单独统计,CowAgent、任务、资源上限、网站和 evaluator 均保持一致。", ) adoption += _section( - "参评浏览器", + "浏览器与版本", _table( - ["模型层", "参评浏览器", "运行版本", "镜像 ID", "接入方式", "本轮成绩"], + ["模型", "浏览器", "运行版本", "镜像 ID", "接入方式", "通过任务"], [ [ MODEL_LABELS[model_id], @@ -2533,7 +2610,7 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat for engine, label in ENGINE_LABELS.items() ], ) - + "

    参评选手始终是浏览器;模型结果不跨层配对,也不把模型与浏览器混成同一排名。

    ", + + "

    每个模型分别比较各浏览器,不把不同模型的结果合并排名。

    ", ) adoption += _section( "共同实验条件", @@ -2569,14 +2646,14 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat ], ], ) - + f"

    这 {task_count} 题由 CowAgent 自主执行。仓库中的 Browser Trajectory Replay(轨迹重放)固定动作微基准属于另一条实验轨道,不是本报告的参评选手,也不计入这 {task_count} 题成绩。

    ", + + f"

    这 {task_count} 题由 CowAgent 自主执行。仓库中的 Browser Trajectory Replay(轨迹重放)是另一项固定动作测试,不计入这里的成绩。

    ", ) adoption += _section( - "如何增加下一名选手", + "增加新的浏览器", "

    新增浏览器时,需要注册其容器镜像、版本与 CDP 端点,并在同一冻结任务集和共同条件下形成完整结果。总榜按浏览器逐行扩展;同题矩阵按浏览器增加列;统计比较按浏览器对单独生成,不能把多个浏览器压成一个总胜负。

    ", ) (stage / "adoption/index.html").write_text( - _page(title="参评浏览器", active="adoption", body=adoption, depth=1), encoding="utf-8" + _page(title="被测浏览器", active="adoption", body=adoption, depth=1), encoding="utf-8" ) versions, images = ( diff --git a/harness/browser_use/report_i18n.py b/harness/browser_use/report_i18n.py index a79c20c22..eb5ccc390 100644 --- a/harness/browser_use/report_i18n.py +++ b/harness/browser_use/report_i18n.py @@ -867,7 +867,76 @@ "同题比较改为基准浏览器加多选对比浏览器和明确关系条件;动作延迟改为按 action 分卡并只比较单次均值;详情首屏只保留简明规则定位,完整 expected、actual 和错误进入规则证据区。": "Changed same-task comparison to one baseline browser, multi-select comparison browsers, and explicit outcome relations; split action latency into action cards and compare only mean per call; kept only concise rule labels in the result summary and moved complete expected, actual, and error evidence to the rule section.", "当前仅有两个参评浏览器;扩展行为已由三浏览器测试模型覆盖,新增真实浏览器仍需形成完整同题结果。": "Only two browser participants are currently measured. A three-browser test model covers extensibility behavior, while any additional real browser still requires complete same-task results.", "参评浏览器 · Browser Use 评测": "Browser participants · Browser Use evaluation", - "参评浏览器与统一实验条件": "Browser participants and shared experimental controls", + "被测浏览器": "Browsers tested", + "被测浏览器与固定实验条件": "Browsers tested and fixed experimental controls", + "本报告比较浏览器。每个模型单独统计,CowAgent、任务、资源上限、网站和 evaluator 均保持一致。": "This report compares browsers. Results are calculated separately for each model, while CowAgent, tasks, resource limits, sites, and evaluators remain fixed.", + "浏览器与版本": "Browsers and versions", + "浏览器": "Browser", + "通过任务": "Tasks passed", + "每个模型分别比较各浏览器,不把不同模型的结果合并排名。": "Browsers are compared separately within each model; results from different models are not merged into one ranking.", + "增加新的浏览器": "Adding another browser", + "新增浏览器时,需要注册其容器镜像、版本与 CDP 端点,并在同一冻结任务集和共同条件下形成完整结果。总榜按浏览器逐行扩展;同题矩阵按浏览器增加列;统计比较按浏览器对单独生成,不能把多个浏览器压成一个总胜负。": "To add a browser, register its container image, version, and CDP endpoint, then run the complete frozen task set under the same controls. Summary tables add rows, the same-task matrix adds a column, and pairwise statistics remain separate instead of collapsing every browser into one winner.", + "仓库中的 Browser Trajectory Replay(轨迹重放)是另一项固定动作测试,不计入这里的成绩。": "The repository's Browser Trajectory Replay track is a separate fixed-action test and is not included in these scores.", + "评测结果": "Evaluation results", + "浏览器 B": "Browser B", + "同一任务上的结果差异": "Same-task outcome differences", + "每行比较同一模型、同一任务和同一网站状态下的两个浏览器。A、B 对应表内所列浏览器。": "Each row compares two browsers under the same model, task, and site state. A and B identify the browsers shown in that row.", + "三类任务的覆盖与成绩": "Coverage and results by task type", + "信息提取、页面导航和页面操作使用不同验收合同,因此分类型展示,不把难度或任务类型混成新的加权分。": "Information retrieval, navigation, and page changes use different acceptance contracts, so they are reported separately without creating a new weighted score.", + "数据是否齐全": "Data completeness", + "检查项": "Check", + "每个失败结果都有分类和具体原因": "Every failed result has a category and a specific cause", + "完整性检查没有发现待处理项": "The integrity check found no unresolved issue", + "包含正式执行及按协议发生的一次重试": "Includes the main execution and the single retry allowed by the protocol", + "总体成绩和耗时": "Overall results and latency", + "报告分别使用 GPT-5.6 Sol 和 GPT-6 Astra 驱动 CowAgent。每组都在相同任务、网站和验收规则下比较浏览器。": "CowAgent was driven by GPT-5.6 Sol and GPT-6 Astra. Within each model run, browsers were compared on the same tasks, sites, and acceptance rules.", + "WebArena‑Verified 1.2.3 · 官方程序化 evaluator · 不使用 LLM judge": "WebArena-Verified 1.2.3 · official programmatic evaluator · no LLM judge", + "在 Sol 运行中,Chromium 比 Moli 多通过 3 题;在 Astra 运行中,两者都是 71/90。耗时需要结合下表和逐题记录看:Agent 总耗时还包含模型推理与页面等待,BrowserTool 耗时也包含工具封装和 CDP 往返。": "In the Sol run, Chromium passed three more tasks than Moli; in the Astra run, both passed 71/90. Latency must be read with per-task records: agent total time includes model inference and page waits, while BrowserTool time still includes tool wrapping and CDP round trips.", + "只标记同一模型下数值唯一更好的浏览器。通过数越高越好;耗时和 Tokens 越低越好。Agent 总耗时包含模型推理、页面等待和浏览器操作,BrowserTool 耗时更接近浏览器执行层,但仍包含工具封装与 CDP 往返。": "marks only the uniquely better browser within the same model. Higher pass counts are better; lower latency and token use are better. Agent total time includes model inference, page waits, and browser actions. BrowserTool time is closer to the browser execution layer but still includes tool wrapping and CDP round trips.", + "打开逐题对照 →": "Open the task-by-task comparison →", + "官方 evaluator 说明哪条验收规则没有满足;下表来自对任务要求、最终回答、网络记录和自主执行轨迹的逐条核验。": "The official evaluator identifies the unmet acceptance rule. The table comes from reviewing each task requirement, final answer, network record, and autonomous trajectory.", + "查看逐条核验原因 →": "View every verified cause →", + "作为分层变量为各浏览器生成决策": "Generates browser decisions within each separately reported model run", + "从模型分层的浏览器对照读出下一轮该怎么做": "What the browser comparisons imply for the next run", + "步骤预算耗尽": "Step budget exhausted", + "导航语义错误": "Navigation-semantics error", + "检索覆盖不足": "Incomplete retrieval coverage", + "商品选择错误": "Incorrect product selection", + "候选比较错误": "Candidate-comparison error", + "提交字段错误": "Submission-field error", + "检索策略错误": "Retrieval-strategy error", + "流程未完成": "Incomplete workflow", + "结果去重遗漏": "Result-deduplication failure", + "可恢复错误后提前终止": "Premature stop after recoverable error", + "批量操作遗漏": "Incomplete batch workflow", + "提交字段遗漏": "Submission-field omission", + "路由验收缺陷": "Route-acceptance defect", + "evaluator 实现错误": "Evaluator implementation defect", + "任务与验收矛盾": "Instruction-evaluator contradiction", + "表单验收缺陷": "Form-acceptance defect", + "任务状态与验收矛盾": "State-evaluator contradiction", + "站点保存故障": "Site save failure", + "站点依赖故障": "Site dependency failure", + "站点会话故障": "Site session failure", + "网络证据缺失": "Missing network evidence", + "网络证据匹配缺陷": "Network-evidence matching defect", + "响应证据缺失": "Missing response evidence", + "共享环境并发污染": "Shared-environment concurrency contamination", + "检索遗漏与参考状态差异": "Retrieval omission and reference-state drift", + "同题比较只在同一模型内进行。A、B 是表内所列的两个浏览器。": "Same-task comparisons are made only within one model. A and B identify the two browsers listed in the row.", + "每行只比较同一模型、同一任务和同一网站状态下的结果。": "Each row compares results under the same model, task, and site state.", + "两个模型的结果不能直接当成模型排名": "The two model runs are not a model ranking", + "Sol 与 Astra 在不同时间、不同代码提交上运行。Astra 的两个 Task 77 结果还受到已确认的共享环境并发污染。表格可以帮助观察差异,但不能把差值全部归因于模型。": "Sol and Astra ran at different times and on different code revisions. Two Astra Task 77 results were also affected by confirmed concurrent contamination of shared site state. The table shows observations, but the differences cannot all be attributed to the model.", + "evaluator 的报错只说明哪条验收规则没有满足。我们继续核对任务要求、最终回答、网络记录和 Agent 轨迹,确认失败发生在 Agent、任务规则、测试网站、共享环境或浏览器证据记录中的哪一处。": "Evaluator errors identify the unmet acceptance rule. The review then checks the task requirement, final answer, network record, and agent trajectory to locate the failure in the agent, task contract, test site, shared environment, or browser evidence capture.", + "查看每个失败结果的核验结论": "View the verified cause for every failed result", + "人工核验用于解释官方失败结果,不修改分数。修复后需要重新运行才能产生新成绩。": "Review explains official failures without changing scores. A fix affects results only after the task is run again.", + "样本覆盖简单、中等和困难任务": "The sample covers short, medium, and long tasks", + "为什么使用 WebArena‑Verified": "Why WebArena-Verified was selected", + "原版 Mind2Web 主要检查离线动作预测。Online‑Mind2Web 和 WebVoyager 使用实时网页,网页内容会变化,部分评分还依赖模型裁判。WebArena‑Verified 可以本地部署、逐题重置,并用最终答案和网络副作用做程序化验收,适合只替换浏览器的对照实验。": "The original Mind2Web mainly tests offline action prediction. Online-Mind2Web and WebVoyager use live pages, where content drifts and some scoring relies on model judges. WebArena-Verified can be self-hosted and reset per task, with programmatic checks of final answers and required network side effects, which suits a browser-only comparison.", + "它的代价是部署较重,环境故障也会影响结果;官方 evaluator 不评价操作路径是否高效,也覆盖不了开放互联网的全部情况。": "Its costs are a heavier deployment and exposure to environment failures. The official evaluator does not score path efficiency or cover the full open web.", + "这组结果能说明什么": "What these results support", + "下一步": "Next step", + "按核验结论分别修复 Agent、评测任务、测试网站和浏览器证据记录,再重新运行受影响的任务;新增浏览器时继续使用同一组任务和验收规则。": "Fix the agent, benchmark tasks, test sites, and browser evidence capture according to the verified causes, then rerun affected tasks. New browsers should use the same task set and acceptance rules.", "Chromium 与 Moli 作为浏览器后端参与同题对照;Agent、模型、任务、资源上限、站点与 evaluator 保持一致。": "Chromium and Moli participate as browser backends in a same-task comparison; the agent, model, tasks, resource limits, sites, and evaluators are held constant.", "参评浏览器是本轮唯一实验变量;完整 Agent 栈的其余组成列为统一实验条件。": "The browser participant is the only experimental variable in this run; all other components of the agent stack are shared controls.", "逻辑结果耗时 P50": "P50 logical-result latency", @@ -981,6 +1050,65 @@ "各模型任务成绩": "Task scores by model cohort", "各模型逻辑结果耗时 P50": "P50 logical-result latency by model cohort", "判分权威执行的原始证据绑定": "Source-evidence binding for the authoritative execution", + "Chromium 与 Moli 的 Browser Use 能力对比": "Chromium vs Moli on Browser Use tasks", + "评测结果": "Results", + "同一任务上的结果差异": "Outcomes on the same tasks", + "三类任务的覆盖与成绩": "Coverage and results by task type", + "数据是否齐全": "Data completeness", + "Agent 总耗时": "Agent total time", + "同题数": "Matched tasks", + "两边都通过": "Both passed", + "A 通过、B 失败": "A passed, B failed", + "A 失败、B 通过": "A failed, B passed", + "两边都失败": "Both failed", + "原因所在": "Cause area", + "失败结果数": "Failed results", + "具体分类": "Detailed categories", + "说明": "Explanation", + "Agent 决策或操作": "Agent decisions or actions", + "任务或 evaluator": "Task or evaluator", + "测试网站": "Test website", + "浏览器证据记录": "Browser evidence capture", + "共享测试环境": "Shared test environment", + "多项原因": "Multiple causes", + "检索不完整、候选选择错误、走错页面、字段遗漏,或没有在步骤上限内完成。": "Incomplete retrieval, incorrect candidate selection, wrong navigation, missing fields, or failure to finish within the step limit.", + "任务要求与验收条件冲突,或 evaluator 的路由、表单和实现规则有缺陷。": "The task conflicts with its acceptance conditions, or the evaluator has route, form, or implementation defects.", + "自托管网站的依赖、会话或保存接口失败。": "A dependency, session, or save endpoint failed in a self-hosted test site.", + "页面操作可能成功,但网络记录缺少响应正文或 Referer,evaluator 无法确认。": "The page action may have succeeded, but missing response bodies or Referer fields prevented evaluator confirmation.", + "并发运行改写了同一网站状态;运行器现已串行化,历史结果仍按原证据保留。": "Concurrent runs changed shared site state. The runner now serializes execution; historical results remain bound to their original evidence.", + "同一结果同时包含 Agent 遗漏和冻结参考状态差异。": "The same result includes both an Agent omission and a difference from the frozen reference state.", + "任务类型": "Task type", + "测什么": "What it measures", + "信息提取": "Information retrieval", + "页面导航": "Page navigation", + "页面操作": "State-changing action", + "从页面读取、筛选或汇总信息,不修改站点状态": "Read, filter, or summarize page information without changing site state", + "打开符合指定条件的页面或列表": "Open a page or list matching the requested conditions", + "提交表单、修改记录、结账等会改变站点状态的任务": "Submit forms, edit records, check out, or perform other state-changing actions", + "检查项": "Check", + "任务结果": "Task results", + "失败原因核验": "Failure-cause review", + "未解决的证据问题": "Unresolved evidence issues", + "至少成功过一次的任务": "Tasks with at least one successful run", + "保存的实际执行": "Saved executions", + "每个失败结果都有分类和具体原因": "Every failed result has a category and a specific reviewed cause", + "完整性检查没有发现待处理项": "Integrity checks found no unresolved items", + "包含正式执行及按协议发生的一次重试": "Includes score-bearing executions and the one retry permitted by the protocol", + "报告解读": "Interpretation", + "环境与复现": "Environment and reproduction", + "总体成绩和耗时": "Overall scores and latency", + "同一任务上的结果": "Outcomes on the same tasks", + "两个模型的结果不能直接当成模型排名": "The two model runs are not a model ranking", + "失败原因": "Failure causes", + "样本覆盖简单、中等和困难任务": "The sample covers short, medium, and long interaction paths", + "为什么使用 WebArena‑Verified": "Why WebArena-Verified", + "这组结果能说明什么": "What these results support", + "下一步": "Next steps", + "结果编号": "Result ID", + "原因分类": "Cause category", + "核验结论": "Reviewed cause", + "查看每个失败结果的核验结论": "View the reviewed cause for every failed result", + "人工核验用于解释官方失败结果,不修改分数。修复后需要重新运行才能产生新成绩。": "Human review explains official failures without changing scores. A fix requires a new run to produce a new result.", } ) @@ -1100,6 +1228,31 @@ def _register_report_change_translations() -> None: PHRASES = { + "步骤预算耗尽": "Step budget exhausted", + "导航语义错误": "Navigation-semantics error", + "检索覆盖不足": "Incomplete retrieval coverage", + "商品选择错误": "Incorrect product selection", + "候选比较错误": "Candidate-comparison error", + "提交字段错误": "Submission-field error", + "检索策略错误": "Retrieval-strategy error", + "流程未完成": "Incomplete workflow", + "结果去重遗漏": "Result-deduplication failure", + "可恢复错误后提前终止": "Premature stop after recoverable error", + "批量操作遗漏": "Incomplete batch workflow", + "提交字段遗漏": "Submission-field omission", + "路由验收缺陷": "Route-acceptance defect", + "evaluator 实现错误": "Evaluator implementation defect", + "任务与验收矛盾": "Instruction-evaluator contradiction", + "表单验收缺陷": "Form-acceptance defect", + "任务状态与验收矛盾": "State-evaluator contradiction", + "站点保存故障": "Site save failure", + "站点依赖故障": "Site dependency failure", + "站点会话故障": "Site session failure", + "网络证据缺失": "Missing network evidence", + "网络证据匹配缺陷": "Network-evidence matching defect", + "响应证据缺失": "Missing response evidence", + "共享环境并发污染": "Shared-environment concurrency contamination", + "检索遗漏与参考状态差异": "Retrieval omission and reference-state drift", "规则 ": "Rule ", " 秒": " s", " 次 BrowserTool 调用": " BrowserTool calls", @@ -1129,6 +1282,68 @@ def translate_text(value: str) -> str: translated = TEXT.get(core) if translated is None: patterns = ( + ( + r"CowAgent 使用 (.+),在同一组 WebArena‑Verified (\d+) 题上分别操作 (.+)。", + lambda m: f"CowAgent uses {m.group(1)} to operate {m.group(3)} on the same {m.group(2)} WebArena-Verified tasks.", + ), + ( + r"(.+?):(.+?) (\d+)/(\d+)(([\d.]+)%),(.+?) (\d+)/(\d+)(([\d.]+)%);(.+?) 多通过 (\d+) 题。", + lambda m: f"{m.group(1)}: {m.group(2)} passed {m.group(3)}/{m.group(4)} ({m.group(5)}%); {m.group(6)} passed {m.group(7)}/{m.group(8)} ({m.group(9)}%). {m.group(10)} passed {m.group(11)} more task(s).", + ), + ( + r"(.+?):(.+?) (\d+)/(\d+)(([\d.]+)%),(.+?) (\d+)/(\d+)(([\d.]+)%);通过数相同。", + lambda m: f"{m.group(1)}: {m.group(2)} passed {m.group(3)}/{m.group(4)} ({m.group(5)}%); {m.group(6)} passed {m.group(7)}/{m.group(8)} ({m.group(9)}%). The pass counts are tied.", + ), + ( + r":(.+?) (\d+)/(\d+)(([\d.]+)%),(.+?) (\d+)/(\d+)(([\d.]+)%);(.+?) 多通过 (\d+) 题。", + lambda m: f": {m.group(1)} passed {m.group(2)}/{m.group(3)} ({m.group(4)}%); {m.group(5)} passed {m.group(6)}/{m.group(7)} ({m.group(8)}%). {m.group(9)} passed {m.group(10)} more task(s).", + ), + ( + r":(.+?) (\d+)/(\d+)(([\d.]+)%),(.+?) (\d+)/(\d+)(([\d.]+)%);通过数相同。", + lambda m: f": {m.group(1)} passed {m.group(2)}/{m.group(3)} ({m.group(4)}%); {m.group(5)} passed {m.group(6)}/{m.group(7)} ({m.group(8)}%). The pass counts are tied.", + ), + ( + r"这 (\d+) 题是固定选择的样本,结果不能直接外推到 WebArena‑Verified 全集,也不能仅凭总耗时判断浏览器内核速度。", + lambda m: f"These {m.group(1)} tasks are a fixed cohort. Results do not generalize directly to the full WebArena-Verified benchmark, and total elapsed time alone does not measure browser-engine speed.", + ), + ( + r"两组模型合计有 (\d+) 个同题结果不同;不同模型的运行互不配对。", + lambda m: f"Across both model runs, {m.group(1)} same-task results differ between browsers. Runs from different models are not paired.", + ), + ( + r"(\d+) 个失败是怎么造成的", + lambda m: f"What caused the {m.group(1)} failures", + ), + ( + r"以上共 (\d+) 个失败结果;每个结果只计一次。", + lambda m: f"The table covers {m.group(1)} failed results, each counted once.", + ), + ( + r"(\d+) 个模型 × (\d+) 个浏览器 × (\d+) 题都有最终判定", + lambda m: f"Every result has a final verdict: {m.group(1)} models × {m.group(2)} browsers × {m.group(3)} tasks", + ), + ( + r"另有 (\d+) 题在本轮没有成功轨迹", + lambda m: f"Another {m.group(1)} tasks have no successful trajectory in this run", + ), + (r"查看 (\d+) 个任务", lambda m: f"View {m.group(1)} tasks"), + (r"查看 (\d+) 个结果", lambda m: f"View {m.group(1)} results"), + ( + r"(\d+) 个任务中,Chromium 与 Moli 的差异在哪里", + lambda m: f"Where Chromium and Moli differ across {m.group(1)} tasks", + ), + ( + r"WebArena‑Verified 没有官方 easy、medium、hard 三档。本项目在运行前按最少可见 UI 操作数分为 1–4、5–7、8\+ 三档,各有 (\d+)、(\d+)、(\d+) 题。这个分组描述操作长度,不代表推理难度。", + lambda m: f"WebArena-Verified has no official easy, medium, or hard tiers. Before execution, this project grouped tasks by minimum visible UI actions: 1-4, 5-7, and 8+, containing {m.group(1)}, {m.group(2)}, and {m.group(3)} tasks. These groups describe action length, not reasoning difficulty.", + ), + ( + r"两次运行合计有 (\d+) 个同题结果不同。Sol 和 Astra 的方向并不一致,因此现有结果不足以支持某个浏览器在所有任务上更好。", + lambda m: f"Across the two runs, {m.group(1)} same-task results differ between browsers. The Sol and Astra runs point in different directions, so the current evidence does not show that one browser is better on every task.", + ), + ( + r"本轮固定 (\d+) 题,覆盖四个已验证站点。结果适用于当前 CowAgent、模型、任务和运行协议。它没有证明两个浏览器内核等价,也不能证明 Moli 已经可以全面替代 Chromium。端到端耗时包含模型、调度、页面等待和工具开销。", + lambda m: f"This run fixes {m.group(1)} tasks across four verified sites. Results apply to the current CowAgent, models, tasks, and run protocol. They do not establish browser-engine equivalence or show that Moli can fully replace Chromium. End-to-end time includes model, scheduling, page waits, and tool overhead.", + ), ( r"最终数据缺少 (\d+) 项;官方期望 (\d+) 项,匹配 (\d+) 项。", lambda match: ( diff --git a/harness/browser_use/runner.py b/harness/browser_use/runner.py index df90f6eaf..0bc222c22 100644 --- a/harness/browser_use/runner.py +++ b/harness/browser_use/runner.py @@ -1114,7 +1114,7 @@ def parse_args() -> argparse.Namespace: "--cowagent-repo", help="trusted, clean CowAgent Git checkout; Python code is imported and executed from it", ) - parser.add_argument("--model", default="gpt-6-astra") + parser.add_argument("--model", required=True) parser.add_argument("--cpus", type=float, default=2.0) parser.add_argument("--memory", default="2g") parser.add_argument("--sample-interval-ms", type=int, default=100) diff --git a/research/report-changes.json b/research/report-changes.json index ae545cd02..14c5f9c45 100644 --- a/research/report-changes.json +++ b/research/report-changes.json @@ -147,6 +147,53 @@ } ], "browser_use_releases": [ + { + "id": "2026-09-11-reader-first-report-and-sites-migration", + "date": "2026-09-11", + "title": "重排结果首页并迁移到正式 Sites", + "summary": "首页先展示四组核心成绩与耗时,再给出同题差异、75 个失败的核验原因和三类任务成绩。此次只调整报告表达、检查流程与发布地址,不修改任何评测分数。", + "attempts_sha256_by_model": { + "gpt-5.6-sol": "73146500152db62d79f84b63c85a14027c005a6320c19bce7426651f167466bd", + "gpt-6-astra": "0659d227cf6d29fbfaf64d39f8e77a56bec73c87f7f6e1f2ac23ea13fd9b667c" + }, + "selection_sha256": "2cb716552e36df7535e83ed6d2cf3f84283e3aca86072817cd6e039a38547c7b", + "changes": [ + [ + "首页结构", + "核心成绩被拆成多张卡片,关键对比不易浏览。", + "改为一张按模型和浏览器展开的核心指标表,并把它放在首页第一节。", + "通过数、通过率、Agent 总耗时、BrowserTool 耗时和 Tokens 可直接横向比较。" + ], + [ + "结果解释", + "部分短标签需要读者了解内部实现才能理解。", + "改用完整句子和明确列名说明同题关系、失败原因、统计范围与限制。", + "不改变计算口径;只降低理解成本。" + ], + [ + "失败原因", + "75 个失败原因以大量卡片堆叠,难以判断责任分布。", + "按 Agent、任务或 evaluator、测试网站、浏览器证据、共享环境和混合原因汇总为表格,并保留逐结果核验入口。", + "所有失败仍各计一次,逐结果核验结论和原始证据保持不变。" + ], + [ + "检查与发布", + "日常修改也会启动完整浏览器视觉检查,反馈较慢。", + "日常检查只验证数据、结构、链接和文案;正式打包发布时再执行全部语言、主题和屏幕宽度的视觉检查。", + "发布质量门槛不降低,常规迭代时间缩短。" + ] + ], + "i18n": {"en": { + "title": "Reorder the results report and migrate to production Sites", + "summary": "The home page now starts with the four core score and latency rows, followed by same-task differences, verified causes for 75 failures, and results for the three task types. This release changes presentation, checks, and the publication target only; no evaluation score changes.", + "changes": [ + ["Home-page structure", "Core results were split across cards, making comparison difficult.", "Replaced them with one model-by-browser core-metrics table at the start of the home page.", "Pass count, pass rate, agent total time, BrowserTool time, and tokens can now be compared directly."], + ["Result interpretation", "Several short labels required implementation context.", "Replaced them with explicit sentences and column names for paired outcomes, failure causes, scope, and limitations.", "Calculation rules are unchanged; the report is easier to understand."], + ["Failure causes", "The 75 failure causes were spread across a large card grid.", "Grouped them into a table covering agent, task or evaluator, test site, browser evidence, shared environment, and mixed causes, while retaining links to every reviewed result.", "Each failure is still counted once, and its reviewed cause and evidence remain unchanged."], + ["Checks and publication", "Routine edits also launched the complete browser-based visual suite.", "Routine checks now cover data, structure, links, and copy; packaging still runs every language, theme, and viewport visual check.", "Release quality gates remain intact while ordinary iteration is faster."] + ] + }} + }, { "id": "2026-09-11-astra-two-model-browser-comparison", "date": "2026-09-11", @@ -444,8 +491,8 @@ ], [ "报告模型", - "评测集页混入成功/失败,多浏览器结果结构写死为两列,方法介绍没有明确参评选手。", - "评测集页只保留任务与验收合同;结果页使用动态浏览器矩阵和自动两两比较;参评浏览器页把 Chromium/Moli 定义为选手,把 CowAgent、模型、站点和 evaluator 列为共同条件。", + "评测集页混入成功/失败,多浏览器结果结构写死为两列,方法介绍没有明确比较对象。", + "评测集页只保留任务与验收合同;结果页使用动态浏览器矩阵和自动两两比较;被测浏览器页列出 Chromium、Moli 及固定实验条件。", "信息职责和未来扩展方式改变,当前两浏览器数据与成绩不变。" ], [ diff --git a/scripts/check_browser_use_report.py b/scripts/check_browser_use_report.py index c20b6bd3d..52914719c 100644 --- a/scripts/check_browser_use_report.py +++ b/scripts/check_browser_use_report.py @@ -33,6 +33,29 @@ from harness.browser_use.selection import load_pilot +FORBIDDEN_READER_TERMS = ( + "模型内完整配对", + "不等于共同根因", + "仅基准通过", + "仅对比通过", +) + + +def _check_reader_facing_ux( + report_dir: Path, rendered_pages: dict[Path, str] +) -> None: + """Enforce stable, data-independent report presentation contracts.""" + + for page, text in rendered_pages.items(): + term = next((term for term in FORBIDDEN_READER_TERMS if term in text), None) + if term: + raise ValueError(f"formal report contains unexplained internal term {term}: {page}") + for homepage in (report_dir / "index.html", report_dir / "en/index.html"): + text = rendered_pages[homepage] + if not re.search(r']*\bid=["\']core-metrics-table["\']', text, flags=re.I): + raise ValueError(f"report home page is missing the core metrics table: {homepage}") + + def _sha256(path: Path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest() @@ -352,6 +375,7 @@ def check( rendered_pages = { page: page.read_text(encoding="utf-8") for page in (*chinese_pages, *english_pages) } + _check_reader_facing_ux(report_dir, rendered_pages) if any('' not in rendered_pages[page] for page in english_pages): raise ValueError("every Chinese report route must have an English mirror") cjk = re.compile(r"[\u3400-\u9fff]") @@ -407,8 +431,8 @@ def check( raise ValueError(f"analysis contains a redundant or retired presentation: {forbidden}") participant_index = rendered_pages[report_dir / "adoption/index.html"] if ( - "参评浏览器与统一实验条件" not in participant_index - or "参评选手是浏览器后端,不是 Agent 或 benchmark" in participant_index + "被测浏览器与固定实验条件" not in participant_index + or "参评选手" in participant_index ): raise ValueError("participant page must use neutral reader-facing terminology") if result_pages and any( @@ -434,7 +458,7 @@ def check( standalone = (report_dir / "guide/article.html").read_text(encoding="utf-8") if " None: states = [] failures = [] executable = _browser_executable() + routes = _route_classes(report) with sync_playwright() as playwright: browser = playwright.chromium.launch(executable_path=executable, headless=True) try: - for route in _route_classes(report): + for route in routes: for width, height in ((1440, 1000), (390, 844)): for theme in ("light", "dark"): page = browser.new_page(viewport={"width": width, "height": height}) @@ -119,7 +120,7 @@ def check(report: Path, receipt: Path) -> None: { "schema": "lex_browser_use_visual_qa/1", "browser_executable": executable, - "route_classes": len(_route_classes(report)), + "route_classes": len(routes), "states_checked": len(states), "failures": failures, }, diff --git a/scripts/check_report_links.py b/scripts/check_report_links.py index a0ecd0e54..8aaca7145 100755 --- a/scripts/check_report_links.py +++ b/scripts/check_report_links.py @@ -25,8 +25,8 @@ def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None self.ids.add(value) -def main() -> int: - root = Path(sys.argv[1]).resolve() +def check(root: Path) -> None: + root = root.resolve() missing: list[str] = [] pages = list(root.rglob("*.html")) parsed_pages: dict[Path, Links] = {} @@ -57,10 +57,16 @@ def parse_page(path: Path) -> Links: if fragment not in parse_page(target).ids: missing.append(f"{page.relative_to(root)} -> {value} (missing fragment)") if missing: - print("Broken local links:") - print("\n".join(missing)) - return 1 + raise ValueError("Broken local links:\n" + "\n".join(missing)) print(f"Report link check: {len(pages)} logical report routes, 0 broken local links") + + +def main() -> int: + try: + check(Path(sys.argv[1])) + except ValueError as error: + print(error) + return 1 return 0 diff --git a/scripts/package_browser_use_artifact_hub.py b/scripts/package_browser_use_artifact_hub.py index 8323db060..9d95e9690 100644 --- a/scripts/package_browser_use_artifact_hub.py +++ b/scripts/package_browser_use_artifact_hub.py @@ -9,10 +9,18 @@ import zipfile from pathlib import Path -from check_browser_use_report import check +if __package__: + from scripts.check_browser_use_report import check + from scripts.check_browser_use_visual import check as check_visual + from scripts.check_report_links import check as check_links +else: # Direct execution adds scripts/, not the repository root. + from check_browser_use_report import check + from check_browser_use_visual import check as check_visual + from check_report_links import check as check_links ATTESTATION = Path("publish/browser-use-sites.attestation.json") PROFILE = Path("evaluation/browser-use/current-report.json") +VISUAL_RECEIPT = Path("publish/browser-use-visual-qa.json") def sha256(path: Path) -> str: @@ -33,18 +41,27 @@ def tree_sha256(root: Path) -> str: return digest.hexdigest() +def validate_release(report: Path, evidence: list[Path], visual_receipt: Path) -> None: + """Run every release gate before creating a publishable archive.""" + + check( + report, + evidence, + Path("evaluation/browser-use/webarena-verified-pilot.json"), + require_complete=True, + ) + check_links(report) + check_visual(report, visual_receipt) + + def main() -> None: parser = argparse.ArgumentParser() parser.add_argument("--report", type=Path, default=Path("reports/browser-use")) parser.add_argument("--evidence", required=True, action="append", type=Path) parser.add_argument("--output", type=Path, default=Path("publish/browser-use-sites.zip")) + parser.add_argument("--visual-receipt", type=Path, default=VISUAL_RECEIPT) args = parser.parse_args() - check( - args.report, - args.evidence, - Path("evaluation/browser-use/webarena-verified-pilot.json"), - require_complete=True, - ) + validate_release(args.report, args.evidence, args.visual_receipt) args.output.parent.mkdir(parents=True, exist_ok=True) with zipfile.ZipFile(args.output, "w", zipfile.ZIP_DEFLATED) as archive: for path in sorted(args.report.rglob("*")): diff --git a/scripts/publish_browser_use_artifact_hub.py b/scripts/publish_browser_use_artifact_hub.py index 9415250ad..4de181fce 100644 --- a/scripts/publish_browser_use_artifact_hub.py +++ b/scripts/publish_browser_use_artifact_hub.py @@ -72,13 +72,15 @@ def validate_package() -> None: ) -def publication_target() -> tuple[str, str, str]: +def publication_target(*, require_existing: bool = True) -> tuple[str, str, str]: profile = json.loads(PROFILE.read_text(encoding="utf-8")) publication = profile.get("publication") or {} base = str(publication.get("base_url") or "").rstrip("/") slug = str(publication.get("site_slug") or "") version = str(publication.get("expected_version") or "") - if not base.startswith("https://") or not slug or not version.startswith("ver_"): + if not base.startswith("https://"): + raise ValueError("current Browser Use report has no valid publication base") + if require_existing and (not slug or not version.startswith("ver_")): raise ValueError("current Browser Use report has no valid publication target") return base, slug, version @@ -90,7 +92,7 @@ def request( *, method: str | None = None, ): - base, _, _ = publication_target() + base, _, _ = publication_target(require_existing=False) headers = { "Authorization": "Bearer " + load_token(base_url=base), "User-Agent": "Browser-Use-Report-Publisher/1.0", @@ -112,7 +114,7 @@ def multipart_archive() -> tuple[bytes, str]: parts = [] for key, value in ( ("mode", "zip"), - ("title", "浏览器 Browser Use 能力对比"), + ("title", "Chromium 与 Moli 的 Browser Use 能力对比"), ): parts.append( ( @@ -142,7 +144,7 @@ def save_receipt(result: dict) -> None: def main(*, create: bool = False) -> None: validate_package() payload, content_type = multipart_archive() - base, expected_slug, expected_version = publication_target() + base, expected_slug, expected_version = publication_target(require_existing=not create) if create: status, raw = request("/api/sites", payload, content_type) result = json.loads(raw) @@ -180,13 +182,13 @@ def main(*, create: bool = False) -> None: ) title_status, title_raw = request( f"/api/sites/{result['slug']}", - json.dumps({"title": "浏览器 Browser Use 能力对比"}).encode(), + json.dumps({"title": "Chromium 与 Moli 的 Browser Use 能力对比"}).encode(), "application/json", method="PATCH", ) if not 200 <= title_status < 300: raise RuntimeError(json.loads(title_raw)) - result["title"] = "浏览器 Browser Use 能力对比" + result["title"] = "Chromium 与 Moli 的 Browser Use 能力对比" if not result.get("share_url"): status, raw = request( f"/api/sites/{result['slug']}/shares", @@ -209,9 +211,20 @@ def main(*, create: bool = False) -> None: ) with urllib.request.urlopen(preview, timeout=60) as response: body = response.read().decode("utf-8", errors="replace") - if response.status != 200 or "浏览器 Browser Use 能力对比" not in body: + if response.status != 200 or "Chromium 与 Moli 的 Browser Use 能力对比" not in body: raise RuntimeError("public preview is unavailable or missing the report heading") - print(json.dumps({"slug": result["slug"], "url": result["share_url"]}, ensure_ascii=False)) + print( + json.dumps( + { + "slug": result["slug"], + "url": result["share_url"], + "version": result.get("versionId") + or result.get("version_id") + or result.get("currentVersionId"), + }, + ensure_ascii=False, + ) + ) if __name__ == "__main__": diff --git a/tests/browser_use/test_publish.py b/tests/browser_use/test_publish.py index e9ae2d547..333531af7 100644 --- a/tests/browser_use/test_publish.py +++ b/tests/browser_use/test_publish.py @@ -5,6 +5,7 @@ from pathlib import Path import scripts.publish_browser_use_artifact_hub as publisher +import scripts.package_browser_use_artifact_hub as packager def _sha256(path: Path) -> str: @@ -61,3 +62,30 @@ def test_validate_package_requires_and_checks_every_evidence_root( assert "evidence" in str(error) else: raise AssertionError("changed secondary evidence must invalidate the package") + + +def test_packaging_runs_complete_release_gate(monkeypatch, tmp_path: Path) -> None: + report = tmp_path / "report" + evidence = [tmp_path / "evidence-a", tmp_path / "evidence-b"] + receipt = tmp_path / "visual.json" + calls = [] + + monkeypatch.setattr( + packager, + "check", + lambda report_dir, evidence_dirs, selection, *, require_complete: calls.append( + ("report", report_dir, evidence_dirs, selection, require_complete) + ), + ) + monkeypatch.setattr(packager, "check_links", lambda report_dir: calls.append(("links", report_dir))) + monkeypatch.setattr( + packager, + "check_visual", + lambda report_dir, receipt_path: calls.append(("visual", report_dir, receipt_path)), + ) + + packager.validate_release(report, evidence, receipt) + + assert [call[0] for call in calls] == ["report", "links", "visual"] + assert calls[0][-1] is True + assert calls[2] == ("visual", report, receipt) diff --git a/tests/browser_use/test_report.py b/tests/browser_use/test_report.py index 42a5be4a7..4c98b7421 100644 --- a/tests/browser_use/test_report.py +++ b/tests/browser_use/test_report.py @@ -17,7 +17,12 @@ ) from harness.browser_use.report_i18n import localize_html from harness.browser_use.selection import load_pilot -from scripts.check_browser_use_report import _check_execution_artifacts, _check_public_privacy +from scripts.check_browser_use_report import ( + FORBIDDEN_READER_TERMS, + _check_execution_artifacts, + _check_public_privacy, + _check_reader_facing_ux, +) def test_best_marks_only_one_visibly_distinct_winner() -> None: @@ -46,6 +51,36 @@ def test_english_localization_includes_visible_attributes() -> None: assert 'placeholder="Task, intent, or acceptance contract"' in localized +def test_reader_facing_ux_requires_home_core_tables_without_current_data( + tmp_path: Path, +) -> None: + pages = { + tmp_path / "index.html": '
    ', + tmp_path / "en/index.html": '
    ', + tmp_path / "analysis/index.html": "普通读者可理解的说明", + } + + _check_reader_facing_ux(tmp_path, pages) + + pages[tmp_path / "index.html"] = "
    只有指标卡,没有核心比较表
    " + with pytest.raises(ValueError, match="core metrics table"): + _check_reader_facing_ux(tmp_path, pages) + + +@pytest.mark.parametrize("term", FORBIDDEN_READER_TERMS) +def test_reader_facing_ux_rejects_unexplained_internal_terms( + tmp_path: Path, term: str +) -> None: + pages = { + tmp_path / "index.html": '
    ', + tmp_path / "en/index.html": '
    ', + tmp_path / "analysis/index.html": f"

    {term}

    ", + } + + with pytest.raises(ValueError, match="unexplained internal term"): + _check_reader_facing_ux(tmp_path, pages) + + def test_result_verification_is_readable_and_does_not_duplicate_raw_panels() -> None: result = { "evaluators_results": [ @@ -471,7 +506,7 @@ def test_report_renders_measured_runtime_identities(tmp_path: Path, monkeypatch) assert report.is_file() assert (output / "assets/webarena-autonomous-evaluation-flow.png").is_file() home_text = report.read_text(encoding="utf-8") - assert "浏览器 Browser Use 能力对比" in home_text + assert "Chromium 与 Moli 的 Browser Use 能力对比" in home_text methodology_text = (output / "methodology/index.html").read_text(encoding="utf-8") assert "webarena-autonomous-evaluation-flow.png" in methodology_text assert "ContainerManager.start(reset=True)" in methodology_text diff --git a/tests/test_runtime_entrypoints.py b/tests/test_runtime_entrypoints.py index f6fc0477c..21c1be244 100644 --- a/tests/test_runtime_entrypoints.py +++ b/tests/test_runtime_entrypoints.py @@ -20,6 +20,19 @@ def test_repository_pins_python_and_uv(self) -> None: self.assertIn("$(UV) sync --frozen --all-extras", makefile) self.assertIn("sys.version_info[:2] == (3, 11)", makefile) + def test_browser_use_routine_and_release_checks_are_separate(self) -> None: + makefile = (ROOT / "Makefile").read_text() + routine = makefile.split("browser-use-report-check:", 1)[1].split( + "browser-use-release-check:", 1 + )[0] + release = makefile.split("browser-use-release-check:", 1)[1] + + self.assertIn("check_browser_use_report.py", routine) + self.assertIn("check_report_links.py", routine) + self.assertNotIn("browser-use-visual-check", routine) + self.assertIn("browser-use-report-check", release.splitlines()[0]) + self.assertIn("browser-use-visual-check", release) + def test_versioned_moli_slots_reject_wrong_or_missing_binaries(self) -> None: valid = { "moli-1.0.6": {"reported_version": "moli 1.0.6"}, From 50a3812f55467892c2145e1cd9284b54bcc321b9 Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 11 Sep 2026 19:15:08 +0800 Subject: [PATCH 064/127] Turn browser use report into diagnostic workbench --- AGENTS.md | 4 + harness/browser_use/report.py | 472 ++++++++++++++++++-------- harness/browser_use/report_i18n.py | 49 +++ research/browser-use-remediation.json | 37 ++ research/report-changes.json | 69 ++++ scripts/check_browser_use_report.py | 77 ++++- scripts/check_browser_use_visual.py | 9 + tests/browser_use/test_report.py | 12 +- tests/browser_use/test_report_ui.mjs | 51 ++- 9 files changed, 605 insertions(+), 175 deletions(-) create mode 100644 research/browser-use-remediation.json diff --git a/AGENTS.md b/AGENTS.md index 47247b679..5ef498ec3 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -20,6 +20,10 @@ - Put a core-metrics comparison table near the start of every report home page, before secondary diagnosis or interpretation. Prefer tables over repeated cards for dense categorical summaries. Generate its rows and columns from the participant registry rather than assuming a fixed participant count. - Use reader-facing labels throughout reports. Do not publish unexplained internal terms or shorthand; Chinese copy must be understandable to a general reader without benchmark implementation context. State the population, denominator, and outcome relation where a compact label would otherwise be ambiguous. - Result-detail summaries contain only the verdict, concise failed-rule labels and short scalar facts. Full evaluator messages, expected/actual payloads, traces and logs belong in dedicated evidence sections, never narrow KPI cards. +- Result inventories are fixed-height, horizontally scrollable diagnosis tables. Do not place full reviewed causes, evaluator payloads, latency stacks or other paragraph-length content in inventory cells; link to the result detail instead. +- Compare every registered model-by-browser configuration in one same-task matrix generated from the participant registry. Do not introduce pairwise selectors or duplicate comparison prose when more configurations can be represented as columns. +- Every failed result detail must expose the evidenced failure stage, the programmatic evaluator classification, the reviewed root cause, the remediation owner/status, a link to the relevant trajectory step, downloadable sanitized logs, and the last public trace frames when available. Label trace frames as last observed pages, not as exact error screenshots unless the evidence proves that timing. +- Keep official scores immutable when diagnosis finds benchmark/evaluator, site, shared-environment, or frozen-reference interference. Quarantine affected tasks across all configurations in a clearly labeled post-hoc diagnostic subset, record remediation in `research/browser-use-remediation.json`, and readmit them only after a preregistered full-scope rerun. - Split environment preparation, browser-session setup, Agent run, BrowserTool wall time, evidence finalization and evaluator time at collection. Publish the self-hosted deployment topology and image identity; claim per-execution reset/readiness only when a preparation receipt binds reset, readiness and application health. - Formal report prose is reader-facing: no prompts, reasoning, internal work narration, conversational rebuttals or references to parallel sessions. Review pages contain review rounds and corrections, not per-case audit tables; release notes contain reader-visible effects. - Before release, inspect every core route class in Chinese and English at desktop and narrow widths in both themes. Prevent vertical table labels, clipped controls and unreadable evidence; use ImageGen raster assets or Mermaid rather than SVG for Browser Use diagrams. diff --git a/harness/browser_use/report.py b/harness/browser_use/report.py index 1aef55d9d..4b6e08f5f 100644 --- a/harness/browser_use/report.py +++ b/harness/browser_use/report.py @@ -11,6 +11,7 @@ import shutil import statistics import tempfile +import zipfile from collections import Counter, defaultdict from collections.abc import Callable, Iterable from pathlib import Path @@ -748,12 +749,12 @@ def _task_family_table(families: list[dict[str, Any]]) -> str: def _failure_cause_table(verified_causes: dict[str, dict[str, Any]]) -> str: groups = { - "agent": ("Agent 决策或操作", "检索不完整、候选选择错误、走错页面、字段遗漏,或没有在步骤上限内完成。"), - "benchmark": ("任务或 evaluator", "任务要求与验收条件冲突,或 evaluator 的路由、表单和实现规则有缺陷。"), - "site": ("测试网站", "自托管网站的依赖、会话或保存接口失败。"), - "evidence": ("浏览器证据记录", "页面操作可能成功,但网络记录缺少响应正文或 Referer,evaluator 无法确认。"), - "environment": ("共享测试环境", "并发运行改写了同一网站状态;运行器现已串行化,历史结果仍按原证据保留。"), - "mixed": ("多项原因", "同一结果同时包含 Agent 遗漏和冻结参考状态差异。"), + "agent": ("Agent / 模型", "是", "保留为对比信号;通过轨迹修正检索、选择和提交策略。"), + "benchmark": ("任务 / evaluator", "否", "修正任务合同或 evaluator,验证后重跑;重跑前不进入干净子集。"), + "site": ("自托管站点", "否", "修复依赖、会话或保存链路,增加任务级写入预检后重跑。"), + "evidence": ("浏览器证据接口", "是", "保留为浏览器集成信号;补齐响应正文或 Referer 采集后重跑。"), + "environment": ("共享测试环境", "否", "跨进程串行锁已上线;受污染任务待重跑。"), + "mixed": ("参考状态 + Agent", "否", "先重新冻结参考状态,再重跑区分 Agent 错误。"), } def group_for(code: str) -> str: @@ -774,19 +775,53 @@ def group_for(code: str) -> str: total = sum(counts[key].values()) if not total: continue - details = ";".join( - f"{label} {count}" - for label, count in sorted(counts[key].items(), key=lambda item: (-item[1], item[0])) - ) - rows.append([groups[key][0], total, details, groups[key][1]]) + rows.append([groups[key][0], total, groups[key][1], groups[key][2]]) return _table( - ["原因所在", "失败结果数", "具体分类", "说明"], + ["原因所在", "失败结果数", "是否属于目标变量", "处理"], rows, table_id="failure-cause-table", classes="wide-data-table", ) +def _external_interference_task_ids( + verified_causes: dict[str, dict[str, Any]], +) -> set[int]: + prefixes = ("benchmark_", "site_", "environment_", "mixed_") + return { + int(result_id.rsplit("-", 1)[-1]) + for result_id in verified_causes + if _cause_code(verified_causes, result_id).startswith(prefixes) + } + + +def _clean_subset_table( + attempts: list[dict[str, Any]], verified_causes: dict[str, dict[str, Any]] +) -> str: + excluded = _external_interference_task_ids(verified_causes) + kept = [row for row in attempts if int(row["task_id"]) not in excluded] + summary = _primary_summary(kept) + return ( + _table( + ["模型", "浏览器", "通过任务", "通过率"], + [ + [ + MODEL_LABELS[model], + ENGINE_LABELS[engine], + f"{summary[model][engine]['passed']}/{summary[model][engine]['completed']}", + _pct(summary[model][engine]["score"]), + ] + for model in MODEL_LABELS + for engine in ENGINE_LABELS + ], + table_id="clean-subset-table", + classes="core-comparison-table", + ) + + '

    这是事后诊断口径,不是官方新分数。它按任务统一剔除任一配置已证实受任务/evaluator、站点、共享环境或参考状态干扰的任务,保证四个配置分母一致。' + f'本轮剔除 {len(excluded)} 题:{_e("、".join(f"#{task_id}" for task_id in sorted(excluded)))}。

    ' + ) + + def _headline_findings(summary: dict[str, dict[str, dict[str, Any]]]) -> str: lines = [] engines = list(ENGINE_LABELS) @@ -844,91 +879,74 @@ def _adoption_top3(evidence: dict[str, Any]) -> str: def _same_task_comparison(tasks: list[dict[str, Any]], attempts: list[dict[str, Any]]) -> str: + """Render one task-by-configuration matrix; never reduce it to pairwise controls.""" + results = {(_row_model(row), str(row["engine"]), int(row["task_id"])): row for row in attempts} - ordered = [BASELINE_ENGINE, *(engine for engine in ENGINE_LABELS if engine != BASELINE_ENGINE)] - if len(ordered) < 2: - return '

    至少需要两个参评浏览器才能进行同题比较。

    ' + engines = [BASELINE_ENGINE, *(engine for engine in ENGINE_LABELS if engine != BASELINE_ENGINE)] + configurations = [(model, engine) for model in MODEL_LABELS for engine in engines] + if len(configurations) < 2: + return '

    至少需要两个参评配置才能进行同题比较。

    ' rows = [] - for model in MODEL_LABELS: - for task in tasks: - task_id = int(task["task_id"]) - present = [engine for engine in ordered if (model, engine, task_id) in results] - passed = [ - engine - for engine in present - if float(results[(model, engine, task_id)]["score"]) == 1 - ] - failed = [engine for engine in present if engine not in passed] - - def outcome_links( - engines: list[str], state: str, *, model: str = model, task_id: int = task_id - ) -> str: - if not engines: - return "—" - return ( - '' - + "".join( - f'' - f"{_e(ENGINE_LABELS[engine])}" - for engine in engines - ) - + "" - ) - - site = str(task["sites"][0]) - attrs = ( - f' data-model="{_e(model)}" data-pass="{_e(" ".join(passed))}"' - f' data-fail="{_e(" ".join(failed))}" data-kind="{_e(task["task_type"])}"' - ) - task_cell = ( - f'
    #{task_id} · ' - f'{_e(task["rationale"])}
    ' - f'{_e(MODEL_LABELS[model])}' - f'{_e(TIER_LABELS[task["tier"]])}' - f'{_e(SITE_LABELS.get(site, site))}' - f'{_e(task["task_type"])}
    ' - ) - rows.append( - f"{task_cell}{outcome_links(passed, 'pass')}" - f"{outcome_links(failed, 'fail')}" + for task in tasks: + task_id = int(task["task_id"]) + states = [] + cells = [] + for model, engine in configurations: + result = results.get((model, engine, task_id)) + if result is None: + state, label = "missing", "未测" + elif float(result["score"]) == 1: + state, label = "pass", "通过" + else: + state, label = "fail", "失败" + states.append(state) + cells.append( + f'' + f"{label}" + if result is not None + else '未测' ) + present = [state for state in states if state != "missing"] + relation = ( + "all_pass" + if present and all(state == "pass" for state in present) + else "all_fail" + if present and all(state == "fail" for state in present) + else "mixed" + ) + site = str(task["sites"][0]) + task_cell = ( + f'
    #{task_id} · ' + f'{_e(task["rationale"])}
    ' + f'{_e(TIER_LABELS[task["tier"]])}' + f'{_e(SITE_LABELS.get(site, site))}' + f'{_e(task["task_type"])}
    ' + ) + rows.append( + f'' + f"{task_cell}{''.join(cells)}" + ) - options = "".join( - f'" - for engine in ordered - ) - checks = "".join( - f'" - for engine in ordered - ) - kinds = '' + "".join( - f'' for kind in ("RETRIEVE", "NAVIGATE", "MUTATE") + headers = "".join( + f"{_e(MODEL_LABELS[model])}
    {_e(ENGINE_LABELS[engine])}" + for model, engine in configurations ) return ( - '
    ' - "

    同题结果组合

    选择一个基准浏览器和一个或多个对比浏览器;浏览器标签可直达该题完整结果。

    " - '
    ' - '
    ' - '" - f'' - '
    ' - '
    对比浏览器(可多选)' - f'
    {checks}
    ' - '
    ' - "" - f'{"".join(rows)}
    任务通过浏览器失败浏览器
    ' + '
    ' + "

    同题全配置矩阵

    每行是同一任务,每列是一个模型×浏览器配置。新增配置只增加列。

    " + '
    ' + '
    ' + '' + '' + '
    ' + '
    ' + f"{headers}" + f'{"".join(rows)}
    任务
    ' ) @@ -1131,15 +1149,14 @@ def _summary_comparison_rows(summary: dict[str, dict[str, dict[str, Any]]]) -> l REPORT_JS = """function theme(){const r=document.documentElement,n=r.dataset.theme==='dark'?'light':'dark';r.dataset.theme=n;try{localStorage.setItem('browser-use-theme',n)}catch(e){}} function updateViewUrl(){const u=new URL(location.href),q=document.querySelector('[data-search]');[...u.searchParams.keys()].filter(k=>k==='q'||k.startsWith('f_')).forEach(k=>u.searchParams.delete(k));if(q?.value)u.searchParams.set('q',q.value);document.querySelectorAll('[data-filter]').forEach(c=>{if(c.value)u.searchParams.set('f_'+c.dataset.filter,c.value)});history.replaceState(null,'',u.pathname+(u.search?'?'+u.searchParams.toString():'')+u.hash)} -function filterRows(save=true){const controls=[...document.querySelectorAll('[data-filter]')],query=(document.querySelector('[data-search]')?.value||'').trim().toLowerCase();let visible=0,total=0;document.querySelectorAll('tbody tr[data-tier]').forEach(r=>{total++;const hidden=Boolean(controls.some(c=>c.value&&r.dataset[c.dataset.filter]!==c.value)||(query&&!String(r.dataset.search||r.textContent).toLowerCase().includes(query)));r.hidden=hidden;if(!hidden)visible++});document.querySelectorAll('[data-visible-count]').forEach(x=>x.textContent=visible+' / '+total);if(save)updateViewUrl()} +function filterRows(save=true){const controls=[...document.querySelectorAll('[data-filter]')],query=(document.querySelector('[data-search]')?.value||'').trim().toLowerCase();let visible=0,total=0;document.querySelectorAll('tbody tr[data-tier]:not([data-relation])').forEach(r=>{total++;const hidden=Boolean(controls.some(c=>c.value&&r.dataset[c.dataset.filter]!==c.value)||(query&&!String(r.dataset.search||r.textContent).toLowerCase().includes(query)));r.hidden=hidden;if(!hidden)visible++});document.querySelectorAll('[data-visible-count]').forEach(x=>x.textContent=visible+' / '+total);if(save)updateViewUrl()} +function filterMatrix(){const root=document.getElementById('configuration-matrix');if(!root)return;const controls=[...root.querySelectorAll('[data-matrix-filter]')];let visible=0,total=0;root.querySelectorAll('tbody tr[data-tier]').forEach(r=>{total++;const hidden=controls.some(c=>c.value&&r.dataset[c.dataset.matrixFilter]!==c.value);r.hidden=hidden;if(!hidden)visible++});const count=root.querySelector('[data-matrix-count]');if(count)count.textContent=visible+' / '+total} function applySort(t,col,dir){const b=t?.tBodies[0];if(!b)return;[...b.rows].sort((a,z)=>{const av=a.cells[col]?.dataset.sort||a.cells[col]?.textContent||'',zv=z.cells[col]?.dataset.sort||z.cells[col]?.textContent||'';const an=Number(av.replace(/[^0-9.-]/g,'')),zn=Number(zv.replace(/[^0-9.-]/g,''));const cmp=!Number.isNaN(an)&&!Number.isNaN(zn)?an-zn:av.localeCompare(zv,'zh-CN');return dir==='asc'?cmp:-cmp}).forEach(r=>b.appendChild(r));t.dataset.col=String(col);t.dataset.dir=dir} function sortTable(id,col){const t=document.getElementById(id);if(!t)return;const dir=t.dataset.col==col&&t.dataset.dir==='asc'?'desc':'asc';applySort(t,col,dir);const u=new URL(location.href);u.searchParams.set('sort',id+':'+col+':'+dir);history.replaceState(null,'',u.pathname+'?'+u.searchParams.toString()+u.hash)} -function browserOutcomes(row,key){return new Set((row.dataset[key]||'').split(' ').filter(Boolean))} -function compareBrowsers(save=true){const model=document.getElementById('browser-model'),a=document.getElementById('browser-a'),relation=document.getElementById('browser-relation'),kind=document.getElementById('browser-kind'),count=document.getElementById('browser-compare-count');if(!model||!a||!relation||!kind||!count)return;const boxes=[...document.querySelectorAll('[data-compare-engine]')];boxes.forEach(box=>{box.disabled=box.value===a.value;box.closest('label')?.classList.toggle('disabled',box.disabled);if(box.disabled)box.checked=false});const others=boxes.filter(box=>box.checked&&!box.disabled).map(box=>box.value),selected=[a.value,...others];let shown=0;document.querySelectorAll('#browser-compare-rows tr').forEach(row=>{const pass=browserOutcomes(row,'pass'),fail=browserOutcomes(row,'fail');let match=row.dataset.model===model.value&&others.length>0&&(!kind.value||row.dataset.kind===kind.value);if(match)match={mixed:selected.some(x=>pass.has(x))&&selected.some(x=>fail.has(x)),a_pass_others_fail:pass.has(a.value)&&others.every(x=>fail.has(x)),a_fail_others_pass:fail.has(a.value)&&others.every(x=>pass.has(x)),all_pass:selected.every(x=>pass.has(x)),all_fail:selected.every(x=>fail.has(x)),exact_pass:selected.every(x=>pass.has(x))&&pass.size===selected.length}[relation.value];row.hidden=!match;if(match)shown++});count.textContent=others.length?(document.documentElement.lang==='en'?shown+' tasks found':'找到 '+shown+' 个任务'):(document.documentElement.lang==='en'?'Select at least one comparison browser':'请至少选择一个对比浏览器');if(save){const u=new URL(location.href);['b_model','b_a','b_relation','b_kind','b_others'].forEach(k=>u.searchParams.delete(k));u.searchParams.set('b_model',model.value);u.searchParams.set('b_a',a.value);u.searchParams.set('b_relation',relation.value);if(kind.value)u.searchParams.set('b_kind',kind.value);if(others.length)u.searchParams.set('b_others',others.join(','));history.replaceState(null,'',u.pathname+(u.search?'?'+u.searchParams.toString():'')+u.hash)}} function applyAnchor(){if(!location.hash)return;const id=decodeURIComponent(location.hash.slice(1)),target=document.getElementById(id);if(target?.scrollIntoView)requestAnimationFrame(()=>target.scrollIntoView({block:'start'}))} function hasOption(control,value){return !control?.options||[...control.options].some(option=>option.value===value)} -function restoreView(){const p=new URLSearchParams(location.search),q=document.querySelector('[data-search]');if(q&&p.has('q'))q.value=p.get('q')||'';document.querySelectorAll('[data-filter]').forEach(c=>{const v=p.get('f_'+c.dataset.filter);if(v!==null&&hasOption(c,v))c.value=v});const model=document.getElementById('browser-model'),a=document.getElementById('browser-a'),relation=document.getElementById('browser-relation'),kind=document.getElementById('browser-kind'),boxes=[...document.querySelectorAll('[data-compare-engine]')],allowed=new Set(boxes.map(box=>box.value)),others=new Set((p.get('b_others')||'').split(',').filter(value=>allowed.has(value)));if(model&&p.has('b_model')&&hasOption(model,p.get('b_model')))model.value=p.get('b_model');if(a&&p.has('b_a')&&hasOption(a,p.get('b_a')))a.value=p.get('b_a');if(relation&&p.has('b_relation')&&hasOption(relation,p.get('b_relation')))relation.value=p.get('b_relation');if(kind&&p.has('b_kind')&&hasOption(kind,p.get('b_kind')))kind.value=p.get('b_kind');boxes.forEach(box=>{if(p.has('b_others'))box.checked=others.has(box.value)});if(boxes.length>1&&!boxes.some(box=>box.checked&&box.value!==a?.value)){const fallback=boxes.find(box=>box.value!==a?.value);if(fallback)fallback.checked=true}const s=(p.get('sort')||'').split(':');if(s.length===3)applySort(document.getElementById(s[0]),Number(s[1]),s[2]);filterRows(false);compareBrowsers(false);try{if(!location.hash){const y=sessionStorage.getItem('browser-use-scroll:'+location.pathname+location.search);if(y!==null)requestAnimationFrame(()=>scrollTo(0,Number(y)||0))}}catch(e){}} -document.addEventListener('DOMContentLoaded',()=>{try{document.documentElement.dataset.theme=localStorage.getItem('browser-use-theme')||'light'}catch(e){}restoreView();applyAnchor();document.querySelectorAll('[data-filter]').forEach(x=>x.addEventListener('change',()=>filterRows()));document.querySelectorAll('[data-search]').forEach(x=>x.addEventListener('input',()=>filterRows()));document.querySelectorAll('[data-browser-compare-control],[data-compare-engine]').forEach(x=>x.addEventListener('change',()=>compareBrowsers()))});addEventListener('pagehide',()=>{try{sessionStorage.setItem('browser-use-scroll:'+location.pathname+location.search,String(scrollY))}catch(e){}});""" +function restoreView(){const p=new URLSearchParams(location.search),q=document.querySelector('[data-search]');if(q&&p.has('q'))q.value=p.get('q')||'';document.querySelectorAll('[data-filter]').forEach(c=>{const v=p.get('f_'+c.dataset.filter);if(v!==null&&hasOption(c,v))c.value=v});const s=(p.get('sort')||'').split(':');if(s.length===3)applySort(document.getElementById(s[0]),Number(s[1]),s[2]);filterRows(false);filterMatrix();try{if(!location.hash){const y=sessionStorage.getItem('browser-use-scroll:'+location.pathname+location.search);if(y!==null)requestAnimationFrame(()=>scrollTo(0,Number(y)||0))}}catch(e){}} +document.addEventListener('DOMContentLoaded',()=>{try{document.documentElement.dataset.theme=localStorage.getItem('browser-use-theme')||'light'}catch(e){}restoreView();applyAnchor();document.querySelectorAll('[data-filter]').forEach(x=>x.addEventListener('change',()=>filterRows()));document.querySelectorAll('[data-search]').forEach(x=>x.addEventListener('input',()=>filterRows()));document.querySelectorAll('[data-matrix-filter]').forEach(x=>x.addEventListener('change',filterMatrix))});addEventListener('pagehide',()=>{try{sessionStorage.setItem('browser-use-scroll:'+location.pathname+location.search,String(scrollY))}catch(e){}});""" REPORT_STYLE = ( SHARED_STYLE + """ @@ -1162,7 +1179,18 @@ def _summary_comparison_rows(summary: dict[str, dict[str, dict[str, Any]]]) -> l #pair-table th:nth-child(-n+4),#pair-table td:nth-child(-n+4){white-space:nowrap} #pair-table th:nth-child(1),#pair-table td:nth-child(1){min-width:260px;white-space:normal} #pair-table th:nth-child(n+5),#pair-table td:nth-child(n+5){min-width:235px} -#result-table,#engine-result-table,#change-table{min-width:1080px} +#result-table{min-width:1580px;table-layout:fixed} +#result-table th,#result-table td{white-space:nowrap;overflow:hidden;text-overflow:ellipsis;vertical-align:middle;height:72px;max-height:72px} +#result-table th:nth-child(1),#result-table td:nth-child(1){width:230px} +#result-table th:nth-child(2),#result-table td:nth-child(2){width:170px} +#result-table th:nth-child(3),#result-table td:nth-child(3){width:80px} +#result-table th:nth-child(4),#result-table td:nth-child(4){width:75px} +#result-table th:nth-child(5),#result-table td:nth-child(5){width:250px} +#result-table th:nth-child(6),#result-table td:nth-child(6){width:180px} +#result-table th:nth-child(7),#result-table td:nth-child(7){width:180px} +#result-table th:nth-child(8),#result-table td:nth-child(8){width:310px} +#result-table th:nth-child(9),#result-table td:nth-child(9){width:170px} +#engine-result-table,#change-table{min-width:1080px} #result-table td:first-child,#engine-result-table td:first-child{white-space:nowrap;min-width:150px} .json-view,.log-view{background:var(--code-bg)!important;color:var(--code-ink)!important;border-color:#334b68!important;line-height:1.65} .json-view code,.log-view code{color:inherit!important;background:transparent!important} @@ -1174,14 +1202,15 @@ def _summary_comparison_rows(summary: dict[str, dict[str, dict[str, Any]]]) -> l .release-current{border-top:4px solid var(--accent)} .release-history>summary,.release-current details>summary,.release-archive details>summary{cursor:pointer;font-weight:800;padding:8px 0} .comparison-winner{color:var(--good);font-weight:850}.winner-mark{display:inline-grid;place-items:center;min-width:22px;height:22px;padding:0 5px;margin-left:7px;border-radius:999px;background:var(--good);color:white;font-size:11px;line-height:1}.winner-cell{position:relative;padding:9px 10px;border:1px solid color-mix(in srgb,var(--good) 42%,var(--line));border-radius:10px;background:color-mix(in srgb,var(--good) 9%,var(--panel))}.metric-stack{display:grid;gap:5px;min-width:150px}.metric-stack>span{display:flex;align-items:center;justify-content:space-between;gap:12px;white-space:nowrap}.metric-stack>span>b{font-size:12px;color:var(--muted)}.review-round{border:1px solid var(--line);border-radius:16px;background:var(--panel);padding:22px;margin:16px 0}.review-round header{display:flex;align-items:start;justify-content:space-between;gap:18px}.review-round h3{margin:5px 0}.review-status{flex:0 0 auto;padding:5px 10px;border-radius:999px;background:var(--wash);font-size:12px;font-weight:800}.review-fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;margin-top:16px}.review-fields>div{border-top:1px solid var(--line);padding-top:12px}.review-fields b{display:block;margin-bottom:5px}.review-fields p{margin:0} -.browser-comparison{border:1px solid var(--line);border-radius:18px;background:var(--panel);padding:24px;box-shadow:0 12px 32px rgba(25,45,70,.06)}.comparison-heading{display:flex;align-items:start;justify-content:space-between;gap:24px;margin-bottom:22px}.comparison-heading h3{margin:0 0 6px;font-size:22px}.comparison-heading p{margin:0;color:var(--muted);line-height:1.65}.comparison-controls{display:grid;grid-template-columns:minmax(170px,1fr) minmax(240px,1.4fr) minmax(170px,1fr);gap:14px}.comparison-controls label{display:grid;gap:6px;font-weight:750}.comparison-controls select{width:100%;padding:11px 12px;border:1px solid var(--line);border-radius:10px;background:var(--bg);color:var(--ink);font:inherit}.comparison-browsers{border:0;padding:0;margin:20px 0 0}.comparison-browsers legend{font-weight:750;margin-bottom:10px}.browser-checks{display:flex;gap:8px;flex-wrap:wrap}.browser-checks label{display:flex;align-items:center;gap:7px;padding:8px 11px;border:1px solid var(--line);border-radius:999px;background:var(--bg);cursor:pointer}.browser-checks label.disabled{opacity:.45;cursor:not-allowed}.browser-checks input{accent-color:var(--accent)}.comparison-result{margin-top:20px}.table-wrap .comparison-table{min-width:720px;table-layout:fixed}.comparison-table th:first-child,.comparison-table td:first-child{width:48%}.comparison-table th:nth-child(2),.comparison-table td:nth-child(2),.comparison-table th:nth-child(3),.comparison-table td:nth-child(3){width:26%}.task-cell a{font-weight:750}.task-meta{display:flex;align-items:center;gap:7px;flex-wrap:wrap;margin-top:7px}.track-badge{display:inline-block;padding:3px 8px;border-radius:999px;background:var(--panel2);color:var(--muted);font-size:12px;white-space:nowrap}.outcome-list{display:flex;gap:6px;flex-wrap:wrap}.outcome-pill{display:inline-block;padding:5px 8px;border-radius:8px;font-size:13px;text-decoration:none}.outcome-pill.pass{background:var(--success-bg);color:var(--good)}.outcome-pill.fail{background:var(--failure-bg);color:var(--bad)} +.browser-comparison{border:1px solid var(--line);border-radius:18px;background:var(--panel);padding:24px;box-shadow:0 12px 32px rgba(25,45,70,.06)}.comparison-heading{display:flex;align-items:start;justify-content:space-between;gap:24px;margin-bottom:10px}.comparison-heading h3{margin:0 0 6px;font-size:22px}.comparison-heading p{margin:0;color:var(--muted);line-height:1.65}.matrix-filters{margin:16px 0}.comparison-result{margin-top:12px}.table-wrap .configuration-matrix{min-width:1120px;table-layout:fixed}.configuration-matrix th:first-child,.configuration-matrix td:first-child{width:420px}.configuration-matrix th:not(:first-child),.configuration-matrix td:not(:first-child){width:165px;text-align:center;vertical-align:middle}.task-cell a{font-weight:750}.task-meta{display:flex;align-items:center;gap:7px;flex-wrap:wrap;margin-top:7px}.track-badge{display:inline-block;padding:3px 8px;border-radius:999px;background:var(--panel2);color:var(--muted);font-size:12px;white-space:nowrap}.outcome-pill{display:inline-block;min-width:62px;padding:6px 10px;border-radius:8px;font-size:13px;text-decoration:none;font-weight:750}.outcome-pill.pass{background:var(--success-bg);color:var(--good)}.outcome-pill.fail{background:var(--failure-bg);color:var(--bad)}.outcome-pill.missing{background:var(--panel2);color:var(--muted)} .action-card-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.action-card{border:1px solid var(--line);border-radius:16px;background:var(--panel);padding:18px;min-width:0}.action-card>header{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:14px}.action-card>header code{font-size:16px;font-weight:800}.action-card>header span{font-size:12px;color:var(--muted)}.action-browser-row{padding:12px 0;border-top:1px solid var(--line)}.action-browser-head{display:flex;align-items:center;justify-content:space-between;gap:12px}.action-bar{height:7px;border-radius:999px;background:var(--panel2);overflow:hidden;margin:8px 0}.action-bar i{display:block;height:100%;border-radius:inherit;background:var(--accent)}.action-browser-row.action-winner .action-bar i{background:var(--good)}.action-browser-meta{font-size:12px;color:var(--muted)} .finding-list{display:grid;gap:8px;margin:0 0 20px;padding:16px 20px 16px 38px;border-left:4px solid var(--accent);border-radius:0 12px 12px 0;background:var(--wash)}.finding-list li{padding-left:4px}.table-kicker{display:block;color:var(--muted);font:700 11px/1.4 ui-monospace,monospace;letter-spacing:.04em;margin-top:3px}.core-comparison-table{min-width:1040px}.core-comparison-table td:nth-child(1),.core-comparison-table td:nth-child(2){font-weight:750}.pair-outcome-table{min-width:900px}.wide-data-table td{line-height:1.65}.report-entry-links{margin-top:18px}.report-entry-links a{font-weight:750}.source-note a{margin-left:5px} .attempt-summary{display:grid;grid-template-columns:minmax(270px,1.4fr) repeat(4,minmax(135px,.7fr));gap:12px;align-items:stretch}.attempt-verdict{padding:20px 22px;border:1px solid var(--line);border-left:5px solid var(--good);border-radius:12px;background:var(--success-bg)}.attempt-verdict.failure{border-left-color:var(--bad);background:var(--failure-bg)}.attempt-verdict .status-line{display:block;font-size:24px;font-weight:850;margin-bottom:6px}.attempt-verdict p{margin:0;line-height:1.65}.attempt-jumps{display:flex;gap:14px;flex-wrap:wrap;margin-top:14px;font-weight:750}.attempt-fact{padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--panel);overflow-wrap:anywhere}.attempt-fact b{display:block;color:var(--muted);font-size:12px;margin-bottom:5px}.attempt-secondary{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-top:12px} +.diagnosis-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}.diagnosis-grid .attempt-fact{min-height:110px}.diagnosis-reason{margin-top:14px;padding:18px 20px;border-left:5px solid var(--bad);border-radius:10px;background:var(--failure-bg);line-height:1.7}.evidence-links{display:flex;gap:8px}.screenshot-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.screenshot-grid figure{margin:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel)}.screenshot-grid img{display:block;width:100%;aspect-ratio:16/10;object-fit:cover;object-position:top}.screenshot-grid figcaption{padding:10px 12px;color:var(--muted);font-size:12px} .task-goal{padding:22px 24px;border-radius:16px;background:var(--wash);border:1px solid var(--line);margin-bottom:18px}.task-goal span,.rule-index{display:block;color:var(--accent);font-size:12px;font-weight:850;letter-spacing:.08em;text-transform:uppercase}.task-goal p{font-size:20px;font-weight:750;line-height:1.6;margin:7px 0}.task-goal small{color:var(--muted)}.verification-list{display:grid;gap:14px}.verification-rule{padding:20px 22px;border:1px solid var(--line);border-left:5px solid var(--good);border-radius:14px;background:var(--panel);min-width:0}.verification-rule.rule-fail{border-left-color:var(--bad)}.verification-rule>header{display:flex;align-items:start;justify-content:space-between;gap:18px}.verification-rule h3{margin:4px 0 0;font-size:19px}.rule-verdict{flex:0 0 auto;padding:5px 10px;border-radius:999px;font-size:12px;font-weight:850}.rule-verdict.pass{background:var(--success-bg);color:var(--good)}.rule-verdict.fail{background:var(--failure-bg);color:var(--bad)}.rule-purpose{color:var(--muted);margin:12px 0 6px}.rule-summary{font-size:17px;font-weight:750;margin:0 0 14px}.verification-rule details{border-top:1px solid var(--line);padding-top:12px;margin-top:10px}.verification-rule summary{cursor:pointer;font-weight:750}.compact-evidence{margin-top:12px}.compact-evidence .json-view{max-height:360px} .page-main{overflow-x:clip} .section[id]{scroll-margin-top:76px} -@media(max-width:980px){.attempt-summary{grid-template-columns:repeat(2,minmax(0,1fr))}.attempt-verdict{grid-column:1/-1}.attempt-secondary{grid-template-columns:repeat(2,minmax(0,1fr))}} +@media(max-width:980px){.attempt-summary{grid-template-columns:repeat(2,minmax(0,1fr))}.attempt-verdict{grid-column:1/-1}.attempt-secondary,.diagnosis-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.screenshot-grid{grid-template-columns:1fr}} @media(max-width:760px){.browser-comparison{padding:18px}.comparison-heading{display:block}.comparison-count{display:inline-block;margin-top:12px}.comparison-controls{grid-template-columns:1fr}.action-card-grid{grid-template-columns:1fr}} @media(max-width:600px){.hero h1{word-break:break-all;line-break:anywhere;text-wrap:balance}.method-figure img{min-width:900px}.table-wrap{margin-left:-4px;margin-right:-4px}.json-view,.log-view{font-size:13px}.review-round header{display:block}.review-status{display:inline-block;margin-top:8px}.review-fields,.attempt-summary,.attempt-secondary{grid-template-columns:1fr}} """ @@ -1355,6 +1384,55 @@ def _automatic_failure_type(result: dict[str, Any] | None) -> tuple[str, str]: return "mixed", "多项验收失败" +def _cause_code(verified: dict[str, dict[str, Any]], result_id: str) -> str: + return str(((verified.get(result_id) or {}).get("subcategory") or {}).get("code") or "") + + +def _failure_point(code: str, steps: list[dict[str, Any]]) -> tuple[str, int | None]: + """Locate the evidenced stage without inventing a precise failing click.""" + + failed_step = next( + ( + step + for step in reversed(steps) + if str(step.get("status") or "").lower() not in {"", "success"} + or step.get("error") + ), + None, + ) + last_step = int(steps[-1].get("step") or len(steps)) if steps else None + if code.startswith("environment_"): + return "环境准备后、Agent 读取前", None + if code.startswith("benchmark_"): + return "运行结束后的 evaluator 验收", last_step + if code.startswith("evidence_") or code == "evidence_capture": + return "运行结束后的网络证据验收", last_step + if code.startswith("site_"): + return (f"第 {last_step} 步后的站点响应/保存" if last_step else "站点响应/保存"), last_step + if failed_step: + number = int(failed_step.get("step") or last_step or 1) + return f"第 {number} 步 BrowserTool 执行异常", number + if code == "agent_step_budget": + return (f"第 {last_step} 步后达到步骤上限" if last_step else "达到步骤上限"), last_step + return (f"第 {last_step} 步后的最终回答/任务状态" if last_step else "最终回答/任务状态"), last_step + + +def _remediation(code: str) -> tuple[str, str, bool]: + """Return owner, current action, and whether this is an external interference.""" + + if code.startswith("benchmark_"): + return "评测集 / evaluator", "修正合同并重跑;当前仅纳入官方分数", True + if code.startswith("site_"): + return "自托管站点", "修复依赖或保存链路,通过写入预检后重跑", True + if code.startswith("environment_"): + return "运行环境", "跨进程锁已上线;受污染任务待重跑", True + if code.startswith("mixed_"): + return "任务与 Agent", "先冻结参考状态,再重跑区分 Agent 错误", True + if code.startswith("evidence_") or code == "evidence_capture": + return "浏览器证据接口", "保留为浏览器集成信号;补齐 HAR 后重跑", False + return "Agent / 模型", "保留为目标变量;按轨迹修正策略或工具合同", False + + def _failure_brief(result: dict[str, Any] | None, row: dict[str, Any]) -> str: labels = _failed_rule_labels(result) if labels: @@ -1452,12 +1530,13 @@ def _trajectory_html(steps: list[dict[str, Any]]) -> str: return '

    没有可展示的 BrowserTool 调用。

    ' rows = [] for step in steps: + step_number = int(step.get("step") or len(rows) + 1) duration = step.get("duration_seconds") meta = f"Turn {step.get('turn') or '—'} · {_e(step.get('status') or 'unknown')}" if duration is not None: meta += f" · {float(duration):.2f} 秒" rows.append( - "
  • " + f'
  • ' + _e(trajectory_label(step)) + f'

    {meta}

    调用参数

    ' + _json_pre(step.get("arguments")) @@ -1473,6 +1552,40 @@ def _trajectory_html(steps: list[dict[str, Any]]) -> str: } +def _extract_trace_screenshots( + trace_path: Path, artifact_dir: Path, *, limit: int = 3 +) -> list[dict[str, Any]]: + """Publish only the last raster frames, never the cookie-bearing trace archive.""" + + if not trace_path.is_file(): + return [] + try: + with zipfile.ZipFile(trace_path) as archive: + names = [ + name + for name in archive.namelist() + if name.startswith("resources/") + and name.lower().endswith((".jpeg", ".jpg", ".png")) + ][-limit:] + output = [] + screenshot_dir = artifact_dir / "screenshots" + screenshot_dir.mkdir(parents=True, exist_ok=True) + for index, name in enumerate(names, start=1): + suffix = ".png" if name.lower().endswith(".png") else ".jpeg" + destination = screenshot_dir / f"frame-{index}{suffix}" + destination.write_bytes(archive.read(name)) + output.append( + { + "path": f"screenshots/{destination.name}", + "sha256": _sha256(destination), + "label": f"最后页面画面 {index}/{len(names)}", + } + ) + return output + except (OSError, KeyError, zipfile.BadZipFile): + return [] + + def _artifact_links(result_id: str, execution: int) -> str: root = f"artifacts/{result_id}/executions/{execution}" return ( @@ -1498,6 +1611,11 @@ def _write_execution_artifacts( artifact_dir = stage / "artifacts" / result_id / "executions" / str(execution) artifact_dir.mkdir(parents=True) measurements = _public_execution(row, authoritative=authoritative) + screenshots = ( + _extract_trace_screenshots(Path(paths.get("agent_trace", "")), artifact_dir) + if authoritative and float(row.get("score") or 0) < 1 + else [] + ) bundle = { "schema": "lex_browser_use_public_execution_evidence/3", "result_id": result_id, @@ -1509,6 +1627,7 @@ def _write_execution_artifacts( "evaluation": evaluation, "agent_events": events, "trajectory": steps, + "screenshots": screenshots, } _write_json(artifact_dir / PUBLIC_EXECUTION_ARTIFACTS["execution_evidence"], bundle) artifacts = { @@ -1518,7 +1637,33 @@ def _write_execution_artifacts( } for label, filename in PUBLIC_EXECUTION_ARTIFACTS.items() } - return {**measurements, "artifacts": artifacts}, response, evaluation, steps + return {**measurements, "artifacts": artifacts, "screenshots": screenshots}, response, evaluation, steps + + +def _screenshot_gallery(result_id: str, executions: list[dict[str, Any]]) -> str: + authoritative = next((item for item in executions if item.get("authoritative")), {}) + execution = int(authoritative.get("execution") or 0) + screenshots = authoritative.get("screenshots") or [] + if not screenshots: + return '

    该执行没有可公开的 trace 页面画面;错误日志与执行证据仍可下载。

    ' + root = f"artifacts/{result_id}/executions/{execution}" + figures = "".join( + '
    '
+        + _e(item[
    ' + + _e(item["label"]) + + f" · SHA-256 {_e(str(item['sha256'])[:12])}…
    " + for item in screenshots + ) + return ( + '

    下图是判分权威执行的最后页面画面,用于核对失败前的可见状态;' + "它们不一定正好对应错误发生的瞬间。

    " + f'
    {figures}
    ' + ) def _execution_evidence_html(result_id: str, executions: list[dict[str, Any]]) -> str: @@ -1758,21 +1903,38 @@ def _attempt_table( result_record = read_public_json(Path((row.get("paths") or {}).get("evaluation", ""))) failure, automatic_failure = _automatic_failure_type(result_record) subcategory, reason = localized_cause(verified_causes, p["id"]) + code = _cause_code(verified_causes, p["id"]) + event_record = read_public_json(Path((row.get("paths") or {}).get("events", ""))) + steps = trajectory(event_record if isinstance(event_record, list) else []) + failure_point, step_number = _failure_point(code, steps) + owner, action, _external = _remediation(code) task = task_meta.get(p["task_id"], {}) + point_link = ( + f'results/{p["id"]}.html#trajectory-step-{step_number}' + if step_number + else f'results/{p["id"]}.html#diagnosis' + ) values = [ Safe(f'{p["id"]}'), - MODEL_LABELS[p["model"]], - ENGINE_LABELS[p["engine"]], - p["task_id"], - TIER_LABELS[p["tier"]], + Safe( + f'{_e(MODEL_LABELS[p["model"]])}' + f'{_e(ENGINE_LABELS[p["engine"]])}' + ), + Safe(f'#{p["task_id"]}'), _status(row), - _seconds(p["duration_ms"]), - _seconds(p["browser_tool_duration_ms"]), - p["action_count"], - _number(p["model_usage"]["total_tokens"]), - automatic_failure, - subcategory, - reason, + Safe(f'{_e(failure_point)}') if result == "fail" else "—", + automatic_failure or "—", + subcategory or "待复核" if result == "fail" else "—", + Safe(f'{_e(owner)}{_e(action)}') if result == "fail" else "—", + Safe( + ( + f'诊断' + f'画面' + f'日志' + ) + if result == "fail" + else f'详情' + ), ] cells = "".join(f"{x if isinstance(x, Safe) else _e(x)}" for x in values) rows.append( @@ -1785,18 +1947,14 @@ def _attempt_table( f"{_e(x)}" for x in [ "结果", - "模型", - "浏览器", + "运行配置", "任务", - "层级", "判定", - "Agent 链路累计耗时", - "BrowserTool 累计耗时", - "动作数", - "Tokens", + "失败位置", "程序自动定位", - "核验根因子类", - "核验原因", + "复核根因", + "负责对象与处理", + "证据", ] ) return ( @@ -2421,10 +2579,15 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat + _action_duration_cards(attempts), ) analysis += _section( - "浏览器组合对比", - "

    按同一任务比较所选浏览器,不用各浏览器不同的成功样本拼接结论。基准浏览器单选,对比浏览器可多选;新增浏览器只增加选项和结果标签,不改变页面结构。

    " + "同题全配置对比", + "

    每道任务同时展示全部模型×浏览器结果,不做两两拆分,也不用不同成功样本拼接结论。

    " + _same_task_comparison(tasks, attempts), ).replace('
    ', '
    ', 1) + analysis += _section( + "排除已证实外部干扰后的诊断子集", + "

    官方 90 题结果保持不变。为了看清浏览器和模型差异,下表暂时排除已核验为评测合同、站点、共享环境或参考状态问题的整道任务。

    " + + _clean_subset_table(attempts, verified_causes), + ) analysis += _section( f"{result_count} 个逻辑结果", "

    默认只显示失败;可按浏览器、层级、站点、任务类型、失败环节和全文关键词筛选。

    " @@ -2486,6 +2649,11 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat public["resource"], item.get("checks") or {}, ) + cause_code = _cause_code(verified_causes, public["id"]) + failure_point, failure_step = _failure_point(cause_code, steps) + remediation_owner, remediation_action, external_interference = _remediation( + cause_code + ) detail = _hero( public["id"], f"{MODEL_LABELS[public['model']]} · {ENGINE_LABELS[public['engine']]} · Task #{public['task_id']}", @@ -2496,10 +2664,20 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat attempt_page = f"results/{public['id']}.html" detail += ( '" + + ( + f'失败诊断' + if float(public["score"]) < 1 + else "" + ) + + f'任务与验收' + + f'执行轨迹' + + ( + f'页面画面' + if float(public["score"]) < 1 + else "" + ) + + f'运行与证据' + + "" ) detail += _section( "结果与运行", @@ -2516,20 +2694,35 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat f'
    证据完整性审计{_e(item.get("outcome") or "未记录")}
    ' '

    Agent 链路耗时包含会话准备、登录、模型推理、调度、工具执行和收尾,不包含 Docker 站点重置;BrowserTool 耗时是全部 browser 工具调用的墙钟时间之和,不等同于纯内核时间。

    ', ) + if float(public["score"]) < 1: + step_href = ( + f'{attempt_page}#trajectory-step-{failure_step}' + if failure_step + else f'{attempt_page}#trajectory' + ) + detail += '
    ' + _section( + "失败诊断", + '
    ' + f'' + f'
    程序自动定位{_e(public["automatic_failure_type"] or "无")}
    ' + f'
    人工复核根因{_e(public["verified_failure_subcategory"] or "待复核")}
    ' + f'
    负责对象与处理{_e(remediation_owner)}
    {_e(remediation_action)}
    ' + '
    核验依据:' + + _e(public["verified_failure_reason"] or "当前尚无已核验根因。") + + '
    ' + + ( + '

    对比口径:该结果已确认含非模型/浏览器干扰,官方分数保留,但暂不纳入干净子集。

    ' + if external_interference + else "" + ), + ) detail += '
    ' + _section( "任务要求与验收结果", _verification_html(task, evaluation_record, response_record) - + ( - '
    程序自动定位:' - + _e(public["automatic_failure_type"]) - + " 核验根因子类:" - + _e(public["verified_failure_subcategory"]) - + "
    核验原因:" - + _e(public["verified_failure_reason"]) - + "
    " - if float(public["score"]) < 1 - else "" - ) + '

    所有规则均为官方程序化验收,必须全部得分 1.0。页面先给出可读结论;官方期望、实际观察和断言保留在折叠技术明细及下载文件中。

    ', ) detail += '
    ' + _section( @@ -2537,6 +2730,11 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat "

    以下顺序来自本次执行的 Agent 事件流,不是固定动作回放。展开可查看调用参数和输出摘录;完整脱敏日志可下载。

    " + _trajectory_html(steps), ) + if float(public["score"]) < 1: + detail += '
    ' + _section( + "失败执行的最后页面画面", + _screenshot_gallery(public["id"], execution_records), + ) detail += '
    ' + _section( "运行、日志与证据", _execution_evidence_html(public["id"], execution_records) diff --git a/harness/browser_use/report_i18n.py b/harness/browser_use/report_i18n.py index eb5ccc390..6239f8094 100644 --- a/harness/browser_use/report_i18n.py +++ b/harness/browser_use/report_i18n.py @@ -1226,6 +1226,46 @@ def _register_report_change_translations() -> None: } ) +TEXT.update( + { + "同题全配置对比": "Same-task comparison across all configurations", + "每道任务同时展示全部模型×浏览器结果,不做两两拆分,也不用不同成功样本拼接结论。": "Each task shows every model-by-browser outcome together. The report does not split them into pairwise views or combine different successful subsets.", + "同题全配置矩阵": "Same-task all-configuration matrix", + "每行是同一任务,每列是一个模型×浏览器配置。新增配置只增加列。": "Each row is one task and each column is one model-by-browser configuration. New configurations add columns only.", + "关系": "Outcome pattern", + "结果有差异": "Outcomes differ", + "全部通过": "All pass", + "全部失败": "All fail", + "排除已证实外部干扰后的诊断子集": "Diagnostic subset after removing confirmed external interference", + "官方 90 题结果保持不变。为了看清浏览器和模型差异,下表暂时排除已核验为评测合同、站点、共享环境或参考状态问题的整道任务。": "The official 90-task results remain unchanged. To isolate browser and model differences, this table temporarily removes every task confirmed to have a task-contract, site, shared-environment, or reference-state problem.", + "这是事后诊断口径,不是官方新分数。它按任务统一剔除任一配置已证实受任务/evaluator、站点、共享环境或参考状态干扰的任务,保证四个配置分母一致。": "This is a post-hoc diagnostic view, not a new official score. It removes a whole task across all configurations when any result is confirmed to be affected by task/evaluator, site, shared-environment, or reference-state interference, keeping denominators aligned.", + "失败诊断": "Failure diagnosis", + "失败位置": "Failure stage", + "人工复核根因": "Reviewed root cause", + "负责对象与处理": "Owner and treatment", + "页面画面": "Page frames", + "查看 evaluator 断言": "View evaluator assertions", + "查看失败附近轨迹": "View trajectory near the failure", + "查看最后页面画面": "View final page frames", + "下载执行日志": "Download execution log", + "核验依据:": "Review evidence:", + "对比口径:": "Comparison treatment:", + "该结果已确认含非模型/浏览器干扰,官方分数保留,但暂不纳入干净子集。": "This result contains confirmed interference outside the model/browser variables. Its official score is retained, but it is temporarily excluded from the clean subset.", + "失败执行的最后页面画面": "Final page frames from the failed execution", + "下图是判分权威执行的最后页面画面,用于核对失败前的可见状态;它们不一定正好对应错误发生的瞬间。": "These are the final page frames from the scoring execution. They help verify visible state before failure but may not capture the exact instant the error occurred.", + "是否属于目标变量": "Target variable?", + "处理": "Treatment", + "运行配置": "Run configuration", + "复核根因": "Reviewed root cause", + "负责对象与处理": "Owner and treatment", + "证据": "Evidence", + "诊断": "Diagnosis", + "画面": "Frames", + "日志": "Logs", + "详情": "Details" + } +) + PHRASES = { "步骤预算耗尽": "Step budget exhausted", @@ -1269,6 +1309,15 @@ def _register_report_change_translations() -> None: "个失败结果": " failed results", "实际": "actual", "期望": "expected", + "环境准备后、Agent 读取前": "After environment preparation and before the Agent read", + "运行结束后的 evaluator 验收": "Evaluator acceptance after execution", + "运行结束后的网络证据验收": "Network-evidence acceptance after execution", + "最终回答/任务状态": "Final answer/task state", + "BrowserTool 执行异常": "BrowserTool execution error", + "步后达到步骤上限": " steps: step limit reached", + "步后的最终回答/任务状态": " steps: final answer/task state", + "步后的站点响应/保存": " steps: site response/save", + "最后页面画面": "Final page frame", "暂无发布记录。": "No release has been recorded.", } diff --git a/research/browser-use-remediation.json b/research/browser-use-remediation.json new file mode 100644 index 000000000..94ffb8c2b --- /dev/null +++ b/research/browser-use-remediation.json @@ -0,0 +1,37 @@ +{ + "schema": "lex_browser_use_remediation/1", + "updated_at": "2026-09-11", + "score_policy": "Official results remain unchanged. A task is excluded from the post-hoc diagnostic clean subset across every configuration when any verified result is affected by benchmark/evaluator, site, shared-environment, or mixed reference-state interference.", + "items": [ + { + "category": "shared_environment", + "task_ids": [77], + "status": "implementation_fixed_rerun_required", + "action": "The full physical-attempt lifecycle is now protected by a cross-process lock. Rerun the affected task before admitting it to the clean comparison." + }, + { + "category": "benchmark_or_evaluator", + "task_ids": [339, 475, 528, 600, 625, 676, 699, 771], + "status": "quarantined_pending_contract_fix", + "action": "Correct and independently validate the task contract or evaluator, then preregister and rerun the complete four-configuration scope." + }, + { + "category": "self_hosted_site", + "task_ids": [470, 547, 605, 694], + "status": "quarantined_pending_site_fix", + "action": "Repair the dependency or save path, add task-relevant read/write readiness checks, then rerun the complete four-configuration scope." + }, + { + "category": "reference_state_mixed", + "task_ids": [279], + "status": "quarantined_pending_reference_refreeze", + "action": "Refreeze the expected state before execution and rerun all configurations so the Agent contribution can be separated from reference drift." + }, + { + "category": "browser_evidence_capture", + "task_ids": [448, 506, 595, 676], + "status": "retained_as_browser_integration_signal", + "action": "Keep in the primary browser comparison because HAR completeness is part of the browser integration contract; improve response-body and Referer capture and rerun." + } + ] +} diff --git a/research/report-changes.json b/research/report-changes.json index 14c5f9c45..04e123853 100644 --- a/research/report-changes.json +++ b/research/report-changes.json @@ -147,6 +147,75 @@ } ], "browser_use_releases": [ + { + "id": "2026-09-11-diagnostic-workbench-and-clean-subset", + "date": "2026-09-11", + "title": "将结果分析改为故障定位台", + "summary": "同题比较改为全部模型与浏览器配置矩阵;失败清单改为固定高度的横向诊断表;每个失败详情补齐失败位置、自动判定、复核根因、处理责任、日志与最后页面画面。官方分数不变,另提供排除已证实外部干扰的诊断子集。", + "attempts_sha256_by_model": { + "gpt-5.6-sol": "73146500152db62d79f84b63c85a14027c005a6320c19bce7426651f167466bd", + "gpt-6-astra": "0659d227cf6d29fbfaf64d39f8e77a56bec73c87f7f6e1f2ac23ea13fd9b667c" + }, + "selection_sha256": "2cb716552e36df7535e83ed6d2cf3f84283e3aca86072817cd6e039a38547c7b", + "changes": [ + [ + "同题比较", + "旧页面要求先选基准浏览器再做两两比较,无法同时观察全部配置。", + "改为每题一行、全部模型×浏览器配置为列的动态矩阵。", + "新增模型或浏览器只增加配置列,不再复制比较模块。" + ], + [ + "失败定位", + "结果清单混入长原因和耗时字段,行高失控,且无法直接进入失败证据。", + "改为固定高度横向表,只保留失败位置、自动定位、复核根因、处理责任和证据入口。", + "完整 evaluator、轨迹、日志和原因留在结果详情,不在清单重复。" + ], + [ + "证据页面", + "失败详情缺少从判定到具体轨迹、日志和可见页面状态的连续入口。", + "增加失败诊断区、轨迹步骤锚点,并从私有 trace 提取最后三张页面画面;原始 trace 仍不公开。", + "读者可从失败阶段直达相关轨迹,并核对日志与最后可见状态。" + ], + [ + "干扰项处理", + "官方分数混合了模型/浏览器信号与已确认的题目、站点、共享环境和参考状态问题。", + "保留官方 90 题分数,同时按任务统一剔除已确认外部干扰,发布非官方诊断子集和修复登记。", + "诊断子集的四个配置使用相同分母;修复项必须完整重跑后才能重新纳入。" + ] + ], + "i18n": { + "en": { + "title": "Turn Results into a failure-diagnosis workbench", + "summary": "Same-task comparison now uses a matrix across every model and browser configuration. The failure inventory is a fixed-height horizontal diagnosis table, and each failed result exposes its failure stage, automatic classification, reviewed root cause, remediation owner, logs, and final page frames. Official scores are unchanged; a separate diagnostic subset removes confirmed external interference.", + "changes": [ + [ + "Same-task comparison", + "The previous page required a baseline browser and pairwise comparisons, preventing a simultaneous view of every configuration.", + "Replaced it with a dynamic matrix with one task per row and every model-by-browser configuration as columns.", + "New models or browsers add configuration columns without duplicating comparison modules." + ], + [ + "Failure location", + "The result inventory mixed long causes and latency fields, producing uncontrolled row heights and no direct route to failure evidence.", + "Replaced it with a fixed-height horizontal table containing failure stage, automatic locator, reviewed cause, owner/treatment, and evidence links.", + "Complete evaluator output, trajectory, logs, and cause remain on the result detail page without duplication." + ], + [ + "Evidence pages", + "Failed-result pages lacked a continuous route from the verdict to the relevant trajectory, logs, and visible page state.", + "Added a diagnosis section, trajectory-step anchors, and the last three page frames extracted from the private trace while keeping the trace itself private.", + "Readers can jump from the failure stage to nearby trajectory evidence and inspect logs and final visible state." + ], + [ + "Interference treatment", + "Official scores combined model/browser signal with confirmed task, site, shared-environment, and reference-state problems.", + "Kept the official 90-task scores and added a non-official diagnostic subset that removes confirmed external interference at task level, plus a remediation registry.", + "All four configurations use the same diagnostic denominator; remediated tasks require a complete rerun before readmission." + ] + ] + } + } + }, { "id": "2026-09-11-reader-first-report-and-sites-migration", "date": "2026-09-11", diff --git a/scripts/check_browser_use_report.py b/scripts/check_browser_use_report.py index 52914719c..3913996bf 100644 --- a/scripts/check_browser_use_report.py +++ b/scripts/check_browser_use_report.py @@ -165,6 +165,25 @@ def _check_execution_artifacts( f"result page omits execution artifact link or hash: " f"{result_id}/{number}/{label}" ) + screenshots = indexed_execution.get("screenshots") or [] + if number != authority and screenshots: + raise ValueError(f"only the authoritative execution may publish frames: {result_id}") + for screenshot in screenshots: + relative = ( + Path("artifacts") + / result_id + / "executions" + / str(number) + / str(screenshot.get("path") or "") + ) + path = report_dir / relative + if path.suffix.lower() not in {".jpeg", ".jpg", ".png"}: + raise ValueError(f"unsafe screenshot type: {relative}") + expected_files.add(path) + if not path.is_file() or screenshot.get("sha256") != _sha256(path): + raise ValueError(f"trace frame binding differs: {relative}") + if relative.as_posix() not in detail_text: + raise ValueError(f"result page omits trace frame: {relative}") bundle_path = ( report_dir / "artifacts" @@ -226,7 +245,7 @@ def _check_execution_artifacts( "agent_trace.zip", }: raise ValueError(f"private HAR or trace was copied into the public report: {path}") - if forbidden_text.search(path.read_text(encoding="utf-8")): + if path.suffix.lower() in {".json", ".jsonl", ".html", ".css", ".js"} and forbidden_text.search(path.read_text(encoding="utf-8")): raise ValueError(f"published execution artifact contains sensitive text: {path}") @@ -421,8 +440,9 @@ def check( "comparison-winner", "BrowserTool 动作延迟", "action-card-grid", - "浏览器组合对比", - "data-compare-engine", + "同题全配置对比", + 'id="configuration-matrix"', + 'id="clean-subset-table"', ): if marker not in analysis_index: raise ValueError(f"analysis is missing timing or scalable comparison: {marker}") @@ -448,6 +468,25 @@ def check( ) ): raise ValueError("every result page must expose result, evaluator, trajectory and logs") + failed_pages = [ + report_dir / "results" / f"{_attempt_id(row)}.html" + for row in logical_rows + if float(row["score"]) < 1 + ] + if failed_pages and any( + marker not in rendered_pages[page] + for page in failed_pages + for marker in ( + "失败诊断", + "失败位置", + "程序自动定位", + "人工复核根因", + "负责对象与处理", + "失败执行的最后页面画面", + "下载执行日志", + ) + ): + raise ValueError("every failed result must expose a complete diagnosis path") if any( marker in rendered_pages[page] for page in result_pages @@ -529,6 +568,9 @@ def check( ".browser-comparison{", ".action-card-grid{", ".attempt-summary{", + "#result-table{min-width:1580px;table-layout:fixed}", + ".diagnosis-grid{", + ".screenshot-grid{", ".review-round{", ".page-main{overflow-x:clip}", ): @@ -664,6 +706,35 @@ def check( ) if published_cause_review.get("results") != verified_causes: raise ValueError("published failure causes differ from verified reviews") + remediation = json.loads( + (Path(__file__).parents[1] / "research/browser-use-remediation.json").read_text( + encoding="utf-8" + ) + ) + if remediation.get("schema") != "lex_browser_use_remediation/1": + raise ValueError("unsupported Browser Use remediation schema") + external_prefixes = ("benchmark_", "site_", "environment_", "mixed_") + expected_quarantine = { + int(result_id.rsplit("-", 1)[-1]) + for result_id, diagnosis in verified_causes.items() + if str((diagnosis.get("subcategory") or {}).get("code") or "").startswith( + external_prefixes + ) + } + registered_quarantine = { + int(task_id) + for item in remediation.get("items") or [] + if item.get("category") + in { + "shared_environment", + "benchmark_or_evaluator", + "self_hosted_site", + "reference_state_mixed", + } + for task_id in item.get("task_ids") or [] + } + if registered_quarantine != expected_quarantine: + raise ValueError("remediation quarantine differs from verified external interference") for bundle in bundles: for _page, text in rendered_pages.items(): if str(bundle["path"]) in text: diff --git a/scripts/check_browser_use_visual.py b/scripts/check_browser_use_visual.py index 6cf1fa25b..a2b72d6d8 100644 --- a/scripts/check_browser_use_visual.py +++ b/scripts/check_browser_use_visual.py @@ -49,6 +49,15 @@ def _route_classes(report: Path) -> list[Path]: pages = sorted((report / folder).glob("*.html")) if pages: dynamic.append(pages[0].relative_to(report).as_posix()) + if folder == "results": + failed_page = next( + (page for page in pages if 'id="diagnosis"' in page.read_text(encoding="utf-8")), + None, + ) + if failed_page: + failed_route = failed_page.relative_to(report).as_posix() + if failed_route not in dynamic: + dynamic.append(failed_route) chinese = [report / name for name in (*fixed, *dynamic)] routes = chinese + [report / "en" / page.relative_to(report) for page in chinese] missing = [path for path in routes if not path.is_file()] diff --git a/tests/browser_use/test_report.py b/tests/browser_use/test_report.py index 4c98b7421..f47f81964 100644 --- a/tests/browser_use/test_report.py +++ b/tests/browser_use/test_report.py @@ -159,9 +159,10 @@ def test_comparison_components_scale_to_three_browsers(monkeypatch) -> None: comparison = _same_task_comparison(tasks, attempts) actions = _action_duration_cards(attempts) - assert comparison.count("data-compare-engine") == 3 - assert 'data-pass="chromium third"' in comparison - assert 'data-fail="moli"' in comparison + assert 'id="configuration-matrix"' in comparison + assert comparison.count('class="outcome-pill pass"') == 2 + assert comparison.count('class="outcome-pill fail"') == 1 + assert "同题全配置矩阵" in comparison assert "Third Browser" in comparison assert "Third Browser" in actions assert "0.040 秒" in actions @@ -356,9 +357,10 @@ def test_report_publishes_one_final_logical_result_and_aggregates_cost( for expected in ( "BrowserTool 动作延迟", "action-card-grid", - "浏览器组合对比", + "同题全配置对比", "browser-comparison", - "data-compare-engine", + 'id="configuration-matrix"', + 'id="clean-subset-table"', ): assert expected in analysis assert "中位数" not in analysis diff --git a/tests/browser_use/test_report_ui.mjs b/tests/browser_use/test_report_ui.mjs index 4a158e786..79759196b 100644 --- a/tests/browser_use/test_report_ui.mjs +++ b/tests/browser_use/test_report_ui.mjs @@ -20,23 +20,24 @@ const rows = [ {dataset: {tier: 'easy', result: 'pass', model: 'model-a', engine: 'chromium'}, textContent: 'complete'}, ]; const count = {textContent: ''}; -const browserControls = { - 'browser-model': {value: 'model-a', options: [{value: 'model-a'}, {value: 'model-b'}], addEventListener() {}}, - 'browser-a': {value: 'chromium', options: [{value: 'chromium'}, {value: 'moli'}], addEventListener() {}}, - 'browser-relation': {value: 'mixed', options: [{value: 'mixed'}, {value: 'all_fail'}], addEventListener() {}}, - 'browser-kind': {value: '', options: [{value: ''}, {value: 'RETRIEVE'}], addEventListener() {}}, - 'browser-compare-count': {textContent: ''}, -}; -const comparisonLabel = {classList: {toggle() {}}}; -const compareChecks = [ - {value: 'chromium', checked: false, disabled: false, closest() { return comparisonLabel; }, addEventListener() {}}, - {value: 'moli', checked: true, disabled: false, closest() { return comparisonLabel; }, addEventListener() {}}, +const matrixControls = [ + {dataset: {matrixFilter: 'relation'}, value: 'mixed', addEventListener() {}}, + {dataset: {matrixFilter: 'kind'}, value: '', addEventListener() {}}, ]; const comparisonRows = [ - {dataset: {model: 'model-a', pass: 'chromium', fail: 'moli', kind: 'RETRIEVE'}}, - {dataset: {model: 'model-a', pass: 'chromium moli', fail: '', kind: 'NAVIGATE'}}, - {dataset: {model: 'model-a', pass: '', fail: 'chromium moli', kind: 'MUTATE'}}, + {dataset: {tier: 'hard', relation: 'mixed', kind: 'RETRIEVE'}}, + {dataset: {tier: 'easy', relation: 'all_pass', kind: 'NAVIGATE'}}, + {dataset: {tier: 'medium', relation: 'all_fail', kind: 'MUTATE'}}, ]; +const matrixCount = {textContent: ''}; +const matrixRoot = { + querySelectorAll(selector) { + if (selector === '[data-matrix-filter]') return matrixControls; + if (selector === 'tbody tr[data-tier]') return comparisonRows; + return []; + }, + querySelector(selector) { return selector === '[data-matrix-count]' ? matrixCount : null; }, +}; const listeners = {}; const storage = new Map(); let replaced = ''; @@ -53,15 +54,13 @@ const document = { querySelectorAll(selector) { if (selector === '[data-filter]') return controls; if (selector === '[data-search]') return [search]; - if (selector === 'tbody tr[data-tier]') return rows; + if (selector === 'tbody tr[data-tier]:not([data-relation])') return rows; if (selector === '[data-visible-count]') return [count]; - if (selector === '[data-browser-compare-control],[data-compare-engine]') return [...Object.values(browserControls).filter(x => x.addEventListener), ...compareChecks]; - if (selector === '[data-compare-engine]') return compareChecks; - if (selector === '#browser-compare-rows tr') return comparisonRows; + if (selector === '[data-matrix-filter]') return matrixControls; return []; }, querySelector(selector) { return selector === '[data-search]' ? search : null; }, - getElementById(id) { return browserControls[id] ?? null; }, + getElementById(id) { return id === 'configuration-matrix' ? matrixRoot : null; }, }; const context = vm.createContext({ URL, @@ -87,19 +86,11 @@ assert.equal(controls[0].value, 'fail'); assert.deepEqual(rows.map(row => row.hidden), [false, true]); assert.equal(count.textContent, '1 / 2'); assert.deepEqual(comparisonRows.map(row => row.hidden), [false, true, true]); -assert.equal(browserControls['browser-compare-count'].textContent, '找到 1 个任务'); -assert.equal(compareChecks[0].disabled, true); - -location.search = '?b_a=retired&b_relation=unknown&b_others=retired'; -vm.runInContext('restoreView()', context); -assert.equal(browserControls['browser-a'].value, 'chromium'); -assert.equal(browserControls['browser-relation'].value, 'mixed'); -assert.equal(compareChecks[1].checked, true, 'invalid saved browsers must fall back to a valid comparison'); +assert.equal(matrixCount.textContent, '1 / 3'); -browserControls['browser-relation'].value = 'all_fail'; -vm.runInContext('compareBrowsers()', context); +matrixControls[0].value = 'all_fail'; +vm.runInContext('filterMatrix()', context); assert.deepEqual(comparisonRows.map(row => row.hidden), [true, true, false]); -assert.match(replaced, /b_relation=all_fail/); controls[0].value = 'pass'; search.value = ''; From 737946046d0f5084546b2fcaf0f2405ed4a7b802 Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 11 Sep 2026 19:18:04 +0800 Subject: [PATCH 065/127] Record diagnostic report site version --- evaluation/browser-use/current-report.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evaluation/browser-use/current-report.json b/evaluation/browser-use/current-report.json index 867c024e0..7b52febb1 100644 --- a/evaluation/browser-use/current-report.json +++ b/evaluation/browser-use/current-report.json @@ -57,7 +57,7 @@ "base_url": "https://artifact-hub.app.lexmount.com", "site_slug": "7l77EeXPRfml", "share_url": "https://artifact-hub.app.lexmount.com/v/Ghn1BR6PCy-njsCMqCe2LRIJ-eyJEJoa", - "expected_version": "ver_3edc1c6856cc4d26960a3bde9ce6fecc", + "expected_version": "ver_f305509b385048a99d9e03e42701ee2a", "state": "verified" } } From 3d16c263609a3223c55527363c45cfbf8d639fc9 Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 11 Sep 2026 19:29:28 +0800 Subject: [PATCH 066/127] Fix report evidence provenance and localization --- AGENTS.md | 3 +- harness/browser_use/report.py | 98 +++++++++++------------------ harness/browser_use/report_i18n.py | 93 ++++++++++++++++++++++++++- research/report-changes.json | 16 ++++- scripts/check_browser_use_report.py | 34 ++++++++++ 5 files changed, 178 insertions(+), 66 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5ef498ec3..4c14b8b28 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,9 +22,10 @@ - Result-detail summaries contain only the verdict, concise failed-rule labels and short scalar facts. Full evaluator messages, expected/actual payloads, traces and logs belong in dedicated evidence sections, never narrow KPI cards. - Result inventories are fixed-height, horizontally scrollable diagnosis tables. Do not place full reviewed causes, evaluator payloads, latency stacks or other paragraph-length content in inventory cells; link to the result detail instead. - Compare every registered model-by-browser configuration in one same-task matrix generated from the participant registry. Do not introduce pairwise selectors or duplicate comparison prose when more configurations can be represented as columns. -- Every failed result detail must expose the evidenced failure stage, the programmatic evaluator classification, the reviewed root cause, the remediation owner/status, a link to the relevant trajectory step, downloadable sanitized logs, and the last public trace frames when available. Label trace frames as last observed pages, not as exact error screenshots unless the evidence proves that timing. +- Every failed result detail must expose the evidenced failure stage, the programmatic evaluator classification, the reviewed root cause, the remediation owner/status, a link to the relevant trajectory step, downloadable sanitized logs, and the last public trace frames when available. Extract frames only from ordered Playwright `screencast-frame` events, validate full-page dimensions and provenance, and never treat arbitrary trace resources as screenshots. Label frames as last observed pages, not as exact error screenshots unless the evidence proves that timing. - Keep official scores immutable when diagnosis finds benchmark/evaluator, site, shared-environment, or frozen-reference interference. Quarantine affected tasks across all configurations in a clearly labeled post-hoc diagnostic subset, record remediation in `research/browser-use-remediation.json`, and readmit them only after a preregistered full-scope rerun. - Split environment preparation, browser-session setup, Agent run, BrowserTool wall time, evidence finalization and evaluator time at collection. Publish the self-hosted deployment topology and image identity; claim per-execution reset/readiness only when a preparation receipt binds reset, readiness and application health. - Formal report prose is reader-facing: no prompts, reasoning, internal work narration, conversational rebuttals or references to parallel sessions. Review pages contain review rounds and corrections, not per-case audit tables; release notes contain reader-visible effects. +- English routes may preserve measured task names, IDs and evidence verbatim, but all report-authored interface and explanatory prose must be localized. Release checks must reject known untranslated interface markers and untranslated accessibility attributes. - Before release, inspect every core route class in Chinese and English at desktop and narrow widths in both themes. Prevent vertical table labels, clipped controls and unreadable evidence; use ImageGen raster assets or Mermaid rather than SVG for Browser Use diagrams. - Keep routine report checks fast and deterministic; they cover structure, evidence, links, terminology, and static readability guards without launching a browser. Run the separate full release check before packaging or publishing; it adds the complete cross-language, cross-theme, desktop and narrow-width visual gate. diff --git a/harness/browser_use/report.py b/harness/browser_use/report.py index 4b6e08f5f..75f30e5a4 100644 --- a/harness/browser_use/report.py +++ b/harness/browser_use/report.py @@ -12,11 +12,14 @@ import statistics import tempfile import zipfile +from io import BytesIO from collections import Counter, defaultdict from collections.abc import Callable, Iterable from pathlib import Path from typing import Any +from PIL import Image + from harness.web_fetch.report_style import STYLE as SHARED_STYLE from .failure_cause_review import ( @@ -1555,30 +1558,51 @@ def _trajectory_html(steps: list[dict[str, Any]]) -> str: def _extract_trace_screenshots( trace_path: Path, artifact_dir: Path, *, limit: int = 3 ) -> list[dict[str, Any]]: - """Publish only the last raster frames, never the cookie-bearing trace archive.""" + """Publish the final Playwright screencast frames, never arbitrary trace resources.""" if not trace_path.is_file(): return [] try: with zipfile.ZipFile(trace_path) as archive: - names = [ - name - for name in archive.namelist() - if name.startswith("resources/") - and name.lower().endswith((".jpeg", ".jpg", ".png")) - ][-limit:] + frames = [] + for raw_line in archive.read("trace.trace").splitlines(): + try: + event = json.loads(raw_line) + except (TypeError, ValueError): + continue + if event.get("type") != "screencast-frame" or not event.get("sha1"): + continue + name = "resources/" + str(event["sha1"]) + if name not in archive.namelist(): + continue + frames.append((float(event.get("timestamp") or 0), name)) + names = [] + for _timestamp, name in reversed(sorted(frames)): + if name not in names: + names.append(name) + if len(names) == limit: + break + names.reverse() output = [] screenshot_dir = artifact_dir / "screenshots" screenshot_dir.mkdir(parents=True, exist_ok=True) for index, name in enumerate(names, start=1): + payload = archive.read(name) + with Image.open(BytesIO(payload)) as image: + width, height = image.size + if width < 800 or height < 450: + continue suffix = ".png" if name.lower().endswith(".png") else ".jpeg" destination = screenshot_dir / f"frame-{index}{suffix}" - destination.write_bytes(archive.read(name)) + destination.write_bytes(payload) output.append( { "path": f"screenshots/{destination.name}", "sha256": _sha256(destination), - "label": f"最后页面画面 {index}/{len(names)}", + "label": f"最后页面画面 {len(output) + 1}/{len(names)}", + "source": "playwright-screencast-frame", + "width": width, + "height": height, } ) return output @@ -1963,56 +1987,6 @@ def _attempt_table( ) -def _engine_attempts( - engine: str, - attempts: list[dict[str, Any]], - tasks: list[dict[str, Any]], - verified_causes: dict[str, dict[str, Any]] | None = None, -) -> str: - verified_causes = verified_causes or {} - meta = {int(task["task_id"]): task for task in tasks} - rows = [] - for row in attempts: - if row["engine"] != engine: - continue - task = meta[int(row["task_id"])] - result = read_public_json(Path((row.get("paths") or {}).get("evaluation", ""))) - automatic_failure = _automatic_failure_type(result)[1] - subcategory, reason = localized_cause(verified_causes, _attempt_id(row)) - rows.append( - [ - Safe(f'{_attempt_id(row)}'), - MODEL_LABELS[str(row["model"])], - task["rationale"], - TIER_LABELS[task["tier"]], - _status(row), - _seconds(float(row.get("duration_ms") or 0)), - _seconds(float(row.get("browser_tool_duration_ms") or 0)), - int(row.get("action_count") or 0), - automatic_failure, - subcategory, - reason, - ] - ) - return _table( - [ - "结果", - "模型", - "任务", - "层级", - "判定", - "Agent 链路耗时", - "BrowserTool 耗时", - "动作数", - "程序自动定位", - "核验根因子类", - "核验原因", - ], - rows, - table_id="engine-result-table", - ) - - def _write_json(path: Path, value: Any) -> None: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(value, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") @@ -2900,9 +2874,9 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat + '

    版本与镜像身份用于限定复现边界,不参与评分。

    ', ) body += _section( - "全部逻辑结果", - "

    每题只展示一个最终逻辑结果;端到端耗时与模型用量包含获得该结果所需的全部执行。

    " - + _engine_attempts(engine, attempts, tasks, verified_causes), + "查看该浏览器的全部结果", + "

    完整结果只保留一份。下方入口打开结果分析页,并预先筛选当前浏览器;失败诊断、轨迹、页面画面与日志均从该清单进入。

    " + f'
    打开筛选后的结果清单 →
    ', ) (stage / f"engines/{engine}.html").write_text( _page(title=ENGINE_LABELS[engine], active="adoption", body=body, depth=1), diff --git a/harness/browser_use/report_i18n.py b/harness/browser_use/report_i18n.py index 6239f8094..037873c10 100644 --- a/harness/browser_use/report_i18n.py +++ b/harness/browser_use/report_i18n.py @@ -1262,7 +1262,11 @@ def _register_report_change_translations() -> None: "诊断": "Diagnosis", "画面": "Frames", "日志": "Logs", - "详情": "Details" + "详情": "Details", + "查看该浏览器的全部结果": "View all results for this browser", + "完整结果只保留一份。下方入口打开结果分析页,并预先筛选当前浏览器;失败诊断、轨迹、页面画面与日志均从该清单进入。": "The report keeps one result inventory. The link below opens it with this browser preselected; failure diagnoses, trajectories, page frames, and logs are all reached from that inventory.", + "打开筛选后的结果清单 →": "Open the filtered result inventory →", + "WebArena-Verified 本地部署、CowAgent 自主操作、双浏览器对照与程序化验收流程": "WebArena-Verified local deployment, autonomous CowAgent operation, browser comparison, and programmatic evaluation" } ) @@ -1293,6 +1297,26 @@ def _register_report_change_translations() -> None: "响应证据缺失": "Missing response evidence", "共享环境并发污染": "Shared-environment concurrency contamination", "检索遗漏与参考状态差异": "Retrieval omission and reference-state drift", + "保留为目标变量;按轨迹修正策略或工具合同": "Keep as a target-variable signal; improve the strategy or tool contract from the trajectory", + "评测集 / evaluator": "Benchmark / evaluator", + "修正合同并重跑;当前仅纳入官方分数": "Fix the contract and rerun; retain only the official score for now", + "修复依赖或保存链路,通过写入预检后重跑": "Fix the dependency or save path, then rerun after a write preflight", + "保留为浏览器集成信号;补齐 HAR 后重跑": "Keep as a browser-integration signal; rerun after completing HAR capture", + "运行环境": "Runtime environment", + "跨进程锁已上线;受污染任务待重跑": "The cross-process lock is implemented; contaminated tasks await rerun", + "任务与 Agent": "Task and Agent", + "先冻结参考状态,再重跑区分 Agent 错误": "Freeze reference state, then rerun to distinguish Agent errors", + "自托管站点": "Self-hosted site", + "浏览器证据接口": "Browser evidence interface", + "保留为对比信号;通过轨迹修正检索、选择和提交策略。": "Keep as a comparison signal; use trajectories to improve retrieval, selection, and submission.", + "任务 / evaluator": "Task / evaluator", + "修正任务合同或 evaluator,验证后重跑;重跑前不进入干净子集。": "Fix the task contract or evaluator, validate it, and rerun; exclude it from the clean subset until then.", + "修复依赖、会话或保存链路,增加任务级写入预检后重跑。": "Fix the dependency, session, or save path and rerun after adding a task-level write preflight.", + "保留为浏览器集成信号;补齐响应正文或 Referer 采集后重跑。": "Keep as a browser-integration signal; rerun after completing response-body or Referer capture.", + "跨进程串行锁已上线;受污染任务待重跑。": "The cross-process serialization lock is implemented; contaminated tasks await rerun.", + "参考状态 + Agent": "Reference state + Agent", + "先重新冻结参考状态,再重跑区分 Agent 错误。": "Refreeze reference state, then rerun to distinguish Agent errors.", + "该执行没有可公开的 trace 页面画面;错误日志与执行证据仍可下载。": "This execution has no publishable trace page frame; the error log and execution evidence remain available.", "规则 ": "Rule ", " 秒": " s", " 次 BrowserTool 调用": " BrowserTool calls", @@ -1318,6 +1342,17 @@ def _register_report_change_translations() -> None: "步后的最终回答/任务状态": " steps: final answer/task state", "步后的站点响应/保存": " steps: site response/save", "最后页面画面": "Final page frame", + "该逻辑结果包含": "This logical result contains", + "次物理执行。标记为判分权威的执行决定最终得分;累计耗时、tokens 与 BrowserTool 指标是下列各次执行之和。": "physical execution(s). The scoring-authority execution determines the final score; cumulative latency, tokens, and BrowserTool metrics sum all executions shown below.", + "物理执行": "Physical execution", + " · 判分权威": " · scoring authority", + "判分": "Score", + "Agent 链路": "Agent path", + "模型 tokens": "model tokens", + "(完整内容见脱敏事件日志)": "(see the sanitized event log for the complete content)", + "个物理执行均保存环境准备回执;公开环境摘要保留逐项状态,原始登录态不发布。": "physical executions retain environment-preparation receipts. The public environment summary preserves each status while authenticated session state remains private.", + "个物理执行均记录 reset、ready、应用健康检查和登录准备状态;事件流随后证明 CowAgent 自主产生 BrowserTool 调用序列。": "physical executions record reset, readiness, application-health, and sign-in preparation status. The subsequent event streams show CowAgent generating BrowserTool calls autonomously.", + "个逻辑结果及其物理执行、下载路径和公开文件哈希。": "logical results with their physical executions, download paths, and public-file hashes.", "暂无发布记录。": "No release has been recorded.", } @@ -1331,6 +1366,62 @@ def translate_text(value: str) -> str: translated = TEXT.get(core) if translated is None: patterns = ( + ( + r"(.+?) · (Chromium|Moli 1\.1\.3) 结果", + lambda m: f"{m.group(1)} · {m.group(2)} result", + ), + ( + r"(简单|中等|困难) · (\d+) 步起", + lambda m: f"{ {'简单': 'Easy', '中等': 'Medium', '困难': 'Hard'}[m.group(1)] } · {m.group(2)} minimum steps", + ), + ( + r"第 (\d+) 步 BrowserTool 执行异常", + lambda m: f"Step {m.group(1)}: BrowserTool execution error", + ), + ( + r"第 (\d+) 步后达到步骤上限", + lambda m: f"After {m.group(1)} steps: step limit reached", + ), + ( + r"第 (\d+) 步后的最终回答/任务状态", + lambda m: f"After {m.group(1)} steps: final answer/task state", + ), + ( + r"第 (\d+) 步后的站点响应/保存", + lambda m: f"After {m.group(1)} steps: site response/save", + ), + ( + r"(.+?) · 单次调用墙钟时间", + lambda m: f"{m.group(1)} · per-call wall time", + ), + ( + r"判分 ([01]) · Agent 链路 ([\d.]+) 秒 · BrowserTool (\d+) 次 / ([\d.]+) 秒 · 模型 tokens ([\d,]+)", + lambda m: f"Score {m.group(1)} · Agent path {m.group(2)} s · BrowserTool {m.group(3)} calls / {m.group(4)} s · model tokens {m.group(5)}", + ), + ( + r",只保留该题所需站点;env-ctrl 报告 ready 后,再做连续应用健康检查、清浏览器状态、登录测试账号并打开起始页。本轮 (\d+)/(\d+) 个物理执行都保存了环境准备回执,并与对应执行记录绑定。", + lambda m: f", keeping only the sites required by the task. After env-ctrl reports ready, the runner performs repeated application-health checks, clears browser state, signs in with test accounts, and opens the start page. All {m.group(1)}/{m.group(2)} physical executions retain preparation receipts bound to their execution records.", + ), + ( + r"预注册文件只含任务 ID、层级和入选理由,不含点击序列。CowAgent 收到自然语言目标后,在最多 (\d+) 步内循环读取页面观察、请求模型决策并生成下一次 BrowserTool 调用。Chromium 与 Moli 分别连接独立 CDP 浏览器容器;每次物理执行都保留最终 JSON、Agent 事件流、网络 HAR 和 Playwright trace。", + lambda m: f"The preregistration contains task IDs, tiers, and selection rationales, but no click sequence. Given a natural-language objective, CowAgent repeatedly reads the page, requests a model decision, and generates the next BrowserTool call for up to {m.group(1)} steps. Chromium and Moli connect to separate CDP browser containers. Every physical execution retains the final JSON, Agent event stream, network HAR, and Playwright trace.", + ), + ( + r"(\d+)/(\d+) 个物理执行均记录 reset、ready、应用健康检查和登录准备状态;事件流随后证明 CowAgent 自主产生 BrowserTool 调用序列。", + lambda m: f"All {m.group(1)}/{m.group(2)} physical executions record reset, readiness, application-health, and sign-in preparation status. The subsequent event streams show CowAgent generating BrowserTool calls autonomously.", + ), + ( + r"本轮 (\d+)/(\d+) 个物理执行均保存环境准备回执;公开环境摘要保留逐项状态,原始登录态不发布。", + lambda m: f"All {m.group(1)}/{m.group(2)} physical executions retain environment-preparation receipts. The public summary preserves each status while authenticated session state remains private.", + ), + ( + r"每次物理执行的脱敏回答、evaluator、事件和轨迹位于", + lambda _m: "The sanitized answer, evaluator output, events, and trajectory for each physical execution are located under", + ), + ( + r"给出全部 (\d+) 个逻辑结果及其物理执行、下载路径和公开文件哈希。", + lambda m: f"lists all {m.group(1)} logical results, their physical executions, download paths, and public-file hashes.", + ), ( r"CowAgent 使用 (.+),在同一组 WebArena‑Verified (\d+) 题上分别操作 (.+)。", lambda m: f"CowAgent uses {m.group(1)} to operate {m.group(3)} on the same {m.group(2)} WebArena-Verified tasks.", diff --git a/research/report-changes.json b/research/report-changes.json index 04e123853..b8abc575b 100644 --- a/research/report-changes.json +++ b/research/report-changes.json @@ -173,9 +173,15 @@ [ "证据页面", "失败详情缺少从判定到具体轨迹、日志和可见页面状态的连续入口。", - "增加失败诊断区、轨迹步骤锚点,并从私有 trace 提取最后三张页面画面;原始 trace 仍不公开。", + "增加失败诊断区、轨迹步骤锚点,并按 Playwright screencast 事件顺序从私有 trace 提取最后三张完整页面画面;原始 trace 仍不公开。", "读者可从失败阶段直达相关轨迹,并核对日志与最后可见状态。" ], + [ + "结果入口", + "浏览器详情页曾重复生成一套含长原因的结果表。", + "删除重复表,改为链接到预先筛选当前浏览器的唯一结果清单。", + "结果、筛选和诊断入口只有一套,新增浏览器不会复制页面结构。" + ], [ "干扰项处理", "官方分数混合了模型/浏览器信号与已确认的题目、站点、共享环境和参考状态问题。", @@ -203,9 +209,15 @@ [ "Evidence pages", "Failed-result pages lacked a continuous route from the verdict to the relevant trajectory, logs, and visible page state.", - "Added a diagnosis section, trajectory-step anchors, and the last three page frames extracted from the private trace while keeping the trace itself private.", + "Added a diagnosis section, trajectory-step anchors, and the last three full-page frames selected in Playwright screencast-event order from the private trace while keeping the trace itself private.", "Readers can jump from the failure stage to nearby trajectory evidence and inspect logs and final visible state." ], + [ + "Result entry point", + "Browser detail pages duplicated a result table containing long cause text.", + "Removed the duplicate and linked to the single result inventory with the current browser preselected.", + "Results, filters, and diagnosis entry points now have one owner; adding a browser does not duplicate page structure." + ], [ "Interference treatment", "Official scores combined model/browser signal with confirmed task, site, shared-environment, and reference-state problems.", diff --git a/scripts/check_browser_use_report.py b/scripts/check_browser_use_report.py index 3913996bf..6679e14f1 100644 --- a/scripts/check_browser_use_report.py +++ b/scripts/check_browser_use_report.py @@ -9,6 +9,8 @@ import re from pathlib import Path +from PIL import Image + from harness.browser_use.failure_cause_review import ( DEFAULT_REVIEW_PATH as FAILURE_CAUSE_REVIEW_PATH, ) @@ -169,6 +171,15 @@ def _check_execution_artifacts( if number != authority and screenshots: raise ValueError(f"only the authoritative execution may publish frames: {result_id}") for screenshot in screenshots: + if set(screenshot) != { + "path", + "sha256", + "label", + "source", + "width", + "height", + } or screenshot.get("source") != "playwright-screencast-frame": + raise ValueError(f"trace frame lacks screencast provenance: {result_id}") relative = ( Path("artifacts") / result_id @@ -182,6 +193,12 @@ def _check_execution_artifacts( expected_files.add(path) if not path.is_file() or screenshot.get("sha256") != _sha256(path): raise ValueError(f"trace frame binding differs: {relative}") + with Image.open(path) as image: + dimensions = image.size + if dimensions != (screenshot.get("width"), screenshot.get("height")): + raise ValueError(f"trace frame dimensions differ: {relative}") + if dimensions[0] < 800 or dimensions[1] < 450: + raise ValueError(f"trace frame is not a full page image: {relative}") if relative.as_posix() not in detail_text: raise ValueError(f"result page omits trace frame: {relative}") bundle_path = ( @@ -399,6 +416,17 @@ def check( raise ValueError("every Chinese report route must have an English mirror") cjk = re.compile(r"[\u3400-\u9fff]") localized_attribute = re.compile(r'(?:aria-label|placeholder|title)="([^"]*)"', flags=re.I) + untranslated_interface = ( + "该逻辑结果包含", + "物理执行", + "判分权威", + "单次调用墙钟时间", + "查看该浏览器的全部结果", + "完整结果只保留一份", + "该执行没有可公开的 trace 页面画面", + "个物理执行均记录", + "个物理执行均保存", + ) for page in english_pages: text = rendered_pages[page] title = re.search(r"(.*?)", text, flags=re.I | re.S) @@ -406,6 +434,12 @@ def check( raise ValueError(f"English report title is not localized: {page}") if any(cjk.search(value) for value in localized_attribute.findall(text)): raise ValueError(f"English report accessibility text is not localized: {page}") + if any(marker in text for marker in untranslated_interface): + raise ValueError(f"English report interface prose is not localized: {page}") + for engine in engines: + engine_page = rendered_pages[report_dir / "engines" / f"{engine}.html"] + if "engine-result-table" in engine_page or f"f_engine={engine}" not in engine_page: + raise ValueError("browser pages must link to the single filtered result inventory") if task_pages and any( "Ground truth · 官方验收合同" not in rendered_pages[page] or "查看本题运行结果" not in rendered_pages[page] From 0e74531d6869f1a89aaad13082bff0207ca64fb4 Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 11 Sep 2026 19:34:19 +0800 Subject: [PATCH 067/127] Record corrected browser use report version --- evaluation/browser-use/current-report.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evaluation/browser-use/current-report.json b/evaluation/browser-use/current-report.json index 7b52febb1..937ab4b52 100644 --- a/evaluation/browser-use/current-report.json +++ b/evaluation/browser-use/current-report.json @@ -57,7 +57,7 @@ "base_url": "https://artifact-hub.app.lexmount.com", "site_slug": "7l77EeXPRfml", "share_url": "https://artifact-hub.app.lexmount.com/v/Ghn1BR6PCy-njsCMqCe2LRIJ-eyJEJoa", - "expected_version": "ver_f305509b385048a99d9e03e42701ee2a", + "expected_version": "ver_ae8ba880bfff4a0e95655e02aedb72f3", "state": "verified" } } From 3085201e9ae87964d3e0adadd5780a896c4c541d Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 11 Sep 2026 19:38:04 +0800 Subject: [PATCH 068/127] Resolve remaining report review findings --- harness/browser_use/report.py | 9 ++++++--- harness/browser_use/report_i18n.py | 4 ++++ scripts/check_browser_use_report.py | 1 + 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/harness/browser_use/report.py b/harness/browser_use/report.py index 75f30e5a4..dca8b20f3 100644 --- a/harness/browser_use/report.py +++ b/harness/browser_use/report.py @@ -731,7 +731,10 @@ def _task_family_table(families: list[dict[str, Any]]) -> str: rows: list[list[Any]] = [] for family in families: kind = str(family["kind"]) - label, description = TASK_TYPE_COPY[kind] + copy = TASK_TYPE_COPY.get(kind) + if copy is None: + raise ValueError(f"unsupported Browser Use task type: {kind}") + label, description = copy rows.append( [ Safe( @@ -820,8 +823,8 @@ def _clean_subset_table( table_id="clean-subset-table", classes="core-comparison-table", ) - + '

    这是事后诊断口径,不是官方新分数。它按任务统一剔除任一配置已证实受任务/evaluator、站点、共享环境或参考状态干扰的任务,保证四个配置分母一致。' - f'本轮剔除 {len(excluded)} 题:{_e("、".join(f"#{task_id}" for task_id in sorted(excluded)))}。

    ' + + '

    这是事后诊断口径,不是官方新分数。它按任务统一剔除任一配置已证实受任务/evaluator、站点、共享环境或参考状态干扰的任务,保证四个配置分母一致。

    ' + f'

    本轮剔除 {len(excluded)} 题:{_e("、".join(f"#{task_id}" for task_id in sorted(excluded)))}。

    ' ) diff --git a/harness/browser_use/report_i18n.py b/harness/browser_use/report_i18n.py index 037873c10..4a8e170b3 100644 --- a/harness/browser_use/report_i18n.py +++ b/harness/browser_use/report_i18n.py @@ -1556,6 +1556,10 @@ def translate_text(value: str) -> str: r"(\d+) 个逻辑结果", lambda match: f"{match.group(1)} logical results", ), + ( + r"本轮剔除 (\d+) 题:(.+)。", + lambda match: f"{match.group(1)} tasks excluded in this run: {match.group(2)}.", + ), ( r"(\d+) 个任务×浏览器结果均保留最终回答、官方 evaluator 结果、Agent 事件和证据哈希。", lambda match: f"All {match.group(1)} task-browser results retain the final answer, official evaluator output, agent events, and evidence hashes.", diff --git a/scripts/check_browser_use_report.py b/scripts/check_browser_use_report.py index 6679e14f1..584b11b33 100644 --- a/scripts/check_browser_use_report.py +++ b/scripts/check_browser_use_report.py @@ -426,6 +426,7 @@ def check( "该执行没有可公开的 trace 页面画面", "个物理执行均记录", "个物理执行均保存", + "本轮剔除", ) for page in english_pages: text = rendered_pages[page] From 4a90f8937cd4ea9eff7fcb1392d08691bc06d571 Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 11 Sep 2026 19:40:18 +0800 Subject: [PATCH 069/127] Bind final diagnostic report release --- evaluation/browser-use/current-report.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evaluation/browser-use/current-report.json b/evaluation/browser-use/current-report.json index 937ab4b52..6e43fe894 100644 --- a/evaluation/browser-use/current-report.json +++ b/evaluation/browser-use/current-report.json @@ -57,7 +57,7 @@ "base_url": "https://artifact-hub.app.lexmount.com", "site_slug": "7l77EeXPRfml", "share_url": "https://artifact-hub.app.lexmount.com/v/Ghn1BR6PCy-njsCMqCe2LRIJ-eyJEJoa", - "expected_version": "ver_ae8ba880bfff4a0e95655e02aedb72f3", + "expected_version": "ver_fedeedfcc9c84074af49b390e7ed8c36", "state": "verified" } } From e1bed72de1cbb9c41c31b520d2e184a4df76d853 Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 11 Sep 2026 20:56:22 +0800 Subject: [PATCH 070/127] Redesign browser use failure analysis --- AGENTS.md | 6 +- harness/browser_use/report.py | 427 +++++++++++++++++---------- harness/browser_use/report_i18n.py | 62 +++- research/report-changes.json | 24 +- scripts/check_browser_use_report.py | 30 +- tests/browser_use/test_report.py | 91 +++++- tests/browser_use/test_report_ui.mjs | 18 +- 7 files changed, 487 insertions(+), 171 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 4c14b8b28..e7d416bab 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,13 +16,13 @@ - Use `Browser Use` for the autonomous track and `Browser Trajectory Replay` (Chinese: `轨迹重放`) for the fixed BrowserTool-action track. Their implementation roots are `harness/browser_use/` and `harness/trajectory_replay/`; do not add aliases or compatibility paths using the retired names. - Browser Use reports expose one manifest-authoritative logical result per browser and task. Dataset routes contain only frozen task definitions and acceptance contracts; result routes own verdicts, timings and trajectories. Keep one filterable result inventory instead of adding duplicate success/failure tables. - Keep automatic evaluator failure location separate from reviewed root cause. A root-cause subtype and reason must come from a per-browser, per-task review bound to the current attempts hash; render both fields blank while that result is unverified, and require complete verified coverage before a formal release when the current profile enables the gate. -- Generate browser rows and comparisons from the participant registry. The same-task explorer uses one baseline browser, multi-select comparison browsers and explicit outcome relations, with browser labels linking to full results; never add one fixed table column per future browser. List Agent, model, prompt/tool contract, resources, environment and evaluator as shared controls. Standard summaries use mean, P50 and P95; mark only unique winners on directionally comparable metrics. For action latency, compare per-call latency and keep unequal-workload totals and call counts neutral. +- Generate browser rows and comparisons from the participant registry. The same-task explorer uses one baseline browser, multi-select comparison browsers and explicit outcome relations, with browser labels linking to full results; never add one fixed table column per future browser. List Agent, model, prompt/tool contract, resources, environment and evaluator as shared controls. Standard summaries use mean, P50 and P95; compare every registered model-by-browser configuration together and mark only globally unique winners on directionally comparable metrics. For action latency, compare all configurations by per-call latency and keep unequal-workload totals and call counts neutral. - Put a core-metrics comparison table near the start of every report home page, before secondary diagnosis or interpretation. Prefer tables over repeated cards for dense categorical summaries. Generate its rows and columns from the participant registry rather than assuming a fixed participant count. - Use reader-facing labels throughout reports. Do not publish unexplained internal terms or shorthand; Chinese copy must be understandable to a general reader without benchmark implementation context. State the population, denominator, and outcome relation where a compact label would otherwise be ambiguous. - Result-detail summaries contain only the verdict, concise failed-rule labels and short scalar facts. Full evaluator messages, expected/actual payloads, traces and logs belong in dedicated evidence sections, never narrow KPI cards. -- Result inventories are fixed-height, horizontally scrollable diagnosis tables. Do not place full reviewed causes, evaluator payloads, latency stacks or other paragraph-length content in inventory cells; link to the result detail instead. +- Result inventories are fixed-height, horizontally scrollable diagnosis tables. Do not place full reviewed causes, evaluator payloads, latency stacks or other paragraph-length content in inventory cells. Every failure row opens an in-page diagnosis with the failure stage, exact program error, reviewed cause, treatment and links to the relevant trajectory, screenshots and logs; the full result page remains the canonical evidence view. - Compare every registered model-by-browser configuration in one same-task matrix generated from the participant registry. Do not introduce pairwise selectors or duplicate comparison prose when more configurations can be represented as columns. -- Every failed result detail must expose the evidenced failure stage, the programmatic evaluator classification, the reviewed root cause, the remediation owner/status, a link to the relevant trajectory step, downloadable sanitized logs, and the last public trace frames when available. Extract frames only from ordered Playwright `screencast-frame` events, validate full-page dimensions and provenance, and never treat arbitrary trace resources as screenshots. Label frames as last observed pages, not as exact error screenshots unless the evidence proves that timing. +- Every failed result detail must expose the evidenced failure stage, the programmatic evaluator classification, the exact evaluator/runtime error, the reviewed root cause, the remediation owner/status, a link to the relevant trajectory step, sanitized logs, and the last public trace frames when available. Distinguish a failed BrowserTool action from the last successful action before evaluator rejection. Provide an inline HTML evidence viewer as well as a raw JSON link because static hosts may not preview JSON reliably. Extract frames only from ordered Playwright `screencast-frame` events, validate full-page dimensions and provenance, and never treat arbitrary trace resources as screenshots. Label frames as last observed pages, not as exact error screenshots unless the evidence proves that timing. - Keep official scores immutable when diagnosis finds benchmark/evaluator, site, shared-environment, or frozen-reference interference. Quarantine affected tasks across all configurations in a clearly labeled post-hoc diagnostic subset, record remediation in `research/browser-use-remediation.json`, and readmit them only after a preregistered full-scope rerun. - Split environment preparation, browser-session setup, Agent run, BrowserTool wall time, evidence finalization and evaluator time at collection. Publish the self-hosted deployment topology and image identity; claim per-execution reset/readiness only when a preparation receipt binds reset, readiness and application health. - Formal report prose is reader-facing: no prompts, reasoning, internal work narration, conversational rebuttals or references to parallel sessions. Review pages contain review rounds and corrections, not per-case audit tables; release notes contain reader-visible effects. diff --git a/harness/browser_use/report.py b/harness/browser_use/report.py index dca8b20f3..5354b88ed 100644 --- a/harness/browser_use/report.py +++ b/harness/browser_use/report.py @@ -755,12 +755,12 @@ def _task_family_table(families: list[dict[str, Any]]) -> str: def _failure_cause_table(verified_causes: dict[str, dict[str, Any]]) -> str: groups = { - "agent": ("Agent / 模型", "是", "保留为对比信号;通过轨迹修正检索、选择和提交策略。"), - "benchmark": ("任务 / evaluator", "否", "修正任务合同或 evaluator,验证后重跑;重跑前不进入干净子集。"), - "site": ("自托管站点", "否", "修复依赖、会话或保存链路,增加任务级写入预检后重跑。"), - "evidence": ("浏览器证据接口", "是", "保留为浏览器集成信号;补齐响应正文或 Referer 采集后重跑。"), - "environment": ("共享测试环境", "否", "跨进程串行锁已上线;受污染任务待重跑。"), - "mixed": ("参考状态 + Agent", "否", "先重新冻结参考状态,再重跑区分 Agent 错误。"), + "agent": ("Agent / 模型行为", "保留", "属于当前被测组合;按轨迹定位检索、选择或提交错误。"), + "benchmark": ("任务 / evaluator", "排除并修复", "修正验收合同,通过独立预检后重跑四种配置。"), + "site": ("自托管站点", "排除并修复", "修复依赖、会话或保存链路,通过读写预检后重跑。"), + "evidence": ("浏览器证据接口", "保留并修复", "这是浏览器集成能力的一部分;补齐响应正文或 Referer 采集后重跑。"), + "environment": ("共享测试环境", "排除,修复已完成", "跨进程串行锁已经上线;受污染任务仍需重跑。"), + "mixed": ("参考状态与 Agent 混合", "排除并重建", "重新冻结参考状态,再重跑以区分 Agent 错误。"), } def group_for(code: str) -> str: @@ -783,7 +783,7 @@ def group_for(code: str) -> str: continue rows.append([groups[key][0], total, groups[key][1], groups[key][2]]) return _table( - ["原因所在", "失败结果数", "是否属于目标变量", "处理"], + ["原因范围", "失败结果数", "本轮处理", "修复与验证"], rows, table_id="failure-cause-table", classes="wide-data-table", @@ -828,25 +828,6 @@ def _clean_subset_table( ) -def _headline_findings(summary: dict[str, dict[str, dict[str, Any]]]) -> str: - lines = [] - engines = list(ENGINE_LABELS) - for model, model_summary in summary.items(): - results = [ - f"{ENGINE_LABELS[engine]} {model_summary[engine]['passed']}/{model_summary[engine]['completed']}" - f"({_pct(model_summary[engine]['score'])})" - for engine in engines - ] - top = max(item["passed"] for item in model_summary.values()) - winner_ids = [engine for engine in engines if model_summary[engine]["passed"] == top] - outcome = "通过数相同" if len(winner_ids) > 1 else f"{ENGINE_LABELS[winner_ids[0]]} 多通过" - if len(engines) == 2 and len(winner_ids) == 1: - loser = next(engine for engine in engines if engine not in winner_ids) - outcome += f" {top - model_summary[loser]['passed']} 题" - lines.append(f"
  • {_e(MODEL_LABELS[model])}:{_e(','.join(results))};{_e(outcome)}。
  • ") - return '
      ' + "".join(lines) + "
    " - - def _adoption_top3(evidence: dict[str, Any]) -> str: families = sorted( (item for item in evidence.get("benchmark_families") or [] if item.get("interaction")), @@ -996,10 +977,11 @@ def _action_duration_cards(attempts: list[dict[str, Any]]) -> str: } ) cards: list[str] = [] - for model in MODEL_LABELS: - for action in actions: - metrics: dict[str, tuple[float, int, float | None]] = {} + for action in actions: + metrics: dict[str, tuple[float, int, float | None]] = {} + for model in MODEL_LABELS: for engine in ENGINE_LABELS: + key = f"{model}|{engine}" total = sum( float((row.get("browser_tool_duration_by_action_ms") or {}).get(action) or 0) for row in attempts @@ -1010,32 +992,33 @@ def _action_duration_cards(attempts: list[dict[str, Any]]) -> str: for row in attempts if _row_model(row) == model and row["engine"] == engine ) - metrics[engine] = (total, count, total / count if count else None) - average_winners = _best( - {engine: values[2] for engine, values in metrics.items()}, - higher=False, - display=lambda value: round(value / 1000, 3), - ) - maximum = max((values[2] or 0 for values in metrics.values()), default=0) - browser_rows = [] + metrics[key] = (total, count, total / count if count else None) + average_winners = _best( + {key: values[2] for key, values in metrics.items()}, + higher=False, + display=lambda value: round(value / 1000, 3), + ) + maximum = max((values[2] or 0 for values in metrics.values()), default=0) + browser_rows = [] + for model in MODEL_LABELS: for engine in ENGINE_LABELS: - total, count, average = metrics[engine] + key = f"{model}|{engine}" + total, count, average = metrics[key] width = 0 if average is None or maximum <= 0 else max(2.0, average / maximum * 100) average_text = "—" if average is None else f"{average / 1000:.3f} 秒" browser_rows.append( - f'
    ' - f"{_e(ENGINE_LABELS[engine])}{_winner(average_text, won=engine in average_winners, label='单次均值更低')}" + f'
    ' + f"{_e(MODEL_LABELS[model])} · {_e(ENGINE_LABELS[engine])}{_winner(average_text, won=key in average_winners, label='四种配置中单次均值最低')}" f'
    ' f'
    总耗时 {total / 1000:.1f} 秒 · {count} 次调用
    ' ) - cards.append( - '
    ' - + _e(action) - + "" - + f"{_e(MODEL_LABELS[model])} · 单次调用墙钟时间
    " - + "".join(browser_rows) - + "
    " - ) + cards.append( + '
    ' + + _e(action) + + "四种配置 · 单次调用墙钟时间
    " + + "".join(browser_rows) + + "
    " + ) return '
    ' + "".join(cards) + "
    " @@ -1051,31 +1034,31 @@ def _summary_comparison_rows(summary: dict[str, dict[str, dict[str, Any]]]) -> l "p50_tokens", "p95_tokens", ) + flat = { + f"{model}|{engine}": values + for model, model_summary in summary.items() + for engine, values in model_summary.items() + } + higher_metrics = { + name: _best({key: values[name] for key, values in flat.items()}, higher=True) + for name in ("passed", "score") + } + lower_metrics = { + name: _best( + {key: values[name] for key, values in flat.items()}, + higher=False, + display=( + (lambda value: round(value / 1000, 1)) + if "duration_ms" in name + else (lambda value: round(value)) + ), + ) + for name in lower_names + } rows: list[list[Any]] = [] for model, model_summary in summary.items(): - higher_metrics = { - "passed": _best( - {engine: model_summary[engine]["passed"] for engine in ENGINE_LABELS}, - higher=True, - ), - "score": _best( - {engine: model_summary[engine]["score"] for engine in ENGINE_LABELS}, - higher=True, - ), - } - lower_metrics = { - name: _best( - {engine: model_summary[engine][name] for engine in ENGINE_LABELS}, - higher=False, - display=( - (lambda value: round(value / 1000, 1)) - if "duration_ms" in name - else (lambda value: round(value)) - ), - ) - for name in lower_names - } for engine, label in ENGINE_LABELS.items(): + key = f"{model}|{engine}" current = model_summary[engine] rows.append( [ @@ -1083,30 +1066,30 @@ def _summary_comparison_rows(summary: dict[str, dict[str, dict[str, Any]]]) -> l label, _winner( f"{current['passed']}/{current['completed']}", - won=engine in higher_metrics["passed"], - label="通过数更高", + won=key in higher_metrics["passed"], + label="四种配置中通过数最高", ), _winner( _pct(current["score"]), - won=engine in higher_metrics["score"], - label="通过率更高", + won=key in higher_metrics["score"], + label="四种配置中通过率最高", ), _metric_stack( [ ( "均值", _seconds(current["mean_duration_ms"]), - engine in lower_metrics["mean_duration_ms"], + key in lower_metrics["mean_duration_ms"], ), ( "P50", _seconds(current["p50_duration_ms"]), - engine in lower_metrics["p50_duration_ms"], + key in lower_metrics["p50_duration_ms"], ), ( "P95", _seconds(current["p95_duration_ms"]), - engine in lower_metrics["p95_duration_ms"], + key in lower_metrics["p95_duration_ms"], ), ] ), @@ -1115,17 +1098,17 @@ def _summary_comparison_rows(summary: dict[str, dict[str, dict[str, Any]]]) -> l ( "均值", _seconds(current["mean_browser_tool_duration_ms"]), - engine in lower_metrics["mean_browser_tool_duration_ms"], + key in lower_metrics["mean_browser_tool_duration_ms"], ), ( "P50", _seconds(current["p50_browser_tool_duration_ms"]), - engine in lower_metrics["p50_browser_tool_duration_ms"], + key in lower_metrics["p50_browser_tool_duration_ms"], ), ( "P95", _seconds(current["p95_browser_tool_duration_ms"]), - engine in lower_metrics["p95_browser_tool_duration_ms"], + key in lower_metrics["p95_browser_tool_duration_ms"], ), ] ), @@ -1134,17 +1117,17 @@ def _summary_comparison_rows(summary: dict[str, dict[str, dict[str, Any]]]) -> l ( "均值", _number(current["mean_tokens"]), - engine in lower_metrics["mean_tokens"], + key in lower_metrics["mean_tokens"], ), ( "P50", _number(current["p50_tokens"]), - engine in lower_metrics["p50_tokens"], + key in lower_metrics["p50_tokens"], ), ( "P95", _number(current["p95_tokens"]), - engine in lower_metrics["p95_tokens"], + key in lower_metrics["p95_tokens"], ), ] ), @@ -1154,6 +1137,8 @@ def _summary_comparison_rows(summary: dict[str, dict[str, dict[str, Any]]]) -> l REPORT_JS = """function theme(){const r=document.documentElement,n=r.dataset.theme==='dark'?'light':'dark';r.dataset.theme=n;try{localStorage.setItem('browser-use-theme',n)}catch(e){}} +function openDiagnosis(id){const d=document.getElementById(id);if(d?.showModal)d.showModal()} +function closeDiagnosis(id){const d=document.getElementById(id);if(d?.open)d.close()} function updateViewUrl(){const u=new URL(location.href),q=document.querySelector('[data-search]');[...u.searchParams.keys()].filter(k=>k==='q'||k.startsWith('f_')).forEach(k=>u.searchParams.delete(k));if(q?.value)u.searchParams.set('q',q.value);document.querySelectorAll('[data-filter]').forEach(c=>{if(c.value)u.searchParams.set('f_'+c.dataset.filter,c.value)});history.replaceState(null,'',u.pathname+(u.search?'?'+u.searchParams.toString():'')+u.hash)} function filterRows(save=true){const controls=[...document.querySelectorAll('[data-filter]')],query=(document.querySelector('[data-search]')?.value||'').trim().toLowerCase();let visible=0,total=0;document.querySelectorAll('tbody tr[data-tier]:not([data-relation])').forEach(r=>{total++;const hidden=Boolean(controls.some(c=>c.value&&r.dataset[c.dataset.filter]!==c.value)||(query&&!String(r.dataset.search||r.textContent).toLowerCase().includes(query)));r.hidden=hidden;if(!hidden)visible++});document.querySelectorAll('[data-visible-count]').forEach(x=>x.textContent=visible+' / '+total);if(save)updateViewUrl()} function filterMatrix(){const root=document.getElementById('configuration-matrix');if(!root)return;const controls=[...root.querySelectorAll('[data-matrix-filter]')];let visible=0,total=0;root.querySelectorAll('tbody tr[data-tier]').forEach(r=>{total++;const hidden=controls.some(c=>c.value&&r.dataset[c.dataset.matrixFilter]!==c.value);r.hidden=hidden;if(!hidden)visible++});const count=root.querySelector('[data-matrix-count]');if(count)count.textContent=visible+' / '+total} @@ -1162,13 +1147,14 @@ def _summary_comparison_rows(summary: dict[str, dict[str, dict[str, Any]]]) -> l function applyAnchor(){if(!location.hash)return;const id=decodeURIComponent(location.hash.slice(1)),target=document.getElementById(id);if(target?.scrollIntoView)requestAnimationFrame(()=>target.scrollIntoView({block:'start'}))} function hasOption(control,value){return !control?.options||[...control.options].some(option=>option.value===value)} function restoreView(){const p=new URLSearchParams(location.search),q=document.querySelector('[data-search]');if(q&&p.has('q'))q.value=p.get('q')||'';document.querySelectorAll('[data-filter]').forEach(c=>{const v=p.get('f_'+c.dataset.filter);if(v!==null&&hasOption(c,v))c.value=v});const s=(p.get('sort')||'').split(':');if(s.length===3)applySort(document.getElementById(s[0]),Number(s[1]),s[2]);filterRows(false);filterMatrix();try{if(!location.hash){const y=sessionStorage.getItem('browser-use-scroll:'+location.pathname+location.search);if(y!==null)requestAnimationFrame(()=>scrollTo(0,Number(y)||0))}}catch(e){}} -document.addEventListener('DOMContentLoaded',()=>{try{document.documentElement.dataset.theme=localStorage.getItem('browser-use-theme')||'light'}catch(e){}restoreView();applyAnchor();document.querySelectorAll('[data-filter]').forEach(x=>x.addEventListener('change',()=>filterRows()));document.querySelectorAll('[data-search]').forEach(x=>x.addEventListener('input',()=>filterRows()));document.querySelectorAll('[data-matrix-filter]').forEach(x=>x.addEventListener('change',filterMatrix))});addEventListener('pagehide',()=>{try{sessionStorage.setItem('browser-use-scroll:'+location.pathname+location.search,String(scrollY))}catch(e){}});""" +document.addEventListener('DOMContentLoaded',()=>{try{document.documentElement.dataset.theme=localStorage.getItem('browser-use-theme')||'light'}catch(e){}restoreView();applyAnchor();document.querySelectorAll('[data-filter]').forEach(x=>x.addEventListener('change',()=>filterRows()));document.querySelectorAll('[data-search]').forEach(x=>x.addEventListener('input',()=>filterRows()));document.querySelectorAll('[data-matrix-filter]').forEach(x=>x.addEventListener('change',filterMatrix));document.querySelectorAll('dialog').forEach(d=>d.addEventListener('click',e=>{if(e.target===d)d.close()}))});addEventListener('pagehide',()=>{try{sessionStorage.setItem('browser-use-scroll:'+location.pathname+location.search,String(scrollY))}catch(e){}});""" REPORT_STYLE = ( SHARED_STYLE + """ +:root{--max:1440px} .hero h1,.publication-article h1,.publication-article h2{overflow-wrap:anywhere;word-break:break-word;max-width:100%} .callout strong{margin-right:.3em} -.top nav{gap:18px;flex-wrap:nowrap;overflow-x:auto;overscroll-behavior-inline:contain;scrollbar-width:thin}.top nav>a,.top nav>button{flex:0 0 auto}.top nav .brand{font-weight:850;color:var(--ink)}.top nav a[aria-current=page]{color:var(--ink);font-weight:800;text-decoration:underline;text-decoration-thickness:2px;text-underline-offset:8px}.top .theme-toggle{margin-left:auto}.page-main{min-height:78vh}.hero.compact{padding-bottom:22px}.hero.compact h1{font-size:clamp(34px,4.5vw,52px)}.fine{font-size:13px;color:var(--muted)}.result-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px}.result-grid .card{border-top:3px solid var(--accent)}.overview-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.overview-card{border:1px solid var(--line);border-radius:14px;background:var(--panel);padding:20px;min-width:0}.overview-card h3{margin:8px 0}.audit-strip{display:grid;gap:10px}.audit-strip .callout{margin:0}.filters,.pair-controls{display:flex;gap:10px;flex-wrap:wrap;align-items:end;margin:18px 0}.filters select,.filters input,.pair-controls select{padding:9px 12px;border:1px solid var(--line);border-radius:9px;background:var(--panel);color:var(--ink)}.filters input{min-width:260px}.comparison-count{margin-left:auto;padding:8px 12px;border-radius:999px;background:var(--wash);color:var(--accent);font-weight:750}.status-pass{color:var(--good);font-weight:750}.status-fail{color:var(--bad);font-weight:750}.status-unproven{color:var(--warn,var(--bad));font-weight:750}.flow{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:9px;list-style:none;padding:0}.flow li{padding:14px;border:1px solid var(--line);border-radius:11px;background:var(--panel);font-size:13px;min-width:0}.flow li+li:before{content:'→';color:var(--accent);margin-right:7px}.method-figure{margin:24px 0}.method-figure img{display:block;width:100%;height:auto;border:1px solid var(--line);border-radius:16px;background:#f6faff}.method-figure figcaption{margin-top:10px;color:var(--muted);font-size:13px;line-height:1.55}.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:12px}.detail-grid .kv{min-height:92px;min-width:0}.page-links{display:flex;gap:10px;flex-wrap:wrap}.page-links a{border:1px solid var(--line);background:var(--panel);padding:8px 12px;border-radius:9px}.rank-list{counter-reset:rank}.rank-item{counter-increment:rank;display:grid;grid-template-columns:54px minmax(0,1fr);gap:16px;padding:20px 0;border-top:1px solid var(--line)}.rank-item:before{content:counter(rank);font-size:34px;font-weight:850;color:var(--accent)}.rank-item h3{margin:4px 0}.article-entry{border-left:4px solid var(--accent)}.source-list li{margin:9px 0}.table-wrap{margin:18px 0}.table-wrap table{min-width:760px}#task-table{min-width:1280px}#pair-table{min-width:1180px}.table-wrap caption{text-align:left;padding:10px 12px;color:var(--muted);caption-side:bottom}.footer{border-top:1px solid var(--line)}.evidence-pair{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:14px}.evidence-card{border:1px solid var(--line);border-radius:12px;background:var(--panel);padding:16px;min-width:0}.evidence-card h3{margin-top:0}.json-view,.log-view{white-space:pre-wrap;overflow-wrap:anywhere;max-height:540px;overflow:auto;padding:14px;border:1px solid var(--line);border-radius:9px;background:var(--bg);font-size:12px;line-height:1.55}.trajectory{list-style:none;padding:0;counter-reset:step}.trajectory>li{counter-increment:step;display:grid;grid-template-columns:46px minmax(0,1fr);gap:12px;padding:15px 0;border-top:1px solid var(--line)}.trajectory>li:before{content:counter(step);display:grid;place-items:center;width:34px;height:34px;border-radius:50%;background:var(--accent);color:white;font-weight:800}.trajectory summary{cursor:pointer;font-weight:750}.rule-fail{border-left:4px solid var(--bad)}.rule-pass{border-left:4px solid var(--good)}.count-note{margin-left:auto;color:var(--muted);font-size:13px}.badge{display:inline-block;padding:3px 8px;border-radius:999px;background:var(--wash);font-size:12px;margin-right:6px}.muted-box{padding:14px;border:1px dashed var(--line);border-radius:10px;color:var(--muted)}@media(max-width:900px){.result-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.overview-grid{grid-template-columns:1fr}.flow{grid-template-columns:repeat(3,minmax(0,1fr))}.evidence-pair{grid-template-columns:1fr}.grid>.card{grid-column:1/-1!important}}@media(max-width:600px){.wrap{width:calc(100% - 28px)}.top nav{gap:14px}.result-grid,.flow{grid-template-columns:1fr}.flow li+li:before{content:'↓';display:block}.hero{padding-top:28px}.hero.compact h1{font-size:30px;line-height:1.18}.filters input{min-width:0;width:100%}.comparison-count{margin-left:0}.rank-item{grid-template-columns:36px minmax(0,1fr)}}@media print{.top,.filters,.pair-controls{display:none!important}.card,.table-wrap{box-shadow:none}.json-view,.log-view{max-height:none;overflow:visible}.evidence-pair{grid-template-columns:1fr}} +.top nav{gap:18px;flex-wrap:nowrap;overflow-x:auto;overscroll-behavior-inline:contain;scrollbar-width:thin}.top nav>a,.top nav>button{flex:0 0 auto}.top nav .brand{font-weight:850;color:var(--ink)}.top nav a[aria-current=page]{color:var(--ink);font-weight:800;text-decoration:underline;text-decoration-thickness:2px;text-underline-offset:8px}.top .theme-toggle{margin-left:auto}.page-main{min-height:78vh}.hero.compact{padding:34px 0 26px}.hero.compact h1{font-size:clamp(34px,4vw,50px);margin:10px 0}.hero.compact .lead{margin:0}.section{padding:32px 0}.fine{font-size:13px;color:var(--muted)}.result-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:14px}.result-grid .card{border-top:3px solid var(--accent)}.overview-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.overview-card{border:1px solid var(--line);border-radius:14px;background:var(--panel);padding:20px;min-width:0}.overview-card h3{margin:8px 0}.audit-strip{display:grid;gap:10px}.audit-strip .callout{margin:0}.filters,.pair-controls{display:flex;gap:10px;flex-wrap:wrap;align-items:end;margin:18px 0}.filters select,.filters input,.pair-controls select{padding:9px 12px;border:1px solid var(--line);border-radius:9px;background:var(--panel);color:var(--ink)}.filters input{min-width:260px}.comparison-count{margin-left:auto;padding:8px 12px;border-radius:999px;background:var(--wash);color:var(--accent);font-weight:750}.status-pass{color:var(--good);font-weight:750}.status-fail{color:var(--bad);font-weight:750}.status-unproven{color:var(--warn,var(--bad));font-weight:750}.flow{display:grid;grid-template-columns:repeat(6,minmax(0,1fr));gap:9px;list-style:none;padding:0}.flow li{padding:14px;border:1px solid var(--line);border-radius:11px;background:var(--panel);font-size:13px;min-width:0}.flow li+li:before{content:'→';color:var(--accent);margin-right:7px}.method-figure{margin:24px 0}.method-figure img{display:block;width:100%;height:auto;border:1px solid var(--line);border-radius:16px;background:#f6faff}.method-figure figcaption{margin-top:10px;color:var(--muted);font-size:13px;line-height:1.55}.detail-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(210px,1fr));gap:12px}.detail-grid .kv{min-height:92px;min-width:0}.page-links{display:flex;gap:10px;flex-wrap:wrap}.page-links a{border:1px solid var(--line);background:var(--panel);padding:8px 12px;border-radius:9px}.rank-list{counter-reset:rank}.rank-item{counter-increment:rank;display:grid;grid-template-columns:54px minmax(0,1fr);gap:16px;padding:20px 0;border-top:1px solid var(--line)}.rank-item:before{content:counter(rank);font-size:34px;font-weight:850;color:var(--accent)}.rank-item h3{margin:4px 0}.article-entry{border-left:4px solid var(--accent)}.source-list li{margin:9px 0}.table-wrap{margin:18px 0}.table-wrap table{min-width:760px}#task-table{min-width:1280px}#pair-table{min-width:1180px}.table-wrap caption{text-align:left;padding:10px 12px;color:var(--muted);caption-side:bottom}.footer{border-top:1px solid var(--line)}.evidence-pair{display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1fr);gap:14px}.evidence-card{border:1px solid var(--line);border-radius:12px;background:var(--panel);padding:16px;min-width:0}.evidence-card h3{margin-top:0}.json-view,.log-view{white-space:pre-wrap;overflow-wrap:anywhere;max-height:540px;overflow:auto;padding:14px;border:1px solid var(--line);border-radius:9px;background:var(--bg);font-size:12px;line-height:1.55}.trajectory{list-style:none;padding:0;counter-reset:step}.trajectory>li{counter-increment:step;display:grid;grid-template-columns:46px minmax(0,1fr);gap:12px;padding:15px 0;border-top:1px solid var(--line)}.trajectory>li:before{content:counter(step);display:grid;place-items:center;width:34px;height:34px;border-radius:50%;background:var(--accent);color:white;font-weight:800}.trajectory summary{cursor:pointer;font-weight:750}.rule-fail{border-left:4px solid var(--bad)}.rule-pass{border-left:4px solid var(--good)}.count-note{margin-left:auto;color:var(--muted);font-size:13px}.badge{display:inline-block;padding:3px 8px;border-radius:999px;background:var(--wash);font-size:12px;margin-right:6px}.muted-box{padding:14px;border:1px dashed var(--line);border-radius:10px;color:var(--muted)}@media(max-width:900px){.result-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.overview-grid{grid-template-columns:1fr}.flow{grid-template-columns:repeat(3,minmax(0,1fr))}.evidence-pair{grid-template-columns:1fr}.grid>.card{grid-column:1/-1!important}}@media(max-width:600px){.wrap{width:calc(100% - 28px)}.top nav{gap:14px}.result-grid,.flow{grid-template-columns:1fr}.flow li+li:before{content:'↓';display:block}.hero{padding-top:24px}.hero.compact h1{font-size:30px;line-height:1.18}.filters input{min-width:0;width:100%}.comparison-count{margin-left:0}.rank-item{grid-template-columns:36px minmax(0,1fr)}}@media print{.top,.filters,.pair-controls{display:none!important}.card,.table-wrap{box-shadow:none}.json-view,.log-view{max-height:none;overflow:visible}.evidence-pair{grid-template-columns:1fr}} /* Publication QA overrides: readable dense tables, code, and text-heavy figures. */ .table-wrap{overflow-x:auto;max-width:100%;contain:inline-size;border:1px solid var(--line);border-radius:14px;background:var(--panel)} .table-wrap table{min-width:860px;margin:0;border:0} @@ -1210,15 +1196,19 @@ def _summary_comparison_rows(summary: dict[str, dict[str, dict[str, Any]]]) -> l .comparison-winner{color:var(--good);font-weight:850}.winner-mark{display:inline-grid;place-items:center;min-width:22px;height:22px;padding:0 5px;margin-left:7px;border-radius:999px;background:var(--good);color:white;font-size:11px;line-height:1}.winner-cell{position:relative;padding:9px 10px;border:1px solid color-mix(in srgb,var(--good) 42%,var(--line));border-radius:10px;background:color-mix(in srgb,var(--good) 9%,var(--panel))}.metric-stack{display:grid;gap:5px;min-width:150px}.metric-stack>span{display:flex;align-items:center;justify-content:space-between;gap:12px;white-space:nowrap}.metric-stack>span>b{font-size:12px;color:var(--muted)}.review-round{border:1px solid var(--line);border-radius:16px;background:var(--panel);padding:22px;margin:16px 0}.review-round header{display:flex;align-items:start;justify-content:space-between;gap:18px}.review-round h3{margin:5px 0}.review-status{flex:0 0 auto;padding:5px 10px;border-radius:999px;background:var(--wash);font-size:12px;font-weight:800}.review-fields{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px;margin-top:16px}.review-fields>div{border-top:1px solid var(--line);padding-top:12px}.review-fields b{display:block;margin-bottom:5px}.review-fields p{margin:0} .browser-comparison{border:1px solid var(--line);border-radius:18px;background:var(--panel);padding:24px;box-shadow:0 12px 32px rgba(25,45,70,.06)}.comparison-heading{display:flex;align-items:start;justify-content:space-between;gap:24px;margin-bottom:10px}.comparison-heading h3{margin:0 0 6px;font-size:22px}.comparison-heading p{margin:0;color:var(--muted);line-height:1.65}.matrix-filters{margin:16px 0}.comparison-result{margin-top:12px}.table-wrap .configuration-matrix{min-width:1120px;table-layout:fixed}.configuration-matrix th:first-child,.configuration-matrix td:first-child{width:420px}.configuration-matrix th:not(:first-child),.configuration-matrix td:not(:first-child){width:165px;text-align:center;vertical-align:middle}.task-cell a{font-weight:750}.task-meta{display:flex;align-items:center;gap:7px;flex-wrap:wrap;margin-top:7px}.track-badge{display:inline-block;padding:3px 8px;border-radius:999px;background:var(--panel2);color:var(--muted);font-size:12px;white-space:nowrap}.outcome-pill{display:inline-block;min-width:62px;padding:6px 10px;border-radius:8px;font-size:13px;text-decoration:none;font-weight:750}.outcome-pill.pass{background:var(--success-bg);color:var(--good)}.outcome-pill.fail{background:var(--failure-bg);color:var(--bad)}.outcome-pill.missing{background:var(--panel2);color:var(--muted)} .action-card-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:14px}.action-card{border:1px solid var(--line);border-radius:16px;background:var(--panel);padding:18px;min-width:0}.action-card>header{display:flex;align-items:center;justify-content:space-between;gap:12px;margin-bottom:14px}.action-card>header code{font-size:16px;font-weight:800}.action-card>header span{font-size:12px;color:var(--muted)}.action-browser-row{padding:12px 0;border-top:1px solid var(--line)}.action-browser-head{display:flex;align-items:center;justify-content:space-between;gap:12px}.action-bar{height:7px;border-radius:999px;background:var(--panel2);overflow:hidden;margin:8px 0}.action-bar i{display:block;height:100%;border-radius:inherit;background:var(--accent)}.action-browser-row.action-winner .action-bar i{background:var(--good)}.action-browser-meta{font-size:12px;color:var(--muted)} -.finding-list{display:grid;gap:8px;margin:0 0 20px;padding:16px 20px 16px 38px;border-left:4px solid var(--accent);border-radius:0 12px 12px 0;background:var(--wash)}.finding-list li{padding-left:4px}.table-kicker{display:block;color:var(--muted);font:700 11px/1.4 ui-monospace,monospace;letter-spacing:.04em;margin-top:3px}.core-comparison-table{min-width:1040px}.core-comparison-table td:nth-child(1),.core-comparison-table td:nth-child(2){font-weight:750}.pair-outcome-table{min-width:900px}.wide-data-table td{line-height:1.65}.report-entry-links{margin-top:18px}.report-entry-links a{font-weight:750}.source-note a{margin-left:5px} +.finding-list{display:grid;gap:8px;margin:0 0 20px;padding:16px 20px 16px 38px;border-left:4px solid var(--accent);border-radius:0 12px 12px 0;background:var(--wash)}.finding-list li{padding-left:4px}.table-kicker{display:block;color:var(--muted);font:700 11px/1.4 ui-monospace,monospace;letter-spacing:.04em;margin-top:3px}.core-comparison-table{min-width:1240px;table-layout:fixed}.core-comparison-table td:nth-child(1),.core-comparison-table td:nth-child(2){font-weight:750}.core-comparison-table th:nth-child(1),.core-comparison-table td:nth-child(1){width:145px}.core-comparison-table th:nth-child(2),.core-comparison-table td:nth-child(2){width:135px}.core-comparison-table th:nth-child(3),.core-comparison-table td:nth-child(3){width:115px}.core-comparison-table th:nth-child(4),.core-comparison-table td:nth-child(4){width:105px}.core-comparison-table th:nth-child(n+5),.core-comparison-table td:nth-child(n+5){width:245px}.pair-outcome-table{min-width:900px}.wide-data-table td{line-height:1.65}.report-entry-links{margin-top:18px}.report-entry-links a{font-weight:750}.source-note a{margin-left:5px} +#task-family-table{min-width:1320px;table-layout:fixed}#task-family-table th:nth-child(1),#task-family-table td:nth-child(1){width:150px}#task-family-table th:nth-child(2),#task-family-table td:nth-child(2){width:90px}#task-family-table th:nth-child(3),#task-family-table td:nth-child(3){width:300px}#task-family-table th:nth-child(n+4),#task-family-table td:nth-child(n+4){width:190px;white-space:nowrap}.metric-note{margin:14px 0 0;border-top:1px solid var(--line);padding-top:12px;color:var(--muted)}.metric-note summary{cursor:pointer;font-weight:750;color:var(--ink)} +.diagnosis-button,.dialog-actions button{border:1px solid var(--accent);background:var(--accent);color:white;padding:8px 12px;border-radius:9px;font:inherit;font-weight:750;cursor:pointer;white-space:nowrap}.diagnosis-button:hover,.dialog-actions button:hover{filter:brightness(.94)}.diagnosis-dialog{width:min(920px,calc(100% - 32px));max-height:88vh;padding:0;border:1px solid var(--line);border-radius:18px;background:var(--panel);color:var(--ink);box-shadow:0 28px 90px rgba(10,25,45,.28)}.diagnosis-dialog::backdrop{background:rgba(5,15,25,.62);backdrop-filter:blur(3px)}.diagnosis-dialog-head{position:sticky;top:0;z-index:1;display:flex;align-items:start;justify-content:space-between;gap:20px;padding:22px 24px 16px;border-bottom:1px solid var(--line);background:var(--panel)}.diagnosis-dialog-head h2{font-size:24px;margin:4px 0}.dialog-close{border:0;background:var(--panel2);color:var(--ink);width:36px;height:36px;border-radius:50%;font-size:24px;line-height:1;cursor:pointer}.diagnosis-dialog-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;padding:18px 24px}.diagnosis-dialog-grid>div{padding:13px;border:1px solid var(--line);border-radius:10px;background:var(--bg);min-width:0;overflow-wrap:anywhere}.diagnosis-dialog-grid b{display:block;font-size:12px;color:var(--muted);margin-bottom:5px}.dialog-error,.dialog-review{padding:0 24px 18px}.dialog-error h3,.dialog-review h3{margin:8px 0}.dialog-error .log-view{max-height:240px}.dialog-review p{margin:6px 0}.dialog-actions{position:sticky;bottom:0;display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:16px 24px;border-top:1px solid var(--line);background:var(--panel)}.dialog-actions a{padding:8px 10px;border:1px solid var(--line);border-radius:9px}.dialog-actions button{margin-left:auto} +.failure-lead{padding:20px 22px;margin-bottom:14px;border:1px solid color-mix(in srgb,var(--bad) 35%,var(--line));border-left:6px solid var(--bad);border-radius:14px;background:var(--failure-bg)}.failure-lead .status-line{color:var(--bad);font-size:13px;font-weight:850}.failure-lead h3{font-size:24px;margin:5px 0}.failure-lead p{margin:4px 0}.success-lead{border-color:color-mix(in srgb,var(--good) 35%,var(--line));border-left-color:var(--good);background:var(--success-bg)}.success-lead .status-line{color:var(--good)}.runtime-strip{grid-template-columns:repeat(6,minmax(0,1fr));margin:14px 0}.failure-error{margin-top:16px}.failure-error .log-view{max-height:260px}.trajectory-step.failure-step{margin:8px 0;padding:14px;border:1px solid color-mix(in srgb,var(--bad) 40%,var(--line));border-left:5px solid var(--bad);border-radius:12px;background:var(--failure-bg)}.trajectory-step.failure-step:before{background:var(--bad)}.inline-evidence{margin:14px 0}.inline-evidence>summary{cursor:pointer;font-weight:750} +.action-card-grid{grid-auto-rows:1fr}.action-card{height:100%}.dialog-step,.step-signature{padding:13px 16px;border-radius:10px;background:var(--wash);overflow-wrap:anywhere}.dialog-step{margin:0 24px 18px}.dialog-step b{margin-right:8px}.step-signature{margin:0 0 14px} .attempt-summary{display:grid;grid-template-columns:minmax(270px,1.4fr) repeat(4,minmax(135px,.7fr));gap:12px;align-items:stretch}.attempt-verdict{padding:20px 22px;border:1px solid var(--line);border-left:5px solid var(--good);border-radius:12px;background:var(--success-bg)}.attempt-verdict.failure{border-left-color:var(--bad);background:var(--failure-bg)}.attempt-verdict .status-line{display:block;font-size:24px;font-weight:850;margin-bottom:6px}.attempt-verdict p{margin:0;line-height:1.65}.attempt-jumps{display:flex;gap:14px;flex-wrap:wrap;margin-top:14px;font-weight:750}.attempt-fact{padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--panel);overflow-wrap:anywhere}.attempt-fact b{display:block;color:var(--muted);font-size:12px;margin-bottom:5px}.attempt-secondary{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-top:12px} .diagnosis-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}.diagnosis-grid .attempt-fact{min-height:110px}.diagnosis-reason{margin-top:14px;padding:18px 20px;border-left:5px solid var(--bad);border-radius:10px;background:var(--failure-bg);line-height:1.7}.evidence-links{display:flex;gap:8px}.screenshot-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.screenshot-grid figure{margin:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel)}.screenshot-grid img{display:block;width:100%;aspect-ratio:16/10;object-fit:cover;object-position:top}.screenshot-grid figcaption{padding:10px 12px;color:var(--muted);font-size:12px} .task-goal{padding:22px 24px;border-radius:16px;background:var(--wash);border:1px solid var(--line);margin-bottom:18px}.task-goal span,.rule-index{display:block;color:var(--accent);font-size:12px;font-weight:850;letter-spacing:.08em;text-transform:uppercase}.task-goal p{font-size:20px;font-weight:750;line-height:1.6;margin:7px 0}.task-goal small{color:var(--muted)}.verification-list{display:grid;gap:14px}.verification-rule{padding:20px 22px;border:1px solid var(--line);border-left:5px solid var(--good);border-radius:14px;background:var(--panel);min-width:0}.verification-rule.rule-fail{border-left-color:var(--bad)}.verification-rule>header{display:flex;align-items:start;justify-content:space-between;gap:18px}.verification-rule h3{margin:4px 0 0;font-size:19px}.rule-verdict{flex:0 0 auto;padding:5px 10px;border-radius:999px;font-size:12px;font-weight:850}.rule-verdict.pass{background:var(--success-bg);color:var(--good)}.rule-verdict.fail{background:var(--failure-bg);color:var(--bad)}.rule-purpose{color:var(--muted);margin:12px 0 6px}.rule-summary{font-size:17px;font-weight:750;margin:0 0 14px}.verification-rule details{border-top:1px solid var(--line);padding-top:12px;margin-top:10px}.verification-rule summary{cursor:pointer;font-weight:750}.compact-evidence{margin-top:12px}.compact-evidence .json-view{max-height:360px} .page-main{overflow-x:clip} .section[id]{scroll-margin-top:76px} -@media(max-width:980px){.attempt-summary{grid-template-columns:repeat(2,minmax(0,1fr))}.attempt-verdict{grid-column:1/-1}.attempt-secondary,.diagnosis-grid{grid-template-columns:repeat(2,minmax(0,1fr))}.screenshot-grid{grid-template-columns:1fr}} +@media(max-width:980px){.attempt-summary{grid-template-columns:repeat(2,minmax(0,1fr))}.attempt-verdict{grid-column:1/-1}.attempt-secondary,.diagnosis-grid,.runtime-strip{grid-template-columns:repeat(2,minmax(0,1fr))}.screenshot-grid{grid-template-columns:1fr}} @media(max-width:760px){.browser-comparison{padding:18px}.comparison-heading{display:block}.comparison-count{display:inline-block;margin-top:12px}.comparison-controls{grid-template-columns:1fr}.action-card-grid{grid-template-columns:1fr}} -@media(max-width:600px){.hero h1{word-break:break-all;line-break:anywhere;text-wrap:balance}.method-figure img{min-width:900px}.table-wrap{margin-left:-4px;margin-right:-4px}.json-view,.log-view{font-size:13px}.review-round header{display:block}.review-status{display:inline-block;margin-top:8px}.review-fields,.attempt-summary,.attempt-secondary{grid-template-columns:1fr}} +@media(max-width:600px){.hero h1{word-break:break-all;line-break:anywhere;text-wrap:balance}.method-figure img{min-width:900px}.table-wrap{margin-left:-4px;margin-right:-4px}.json-view,.log-view{font-size:13px}.review-round header{display:block}.review-status{display:inline-block;margin-top:8px}.review-fields,.attempt-summary,.attempt-secondary,.diagnosis-dialog-grid{grid-template-columns:1fr}.diagnosis-dialog-head,.diagnosis-dialog-grid,.dialog-error,.dialog-review,.dialog-actions{padding-left:16px;padding-right:16px}.dialog-actions button{margin-left:0}} """ ) NAV = [ @@ -1367,6 +1357,33 @@ def _failure_diagnosis(result: dict[str, Any] | None, row: dict[str, Any]) -> st return ";".join(dict.fromkeys(x.strip() for x in parts if x.strip())).rstrip("。.;; ") + "。" +def _evaluator_error_text(result: dict[str, Any] | None, row: dict[str, Any]) -> str: + """Return the exact sanitized evaluator/runtime error without inferring a cause.""" + + lines: list[str] = [] + for item in evaluator_summary(result): + if float(item.get("score") or 0) == 1: + continue + messages = [ + str(message) + for assertion in item.get("assertions") or [] + for message in [*(assertion.get("assertion_msgs") or []), assertion.get("error_msg")] + if message + ] + if item.get("error"): + messages.append(str(item["error"])) + if not messages: + messages.append(_rule_outcome_summary(item)) + for message in dict.fromkeys(messages): + lines.append(f"规则 {item['index']} · {item['name']}: {sanitize(message)}") + if row.get("error"): + lines.append(f"运行错误: {sanitize(row['error'])}") + response = row.get("agent_response") or {} + if response.get("error_details"): + lines.append(f"Agent 错误: {sanitize(response['error_details'])}") + return "\n".join(lines) or "程序没有提供更具体的错误文本。" + + def _failed_rule_labels(result: dict[str, Any] | None) -> list[str]: return [ f"规则 {item['index']} {item['name']}" @@ -1423,6 +1440,23 @@ def _failure_point(code: str, steps: list[dict[str, Any]]) -> tuple[str, int | N return (f"第 {last_step} 步后的最终回答/任务状态" if last_step else "最终回答/任务状态"), last_step +def _trajectory_context( + steps: list[dict[str, Any]], step_number: int | None, failure_point: str +) -> tuple[str, str]: + """Describe the linked step without claiming a successful action caused evaluator failure.""" + + if step_number is None: + return "", "" + step = next( + (item for item in steps if int(item.get("step") or 0) == step_number), None + ) + if step is None: + return "", "" + label = trajectory_label(step) + prefix = "失败动作" if "BrowserTool 执行异常" in failure_point else "验收前最后一步" + return prefix, label + + def _remediation(code: str) -> tuple[str, str, bool]: """Return owner, current action, and whether this is an external interference.""" @@ -1537,12 +1571,16 @@ def _trajectory_html(steps: list[dict[str, Any]]) -> str: rows = [] for step in steps: step_number = int(step.get("step") or len(rows) + 1) + failed = ( + str(step.get("status") or "").lower() not in {"", "success"} + or bool(step.get("error")) + ) duration = step.get("duration_seconds") meta = f"Turn {step.get('turn') or '—'} · {_e(step.get('status') or 'unknown')}" if duration is not None: meta += f" · {float(duration):.2f} 秒" rows.append( - f'
  • ' + f'
  • ' + _e(trajectory_label(step)) + f'

    {meta}

    调用参数

    ' + _json_pre(step.get("arguments")) @@ -1617,7 +1655,7 @@ def _artifact_links(result_id: str, execution: int) -> str: root = f"artifacts/{result_id}/executions/{execution}" return ( '" ) @@ -1664,7 +1702,12 @@ def _write_execution_artifacts( } for label, filename in PUBLIC_EXECUTION_ARTIFACTS.items() } - return {**measurements, "artifacts": artifacts, "screenshots": screenshots}, response, evaluation, steps + return { + **measurements, + "artifacts": artifacts, + "screenshots": screenshots, + "_inline_evidence": bundle, + }, response, evaluation, steps def _screenshot_gallery(result_id: str, executions: list[dict[str, Any]]) -> str: @@ -1709,6 +1752,9 @@ def _execution_evidence_html(result_id: str, executions: list[dict[str, Any]]) - f"BrowserTool {execution['action_count']} 次 / {_seconds(execution['browser_tool_duration_ms'])} · " f"模型 tokens {_number(usage['total_tokens'])}

    " + _artifact_links(result_id, int(execution["execution"])) + + '
    在页面中查看完整执行证据' + + _json_pre(execution.get("_inline_evidence") or {}) + + "
    " + hashes + "" ) @@ -1916,6 +1962,52 @@ def _task_table(tasks: list[dict[str, Any]]) -> str: ) +def _diagnosis_dialog( + *, + result_id: str, + task_id: int, + configuration: str, + failure_point: str, + automatic_failure: str, + evaluator_error: str, + subcategory: str, + reason: str, + owner: str, + action: str, + step_link: str, + step_context_label: str = "", + step_context: str = "", +) -> str: + dialog_id = f"diagnosis-{result_id}" + detail_url = f"results/{result_id}.html" + return ( + f'' + '
    失败诊断' + f'

    Task #{task_id} · {_e(configuration)}

    ' + f'
    ' + '
    ' + f'' + f'
    程序判定{_e(automatic_failure or "未分类")}
    ' + f'
    复核根因{_e(subcategory or "待复核")}
    ' + f'
    负责范围{_e(owner)}
    ' + + ( + f'
    {_e(step_context_label)}{_e(step_context)}
    ' + if step_context + else "" + ) + + '

    程序报错

    ' + f'
    {_e(evaluator_error)}
    ' + '

    复核结论

    ' + f'

    {_e(reason or "尚未完成根因复核。")}

    处理:{_e(action)}

    ' + '
    ' + ) + + def _attempt_table( attempts: list[dict[str, Any]], tasks: list[dict[str, Any]], @@ -1924,6 +2016,7 @@ def _attempt_table( verified_causes = verified_causes or {} task_meta = {int(task["task_id"]): task for task in tasks} rows = [] + dialogs = [] for row in attempts: p = _public_attempt(row) result = "pass" if p["score"] == 1 else "fail" @@ -1934,6 +2027,9 @@ def _attempt_table( event_record = read_public_json(Path((row.get("paths") or {}).get("events", ""))) steps = trajectory(event_record if isinstance(event_record, list) else []) failure_point, step_number = _failure_point(code, steps) + step_context_label, step_context = _trajectory_context( + steps, step_number, failure_point + ) owner, action, _external = _remediation(code) task = task_meta.get(p["task_id"], {}) point_link = ( @@ -1941,6 +2037,25 @@ def _attempt_table( if step_number else f'results/{p["id"]}.html#diagnosis' ) + dialog_id = f"diagnosis-{p['id']}" + if result == "fail": + dialogs.append( + _diagnosis_dialog( + result_id=p["id"], + task_id=p["task_id"], + configuration=f"{MODEL_LABELS[p['model']]} · {ENGINE_LABELS[p['engine']]}", + failure_point=failure_point, + automatic_failure=automatic_failure, + evaluator_error=_evaluator_error_text(result_record, row), + subcategory=subcategory, + reason=reason, + owner=owner, + action=action, + step_link=point_link, + step_context_label=step_context_label, + step_context=step_context, + ) + ) values = [ Safe(f'{p["id"]}'), Safe( @@ -1955,9 +2070,7 @@ def _attempt_table( Safe(f'{_e(owner)}{_e(action)}') if result == "fail" else "—", Safe( ( - f'诊断' - f'画面' - f'日志' + f'' ) if result == "fail" else f'详情' @@ -1987,6 +2100,7 @@ def _attempt_table( return ( _filters(attempts=True) + f'
    {headers}{"".join(rows)}
    ' + + "".join(dialogs) ) @@ -2239,31 +2353,20 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat f"CowAgent 使用 {', '.join(MODEL_LABELS.values())},在同一组 WebArena‑Verified {task_count} 题上分别操作 {', '.join(ENGINE_LABELS.values())}。", ) home += _section( - "评测结果", - _headline_findings(summary_by_model) - + _table( + "四种配置的总体结果", + _table( ["模型", "浏览器", "通过任务", "通过率", "Agent 总耗时", "BrowserTool 耗时", "Tokens"], _summary_comparison_rows(summary_by_model), table_id="core-metrics-table", classes="core-comparison-table", ) - + '

    只标记同一模型下数值唯一更好的浏览器。通过数越高越好;耗时和 Tokens 越低越好。Agent 总耗时包含模型推理、页面等待和浏览器操作,BrowserTool 耗时更接近浏览器执行层,但仍包含工具封装与 CDP 往返。

    ' - + f'

    这 {task_count} 题是固定选择的样本,结果不能直接外推到 WebArena‑Verified 全集,也不能仅凭总耗时判断浏览器内核速度。

    ', - ) - home += _section( - "同一任务上的结果差异", - "

    每行比较同一模型、同一任务和同一网站状态下的两个浏览器。A、B 对应表内所列浏览器。

    " - + _table( - ["模型", "浏览器 A", "浏览器 B", "同题数", "两边都通过", "A 通过、B 失败", "A 失败、B 通过", "两边都失败"], - pairing_rows, - table_id="pair-outcomes", - classes="pair-outcome-table", - ) - + f'

    两组模型合计有 {discordant_total} 个同题结果不同;不同模型的运行互不配对。打开逐题对照 →

    ', + + '

    在四种配置中统一比较并只标记唯一最优值;相同值不标。通过数越高越好,耗时和 Tokens 越低越好。

    ' + + f'

    四种配置均使用同一组 {task_count} 题。模型和浏览器同时变化时,表格比较的是整套配置,不能把差值只归因于浏览器。查看全部配置的逐题矩阵 →

    ' + + '
    耗时统计包含什么

    Agent 总耗时包含模型推理、页面等待和浏览器操作;BrowserTool 耗时是全部浏览器工具调用的墙钟时间之和,仍包含工具封装与 CDP 往返,不是纯内核 CPU 时间。

    ', ) home += _section( - f"{len(failed_result_ids)} 个失败是怎么造成的", - "

    官方 evaluator 说明哪条验收规则没有满足;下表来自对任务要求、最终回答、网络记录和自主执行轨迹的逐条核验。

    " + "失败与干扰项", + f"

    {len(failed_result_ids)} 个失败结果已经逐条核验。与任务合同、站点、共享环境或参考状态有关的任务不会进入诊断子集,修复后需完整重跑。

    " + failure_overview, ) home += _section( @@ -2530,11 +2633,11 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat analysis = _hero( "Results", - "从总体配对下钻到每个逻辑结果", - "先看总体与同题比较,再通过筛选下钻到每个结果的官方规则、实际回答、轨迹和日志。", + "结果分析", + "四种配置先在同一张表和同一任务矩阵中比较;失败结果可直接打开诊断,查看发生位置、程序报错、复核根因、轨迹、画面和日志。", ) analysis += _section( - "汇总", + "四种配置总览", _table( [ "模型", @@ -2546,28 +2649,30 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat "Tokens", ], _summary_comparison_rows(summary_by_model), + table_id="analysis-core-metrics", + classes="core-comparison-table", ) - + '

    标记方向明确且数值更优的指标:通过数与通过率越高越优,耗时与 Tokens 越低越优;数值相同时不标记。

    ' - + '

    Agent 链路累计耗时把获得该任务结果实际发生的执行相加;每次从浏览器会话准备开始,到 CowAgent 完成、验收所需证据落盘前结束,包含登录、模型推理、调度、页面等待、BrowserTool 与收尾,不包含此前的 Docker 站点重置。BrowserTool 累计耗时是事件流中全部 browser 工具调用墙钟时间之和,包含工具封装、CDP 往返与页面等待,仍不是纯内核 CPU 时间。

    ', - ) - analysis += _section( - "BrowserTool 动作延迟", - "

    每张卡对应一种 CowAgent action。以单次调用墙钟均值作为主要比较值并标记较低者;总耗时与调用数用于解释工作量,不判优。该指标仍包含工具封装、CDP 往返和页面等待,不是纯内核 CPU 时间。

    " - + _action_duration_cards(attempts), + + '

    四种配置统一比较,只标记唯一最优值;相同值不标。通过数越高越好,耗时和 Tokens 越低越好。

    ' + + '
    耗时和 Tokens 如何统计

    Agent 链路累计耗时把取得逻辑结果所发生的执行相加,包含登录、模型推理、调度、页面等待、BrowserTool 与收尾,不包含此前的 Docker 站点重置。BrowserTool 累计耗时是所有 browser 工具调用的墙钟时间之和,包含工具封装、CDP 往返和页面等待。Tokens 同样累计正式结果包含的物理执行。

    ', ) analysis += _section( "同题全配置对比", - "

    每道任务同时展示全部模型×浏览器结果,不做两两拆分,也不用不同成功样本拼接结论。

    " + "

    每行是一道任务,四列分别对应模型×浏览器配置;可筛出结果有差异、全部通过或全部失败的任务。

    " + _same_task_comparison(tasks, attempts), ).replace('
    ', '
    ', 1) + analysis += _section( + "BrowserTool 动作延迟", + "

    每个动作同时比较四种配置的单次调用墙钟均值,只标记唯一最低值。总耗时和调用数受 Agent 行为量影响,仅用于解释工作量,不参与快慢判定。

    " + + _action_duration_cards(attempts), + ) analysis += _section( "排除已证实外部干扰后的诊断子集", "

    官方 90 题结果保持不变。为了看清浏览器和模型差异,下表暂时排除已核验为评测合同、站点、共享环境或参考状态问题的整道任务。

    " + _clean_subset_table(attempts, verified_causes), ) analysis += _section( - f"{result_count} 个逻辑结果", - "

    默认只显示失败;可按浏览器、层级、站点、任务类型、失败环节和全文关键词筛选。

    " + f"{result_count} 个结果与失败诊断", + "

    默认显示失败。点击“查看诊断”可先在当前页核对发生位置、程序报错、复核根因和处理方案,再进入完整轨迹、画面与日志。

    " + _attempt_table(attempts, tasks, verified_causes), ) (stage / "analysis/index.html").write_text( @@ -2618,7 +2723,10 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat "authoritative_execution": int(row["attempt"]), "task_page": f"datasets/{public['task_id']}.html", "result_page": f"results/{public['id']}.html", - "executions": execution_records, + "executions": [ + {key: value for key, value in execution.items() if key != "_inline_evidence"} + for execution in execution_records + ], } ) usage, resource, checks = ( @@ -2628,13 +2736,20 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat ) cause_code = _cause_code(verified_causes, public["id"]) failure_point, failure_step = _failure_point(cause_code, steps) + step_context_label, step_context = _trajectory_context( + steps, failure_step, failure_point + ) remediation_owner, remediation_action, external_interference = _remediation( cause_code ) detail = _hero( public["id"], f"{MODEL_LABELS[public['model']]} · {ENGINE_LABELS[public['engine']]} · Task #{public['task_id']}", - "先看任务目标与验收结论,再按需展开技术明细和 CowAgent 的每一步 BrowserTool 调用。", + ( + f"失败发生在{failure_point};下方保留程序报错、复核根因和对应执行证据。" + if float(public["score"]) < 1 + else "该结果通过全部官方程序化验收;下方可核对任务合同、执行轨迹和运行证据。" + ), ) # Pages declare so every internal URL is rooted at # the report bundle, including same-page fragment links. @@ -2656,20 +2771,14 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat + f'运行与证据' + "" ) - detail += _section( - "结果与运行", - f'
    ' - f'
    累计自主操作{public["action_count"]} 次 BrowserTool 调用
    ' - f'
    Agent 链路累计耗时{_seconds(public["duration_ms"])}
    ' - f'
    BrowserTool 累计耗时{_seconds(public["browser_tool_duration_ms"])}
    ' - f'
    模型 tokens{_number(usage["total_tokens"])}
    ' - f'
    模型请求{usage["request_count"]} 次调用 / {usage["request_attempt_count"]} 次传输尝试
    ' - f'
    传输错误{public["transport_error_count"]}
    ' - f'
    Run ID{_e(public["run_id"] or "未记录")}
    ' - f'
    证据完整性审计{_e(item.get("outcome") or "未记录")}
    ' - '

    Agent 链路耗时包含会话准备、登录、模型推理、调度、工具执行和收尾,不包含 Docker 站点重置;BrowserTool 耗时是全部 browser 工具调用的墙钟时间之和,不等同于纯内核时间。

    ', + runtime_facts = ( + '
    ' + f'
    自主操作{public["action_count"]} 次
    ' + f'
    Agent 链路{_seconds(public["duration_ms"])}
    ' + f'
    BrowserTool{_seconds(public["browser_tool_duration_ms"])}
    ' + f'
    模型 tokens{_number(usage["total_tokens"])}
    ' + f'
    模型请求{usage["request_count"]} 次 / {usage["request_attempt_count"]} 次传输
    ' + f'
    Run ID{_e(public["run_id"] or "未记录")}
    ' ) if float(public["score"]) < 1: step_href = ( @@ -2679,24 +2788,42 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat ) detail += '
    ' + _section( "失败诊断", - '
    ' - f'' - f'
    程序自动定位{_e(public["automatic_failure_type"] or "无")}
    ' - f'
    人工复核根因{_e(public["verified_failure_subcategory"] or "待复核")}
    ' - f'
    负责对象与处理{_e(remediation_owner)}
    {_e(remediation_action)}
    ' - '
    核验依据:' + '' + + ( + f'

    {_e(step_context_label)}:{_e(step_context)}

    ' + if step_context + else "" + ) + + '
    ' + f'
    程序判定{_e(public["automatic_failure_type"] or "无")}
    ' + f'
    复核根因{_e(public["verified_failure_subcategory"] or "待复核")}
    ' + f'
    负责范围{_e(remediation_owner)}
    ' + f'
    处理{_e(remediation_action)}
    ' + '

    程序报错

    ' + f'
    {_e(_evaluator_error_text(evaluation_record, row))}
    ' + '
    复核结论:' + _e(public["verified_failure_reason"] or "当前尚无已核验根因。") + '
    ' + f'查看页面画面' + f'查看完整日志

    ' + + runtime_facts + ( '

    对比口径:该结果已确认含非模型/浏览器干扰,官方分数保留,但暂不纳入干净子集。

    ' if external_interference else "" ), ) + else: + detail += _section( + "结果", + '
    通过' + f'

    {_e(_failure_brief(evaluation_record, row))}

    ' + + runtime_facts, + ) detail += '
    ' + _section( "任务要求与验收结果", _verification_html(task, evaluation_record, response_record) diff --git a/harness/browser_use/report_i18n.py b/harness/browser_use/report_i18n.py index 4a8e170b3..4d61a0526 100644 --- a/harness/browser_use/report_i18n.py +++ b/harness/browser_use/report_i18n.py @@ -1228,8 +1228,19 @@ def _register_report_change_translations() -> None: TEXT.update( { + "四种配置的总体结果": "Overall results across four configurations", + "四种配置总览": "Four-configuration overview", + "四种配置 · 单次调用墙钟时间": "Four configurations · per-call wall time", + "四种配置中通过数最高": "Highest pass count across four configurations", + "四种配置中通过率最高": "Highest pass rate across four configurations", + "四种配置中单次均值最低": "Lowest mean per-call latency across four configurations", + "四种配置统一比较,只标记唯一最优值;相同值不标。通过数越高越好,耗时和 Tokens 越低越好。": "All four configurations are compared together, and only a unique best value is marked. Ties are unmarked. Higher pass counts are better; lower latency and token use are better.", + "耗时和 Tokens 如何统计": "How latency and tokens are measured", + "Agent 链路累计耗时把取得逻辑结果所发生的执行相加,包含登录、模型推理、调度、页面等待、BrowserTool 与收尾,不包含此前的 Docker 站点重置。BrowserTool 累计耗时是所有 browser 工具调用的墙钟时间之和,包含工具封装、CDP 往返和页面等待。Tokens 同样累计正式结果包含的物理执行。": "Cumulative Agent-path latency sums the executions used to obtain the logical result. It includes sign-in, model inference, orchestration, page waits, BrowserTool calls, and cleanup, but excludes the preceding Docker site reset. Cumulative BrowserTool latency sums the wall time of all browser-tool calls, including tool-wrapper, CDP round-trip, and page-wait overhead. Tokens likewise sum the physical executions included in the official result.", + "结果分析": "Results analysis", + "四种配置先在同一张表和同一任务矩阵中比较;失败结果可直接打开诊断,查看发生位置、程序报错、复核根因、轨迹、画面和日志。": "The four configurations are compared in one summary table and one same-task matrix. Open any failed result to inspect its location, exact program error, reviewed root cause, trajectory, screenshots, and logs.", "同题全配置对比": "Same-task comparison across all configurations", - "每道任务同时展示全部模型×浏览器结果,不做两两拆分,也不用不同成功样本拼接结论。": "Each task shows every model-by-browser outcome together. The report does not split them into pairwise views or combine different successful subsets.", + "每行是一道任务,四列分别对应模型×浏览器配置;可筛出结果有差异、全部通过或全部失败的任务。": "Each row is one task, with four model-by-browser configuration columns. Filter for differing outcomes, all-pass tasks, or all-fail tasks.", "同题全配置矩阵": "Same-task all-configuration matrix", "每行是同一任务,每列是一个模型×浏览器配置。新增配置只增加列。": "Each row is one task and each column is one model-by-browser configuration. New configurations add columns only.", "关系": "Outcome pattern", @@ -1240,6 +1251,43 @@ def _register_report_change_translations() -> None: "官方 90 题结果保持不变。为了看清浏览器和模型差异,下表暂时排除已核验为评测合同、站点、共享环境或参考状态问题的整道任务。": "The official 90-task results remain unchanged. To isolate browser and model differences, this table temporarily removes every task confirmed to have a task-contract, site, shared-environment, or reference-state problem.", "这是事后诊断口径,不是官方新分数。它按任务统一剔除任一配置已证实受任务/evaluator、站点、共享环境或参考状态干扰的任务,保证四个配置分母一致。": "This is a post-hoc diagnostic view, not a new official score. It removes a whole task across all configurations when any result is confirmed to be affected by task/evaluator, site, shared-environment, or reference-state interference, keeping denominators aligned.", "失败诊断": "Failure diagnosis", + "发生位置": "Location", + "程序判定": "Program verdict", + "程序报错": "Program error", + "程序没有提供更具体的错误文本。": "The program did not provide a more specific error message.", + "复核结论": "Reviewed conclusion", + "复核结论:": "Reviewed conclusion:", + "负责范围": "Owning scope", + "失败动作": "Failed action", + "验收前最后一步": "Last step before evaluation", + "查看诊断": "Open diagnosis", + "查看期望与实际值": "View expected and actual values", + "查看页面画面": "View page screenshots", + "查看日志与完整证据": "View logs and complete evidence", + "查看完整日志": "View complete logs", + "关闭": "Close", + "尚未完成根因复核。": "Root-cause review is not complete.", + "当前尚无已核验根因。": "No verified root cause is available yet.", + "打开原始 JSON": "Open raw JSON", + "在页面中查看完整执行证据": "View complete execution evidence on this page", + "失败与干扰项": "Failures and interference", + "原因范围": "Cause scope", + "本轮处理": "Treatment in this run", + "修复与验证": "Fix and verification", + "保留": "Retain", + "排除并修复": "Exclude and fix", + "排除,修复已完成": "Exclude; fix completed", + "排除并重建": "Exclude and rebuild", + "属于当前被测组合;按轨迹定位检索、选择或提交错误。": "Part of the configuration under test; use the trajectory to locate retrieval, selection, or submission errors.", + "修正验收合同,通过独立预检后重跑四种配置。": "Correct the acceptance contract, pass an independent preflight, then rerun all four configurations.", + "修复依赖、会话或保存链路,通过读写预检后重跑。": "Fix the dependency, session, or save path, then rerun after read/write preflight checks.", + "这是浏览器集成能力的一部分;补齐响应正文或 Referer 采集后重跑。": "This is part of browser integration capability; complete response-body or Referer capture and rerun.", + "跨进程串行锁已经上线;受污染任务仍需重跑。": "The cross-process serialization lock is deployed; contaminated tasks still require reruns.", + "重新冻结参考状态,再重跑以区分 Agent 错误。": "Refreeze the reference state, then rerun to distinguish Agent errors.", + "参考状态与 Agent 混合": "Reference state mixed with Agent behavior", + "每个动作同时比较四种配置的单次调用墙钟均值,只标记唯一最低值。总耗时和调用数受 Agent 行为量影响,仅用于解释工作量,不参与快慢判定。": "Each action compares mean per-call wall time across all four configurations and marks only a unique minimum. Total time and call count depend on Agent workload, so they explain workload but are not ranked.", + "默认显示失败。点击“查看诊断”可先在当前页核对发生位置、程序报错、复核根因和处理方案,再进入完整轨迹、画面与日志。": "Failures are shown by default. Select Open diagnosis to inspect the location, exact program error, reviewed root cause, and treatment before opening the full trajectory, screenshots, and logs.", + "该结果通过全部官方程序化验收;下方可核对任务合同、执行轨迹和运行证据。": "This result passed every official programmatic evaluator. The task contract, execution trajectory, and runtime evidence are available below.", "失败位置": "Failure stage", "人工复核根因": "Reviewed root cause", "负责对象与处理": "Owner and treatment", @@ -1366,6 +1414,18 @@ def translate_text(value: str) -> str: translated = TEXT.get(core) if translated is None: patterns = ( + ( + r"(\d+) 个结果与失败诊断", + lambda m: f"{m.group(1)} results and failure diagnoses", + ), + ( + r"失败发生在(.+);下方保留程序报错、复核根因和对应执行证据。", + lambda m: f"The failure occurred at {translate_text(m.group(1))}. The exact program error, reviewed root cause, and corresponding execution evidence are provided below.", + ), + ( + r"四种配置均使用同一组 (\d+) 题。模型和浏览器同时变化时,表格比较的是整套配置,不能把差值只归因于浏览器。", + lambda m: f"All four configurations use the same {m.group(1)} tasks. When both model and browser vary, the table compares complete configurations; differences cannot be attributed to the browser alone.", + ), ( r"(.+?) · (Chromium|Moli 1\.1\.3) 结果", lambda m: f"{m.group(1)} · {m.group(2)} result", diff --git a/research/report-changes.json b/research/report-changes.json index b8abc575b..4f9abb9fd 100644 --- a/research/report-changes.json +++ b/research/report-changes.json @@ -151,13 +151,19 @@ "id": "2026-09-11-diagnostic-workbench-and-clean-subset", "date": "2026-09-11", "title": "将结果分析改为故障定位台", - "summary": "同题比较改为全部模型与浏览器配置矩阵;失败清单改为固定高度的横向诊断表;每个失败详情补齐失败位置、自动判定、复核根因、处理责任、日志与最后页面画面。官方分数不变,另提供排除已证实外部干扰的诊断子集。", + "summary": "总体指标和动作延迟统一比较全部模型×浏览器配置;失败清单改为固定高度的横向诊断表并提供页内诊断弹窗;每个失败详情补齐失败位置、程序报错、复核根因、处理责任、轨迹、画面与可直接查看的执行证据。官方分数不变,另提供排除已证实外部干扰的诊断子集。", "attempts_sha256_by_model": { "gpt-5.6-sol": "73146500152db62d79f84b63c85a14027c005a6320c19bce7426651f167466bd", "gpt-6-astra": "0659d227cf6d29fbfaf64d39f8e77a56bec73c87f7f6e1f2ac23ea13fd9b667c" }, "selection_sha256": "2cb716552e36df7535e83ed6d2cf3f84283e3aca86072817cd6e039a38547c7b", "changes": [ + [ + "总体与速度比较", + "旧版按模型分组标记浏览器优胜者,无法直接比较四种完整配置。", + "通过率、Agent 耗时、BrowserTool 耗时、Tokens 和逐动作单次延迟均改为四种配置同台比较,只标记唯一最优值。", + "总耗时与动作调用数仍只解释工作量,不参与浏览器快慢判定。" + ], [ "同题比较", "旧页面要求先选基准浏览器再做两两比较,无法同时观察全部配置。", @@ -167,8 +173,8 @@ [ "失败定位", "结果清单混入长原因和耗时字段,行高失控,且无法直接进入失败证据。", - "改为固定高度横向表,只保留失败位置、自动定位、复核根因、处理责任和证据入口。", - "完整 evaluator、轨迹、日志和原因留在结果详情,不在清单重复。" + "改为固定高度横向表;每行可打开页内诊断,直接查看失败位置、程序报错、复核根因、处理责任和证据入口。", + "完整 evaluator、轨迹、日志和原因留在结果详情;原始 JSON 同时提供页内阅读和独立打开入口。" ], [ "证据页面", @@ -192,8 +198,14 @@ "i18n": { "en": { "title": "Turn Results into a failure-diagnosis workbench", - "summary": "Same-task comparison now uses a matrix across every model and browser configuration. The failure inventory is a fixed-height horizontal diagnosis table, and each failed result exposes its failure stage, automatic classification, reviewed root cause, remediation owner, logs, and final page frames. Official scores are unchanged; a separate diagnostic subset removes confirmed external interference.", + "summary": "Overall metrics and action latency now compare every model-by-browser configuration together. The failure inventory is a fixed-height horizontal table with an in-page diagnosis dialog, and each failed result exposes its failure stage, exact program error, reviewed root cause, remediation owner, trajectory, screenshots, and directly readable execution evidence. Official scores are unchanged; a separate diagnostic subset removes confirmed external interference.", "changes": [ + [ + "Overall and speed comparison", + "The previous version marked browser winners within each model, preventing direct comparison of all four complete configurations.", + "Pass rate, Agent latency, BrowserTool latency, tokens, and per-action per-call latency now compare all four configurations together and mark only a unique winner.", + "Total action latency and call counts remain workload context and are not used to rank browser speed." + ], [ "Same-task comparison", "The previous page required a baseline browser and pairwise comparisons, preventing a simultaneous view of every configuration.", @@ -203,8 +215,8 @@ [ "Failure location", "The result inventory mixed long causes and latency fields, producing uncontrolled row heights and no direct route to failure evidence.", - "Replaced it with a fixed-height horizontal table containing failure stage, automatic locator, reviewed cause, owner/treatment, and evidence links.", - "Complete evaluator output, trajectory, logs, and cause remain on the result detail page without duplication." + "Replaced it with a fixed-height horizontal table whose rows open an in-page diagnosis with the failure stage, exact program error, reviewed cause, owner/treatment, and evidence links.", + "Complete evaluator output, trajectory, logs, and cause remain on the result detail page; raw JSON is available both inline and as a separate link." ], [ "Evidence pages", diff --git a/scripts/check_browser_use_report.py b/scripts/check_browser_use_report.py index 584b11b33..367154a48 100644 --- a/scripts/check_browser_use_report.py +++ b/scripts/check_browser_use_report.py @@ -473,15 +473,27 @@ def check( "P50", "P95", "comparison-winner", + 'id="analysis-core-metrics"', "BrowserTool 动作延迟", "action-card-grid", + "四种配置 · 单次调用墙钟时间", "同题全配置对比", 'id="configuration-matrix"', 'id="clean-subset-table"', + "查看诊断", + 'class="diagnosis-dialog"', + "程序报错", ): if marker not in analysis_index: raise ValueError(f"analysis is missing timing or scalable comparison: {marker}") - for forbidden in ("中位数", "失败结果定位", 'id="failure-table"', 'id="pair-table"'): + for forbidden in ( + "中位数", + "失败结果定位", + "是否属于目标变量", + 'id="failure-table"', + 'id="pair-table"', + 'class="finding-list"', + ): if forbidden in analysis_index: raise ValueError(f"analysis contains a redundant or retired presentation: {forbidden}") participant_index = rendered_pages[report_dir / "adoption/index.html"] @@ -498,8 +510,8 @@ def check( "查看官方期望与本次观察", "CowAgent 自主执行轨迹", "运行、日志与证据", - "attempt-summary", - "查看任务与验收", + "runtime-strip", + "任务与验收", ) ): raise ValueError("every result page must expose result, evaluator, trajectory and logs") @@ -513,12 +525,14 @@ def check( for page in failed_pages for marker in ( "失败诊断", - "失败位置", - "程序自动定位", - "人工复核根因", - "负责对象与处理", + 'class="failure-lead"', + "程序报错", + "程序判定", + "复核根因", + "负责范围", "失败执行的最后页面画面", - "下载执行日志", + "在页面中查看完整执行证据", + "打开原始 JSON", ) ): raise ValueError("every failed result must expose a complete diagnosis path") diff --git a/tests/browser_use/test_report.py b/tests/browser_use/test_report.py index f47f81964..73641a572 100644 --- a/tests/browser_use/test_report.py +++ b/tests/browser_use/test_report.py @@ -6,6 +6,7 @@ import harness.browser_use.report as report_module from harness.browser_use.report import ( _action_duration_cards, + _attempt_table, _best, _failure_diagnosis, _load_evidence_bundles, @@ -38,6 +39,87 @@ def test_best_marks_only_one_visibly_distinct_winner() -> None: ) +def test_failed_result_inventory_opens_exact_diagnosis_and_links_trajectory( + tmp_path: Path, +) -> None: + evaluation = tmp_path / "evaluation.json" + events = tmp_path / "events.json" + evaluation.write_text( + json.dumps( + { + "score": 0, + "evaluators_results": [ + { + "evaluator_name": "AgentResponseEvaluator", + "score": 0, + "assertions": [ + {"assertion_msgs": ["Array is missing 1 expected element(s)."]} + ], + } + ], + } + ), + encoding="utf-8", + ) + events.write_text( + json.dumps( + [ + { + "type": "tool_execution_start", + "data": { + "tool_call_id": "1", + "tool_name": "browser", + "arguments": {"action": "click", "ref": "button-1"}, + }, + }, + { + "type": "tool_execution_end", + "data": { + "tool_call_id": "1", + "status": "failure", + "error": "element detached", + }, + }, + ] + ), + encoding="utf-8", + ) + attempt = { + "model": "gpt-5.6-sol", + "engine": "chromium", + "task_id": 44, + "tier": "easy", + "attempt": 1, + "score": 0, + "paths": {"evaluation": str(evaluation), "events": str(events)}, + } + tasks = [ + { + "task_id": 44, + "tier": "easy", + "sites": ["shopping_admin"], + "task_type": "RETRIEVE", + "intent": "Inspect a report", + } + ] + + html = _attempt_table([attempt], tasks) + + for expected in ( + "查看诊断", + ' None: source = ( '' @@ -361,8 +443,11 @@ def test_report_publishes_one_final_logical_result_and_aggregates_cost( "browser-comparison", 'id="configuration-matrix"', 'id="clean-subset-table"', + 'id="analysis-core-metrics"', + "四种配置 · 单次调用墙钟时间", ): assert expected in analysis + assert analysis.index("同题全配置对比") < analysis.index("BrowserTool 动作延迟") assert "中位数" not in analysis assert "失败结果定位" not in analysis assert 'id="pair-table"' not in analysis @@ -377,8 +462,10 @@ def test_report_publishes_one_final_logical_result_and_aggregates_cost( "CowAgent 自主执行轨迹", "运行、日志与证据", f'href="results/{result_id}.html#trajectory"', - "attempt-summary", - "查看任务与验收", + "runtime-strip", + "任务与验收", + "在页面中查看完整执行证据", + "打开原始 JSON", ): assert expected in attempt assert "没有 evaluator 明细" in attempt diff --git a/tests/browser_use/test_report_ui.mjs b/tests/browser_use/test_report_ui.mjs index 79759196b..99bbc5219 100644 --- a/tests/browser_use/test_report_ui.mjs +++ b/tests/browser_use/test_report_ui.mjs @@ -38,6 +38,12 @@ const matrixRoot = { }, querySelector(selector) { return selector === '[data-matrix-count]' ? matrixCount : null; }, }; +const diagnosisDialog = { + open: false, + showModal() { this.open = true; }, + close() { this.open = false; }, + addEventListener() {}, +}; const listeners = {}; const storage = new Map(); let replaced = ''; @@ -57,10 +63,15 @@ const document = { if (selector === 'tbody tr[data-tier]:not([data-relation])') return rows; if (selector === '[data-visible-count]') return [count]; if (selector === '[data-matrix-filter]') return matrixControls; + if (selector === 'dialog') return [diagnosisDialog]; return []; }, querySelector(selector) { return selector === '[data-search]' ? search : null; }, - getElementById(id) { return id === 'configuration-matrix' ? matrixRoot : null; }, + getElementById(id) { + if (id === 'configuration-matrix') return matrixRoot; + if (id === 'diagnosis-example') return diagnosisDialog; + return null; + }, }; const context = vm.createContext({ URL, @@ -92,6 +103,11 @@ matrixControls[0].value = 'all_fail'; vm.runInContext('filterMatrix()', context); assert.deepEqual(comparisonRows.map(row => row.hidden), [true, true, false]); +vm.runInContext("openDiagnosis('diagnosis-example')", context); +assert.equal(diagnosisDialog.open, true); +vm.runInContext("closeDiagnosis('diagnosis-example')", context); +assert.equal(diagnosisDialog.open, false); + controls[0].value = 'pass'; search.value = ''; vm.runInContext('filterRows()', context); From ed9bb9c4d509be5ebc706b285669abd0ebcec7fa Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 11 Sep 2026 20:58:48 +0800 Subject: [PATCH 071/127] Reuse bound browser use visual QA receipts --- scripts/check_browser_use_visual.py | 12 +++++++++++ scripts/package_browser_use_artifact_hub.py | 12 ++++++++++- tests/browser_use/test_publish.py | 22 ++++++++++++++++++--- 3 files changed, 42 insertions(+), 4 deletions(-) diff --git a/scripts/check_browser_use_visual.py b/scripts/check_browser_use_visual.py index a2b72d6d8..c60ef128e 100644 --- a/scripts/check_browser_use_visual.py +++ b/scripts/check_browser_use_visual.py @@ -4,6 +4,7 @@ from __future__ import annotations import argparse +import hashlib import json import os import shutil @@ -12,6 +13,16 @@ from playwright.sync_api import sync_playwright +def report_tree_sha256(report: Path) -> str: + digest = hashlib.sha256() + for path in sorted(item for item in report.rglob("*") if item.is_file()): + relative = path.relative_to(report).as_posix().encode() + digest.update(len(relative).to_bytes(8, "big")) + digest.update(relative) + digest.update(hashlib.sha256(path.read_bytes()).digest()) + return digest.hexdigest() + + def _browser_executable() -> str: configured = os.environ.get("BROWSER_USE_VISUAL_BROWSER") or os.environ.get( "CHROMIUM_BINARY" @@ -128,6 +139,7 @@ def check(report: Path, receipt: Path) -> None: json.dumps( { "schema": "lex_browser_use_visual_qa/1", + "report_tree_sha256": report_tree_sha256(report), "browser_executable": executable, "route_classes": len(routes), "states_checked": len(states), diff --git a/scripts/package_browser_use_artifact_hub.py b/scripts/package_browser_use_artifact_hub.py index 9d95e9690..adaa87633 100644 --- a/scripts/package_browser_use_artifact_hub.py +++ b/scripts/package_browser_use_artifact_hub.py @@ -51,7 +51,17 @@ def validate_release(report: Path, evidence: list[Path], visual_receipt: Path) - require_complete=True, ) check_links(report) - check_visual(report, visual_receipt) + try: + visual = json.loads(visual_receipt.read_text(encoding="utf-8")) + except (OSError, ValueError): + visual = {} + if ( + visual.get("schema") != "lex_browser_use_visual_qa/1" + or visual.get("report_tree_sha256") != tree_sha256(report) + or visual.get("failures") + or int(visual.get("states_checked") or 0) <= 0 + ): + check_visual(report, visual_receipt) def main() -> None: diff --git a/tests/browser_use/test_publish.py b/tests/browser_use/test_publish.py index 333531af7..533a6626d 100644 --- a/tests/browser_use/test_publish.py +++ b/tests/browser_use/test_publish.py @@ -64,10 +64,23 @@ def test_validate_package_requires_and_checks_every_evidence_root( raise AssertionError("changed secondary evidence must invalidate the package") -def test_packaging_runs_complete_release_gate(monkeypatch, tmp_path: Path) -> None: +def test_packaging_reuses_visual_gate_bound_to_current_report(monkeypatch, tmp_path: Path) -> None: report = tmp_path / "report" + report.mkdir() + (report / "index.html").write_text("report", encoding="utf-8") evidence = [tmp_path / "evidence-a", tmp_path / "evidence-b"] receipt = tmp_path / "visual.json" + receipt.write_text( + json.dumps( + { + "schema": "lex_browser_use_visual_qa/1", + "report_tree_sha256": packager.tree_sha256(report), + "states_checked": 112, + "failures": [], + } + ), + encoding="utf-8", + ) calls = [] monkeypatch.setattr( @@ -86,6 +99,9 @@ def test_packaging_runs_complete_release_gate(monkeypatch, tmp_path: Path) -> No packager.validate_release(report, evidence, receipt) - assert [call[0] for call in calls] == ["report", "links", "visual"] + assert [call[0] for call in calls] == ["report", "links"] assert calls[0][-1] is True - assert calls[2] == ("visual", report, receipt) + + (report / "index.html").write_text("changed", encoding="utf-8") + packager.validate_release(report, evidence, receipt) + assert calls[-1] == ("visual", report, receipt) From 46806d0ba32dba4dd3e55039ff5f3fe60041753b Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 11 Sep 2026 21:08:25 +0800 Subject: [PATCH 072/127] Add hosted execution evidence viewer --- AGENTS.md | 2 +- harness/browser_use/report.py | 46 +++++++++++++++++++++++------ harness/browser_use/report_i18n.py | 6 ++++ research/report-changes.json | 4 +-- scripts/check_browser_use_report.py | 16 ++++++++-- tests/browser_use/test_report.py | 7 +++++ 6 files changed, 67 insertions(+), 14 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index e7d416bab..5857bb61c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -22,7 +22,7 @@ - Result-detail summaries contain only the verdict, concise failed-rule labels and short scalar facts. Full evaluator messages, expected/actual payloads, traces and logs belong in dedicated evidence sections, never narrow KPI cards. - Result inventories are fixed-height, horizontally scrollable diagnosis tables. Do not place full reviewed causes, evaluator payloads, latency stacks or other paragraph-length content in inventory cells. Every failure row opens an in-page diagnosis with the failure stage, exact program error, reviewed cause, treatment and links to the relevant trajectory, screenshots and logs; the full result page remains the canonical evidence view. - Compare every registered model-by-browser configuration in one same-task matrix generated from the participant registry. Do not introduce pairwise selectors or duplicate comparison prose when more configurations can be represented as columns. -- Every failed result detail must expose the evidenced failure stage, the programmatic evaluator classification, the exact evaluator/runtime error, the reviewed root cause, the remediation owner/status, a link to the relevant trajectory step, sanitized logs, and the last public trace frames when available. Distinguish a failed BrowserTool action from the last successful action before evaluator rejection. Provide an inline HTML evidence viewer as well as a raw JSON link because static hosts may not preview JSON reliably. Extract frames only from ordered Playwright `screencast-frame` events, validate full-page dimensions and provenance, and never treat arbitrary trace resources as screenshots. Label frames as last observed pages, not as exact error screenshots unless the evidence proves that timing. +- Every failed result detail must expose the evidenced failure stage, the programmatic evaluator classification, the exact evaluator/runtime error, the reviewed root cause, the remediation owner/status, a link to the relevant trajectory step, sanitized logs, and the last public trace frames when available. Distinguish a failed BrowserTool action from the last successful action before evaluator rejection. Provide a shared HTML evidence viewer as well as a raw JSON link because static hosts may not preview JSON reliably; load the selected JSON on demand and never duplicate complete evidence payloads into every result page. Extract frames only from ordered Playwright `screencast-frame` events, validate full-page dimensions and provenance, and never treat arbitrary trace resources as screenshots. Label frames as last observed pages, not as exact error screenshots unless the evidence proves that timing. - Keep official scores immutable when diagnosis finds benchmark/evaluator, site, shared-environment, or frozen-reference interference. Quarantine affected tasks across all configurations in a clearly labeled post-hoc diagnostic subset, record remediation in `research/browser-use-remediation.json`, and readmit them only after a preregistered full-scope rerun. - Split environment preparation, browser-session setup, Agent run, BrowserTool wall time, evidence finalization and evaluator time at collection. Publish the self-hosted deployment topology and image identity; claim per-execution reset/readiness only when a preparation receipt binds reset, readiness and application health. - Formal report prose is reader-facing: no prompts, reasoning, internal work narration, conversational rebuttals or references to parallel sessions. Review pages contain review rounds and corrections, not per-case audit tables; release notes contain reader-visible effects. diff --git a/harness/browser_use/report.py b/harness/browser_use/report.py index 5354b88ed..efe13f9e1 100644 --- a/harness/browser_use/report.py +++ b/harness/browser_use/report.py @@ -1199,7 +1199,7 @@ def _summary_comparison_rows(summary: dict[str, dict[str, dict[str, Any]]]) -> l .finding-list{display:grid;gap:8px;margin:0 0 20px;padding:16px 20px 16px 38px;border-left:4px solid var(--accent);border-radius:0 12px 12px 0;background:var(--wash)}.finding-list li{padding-left:4px}.table-kicker{display:block;color:var(--muted);font:700 11px/1.4 ui-monospace,monospace;letter-spacing:.04em;margin-top:3px}.core-comparison-table{min-width:1240px;table-layout:fixed}.core-comparison-table td:nth-child(1),.core-comparison-table td:nth-child(2){font-weight:750}.core-comparison-table th:nth-child(1),.core-comparison-table td:nth-child(1){width:145px}.core-comparison-table th:nth-child(2),.core-comparison-table td:nth-child(2){width:135px}.core-comparison-table th:nth-child(3),.core-comparison-table td:nth-child(3){width:115px}.core-comparison-table th:nth-child(4),.core-comparison-table td:nth-child(4){width:105px}.core-comparison-table th:nth-child(n+5),.core-comparison-table td:nth-child(n+5){width:245px}.pair-outcome-table{min-width:900px}.wide-data-table td{line-height:1.65}.report-entry-links{margin-top:18px}.report-entry-links a{font-weight:750}.source-note a{margin-left:5px} #task-family-table{min-width:1320px;table-layout:fixed}#task-family-table th:nth-child(1),#task-family-table td:nth-child(1){width:150px}#task-family-table th:nth-child(2),#task-family-table td:nth-child(2){width:90px}#task-family-table th:nth-child(3),#task-family-table td:nth-child(3){width:300px}#task-family-table th:nth-child(n+4),#task-family-table td:nth-child(n+4){width:190px;white-space:nowrap}.metric-note{margin:14px 0 0;border-top:1px solid var(--line);padding-top:12px;color:var(--muted)}.metric-note summary{cursor:pointer;font-weight:750;color:var(--ink)} .diagnosis-button,.dialog-actions button{border:1px solid var(--accent);background:var(--accent);color:white;padding:8px 12px;border-radius:9px;font:inherit;font-weight:750;cursor:pointer;white-space:nowrap}.diagnosis-button:hover,.dialog-actions button:hover{filter:brightness(.94)}.diagnosis-dialog{width:min(920px,calc(100% - 32px));max-height:88vh;padding:0;border:1px solid var(--line);border-radius:18px;background:var(--panel);color:var(--ink);box-shadow:0 28px 90px rgba(10,25,45,.28)}.diagnosis-dialog::backdrop{background:rgba(5,15,25,.62);backdrop-filter:blur(3px)}.diagnosis-dialog-head{position:sticky;top:0;z-index:1;display:flex;align-items:start;justify-content:space-between;gap:20px;padding:22px 24px 16px;border-bottom:1px solid var(--line);background:var(--panel)}.diagnosis-dialog-head h2{font-size:24px;margin:4px 0}.dialog-close{border:0;background:var(--panel2);color:var(--ink);width:36px;height:36px;border-radius:50%;font-size:24px;line-height:1;cursor:pointer}.diagnosis-dialog-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:10px;padding:18px 24px}.diagnosis-dialog-grid>div{padding:13px;border:1px solid var(--line);border-radius:10px;background:var(--bg);min-width:0;overflow-wrap:anywhere}.diagnosis-dialog-grid b{display:block;font-size:12px;color:var(--muted);margin-bottom:5px}.dialog-error,.dialog-review{padding:0 24px 18px}.dialog-error h3,.dialog-review h3{margin:8px 0}.dialog-error .log-view{max-height:240px}.dialog-review p{margin:6px 0}.dialog-actions{position:sticky;bottom:0;display:flex;align-items:center;gap:10px;flex-wrap:wrap;padding:16px 24px;border-top:1px solid var(--line);background:var(--panel)}.dialog-actions a{padding:8px 10px;border:1px solid var(--line);border-radius:9px}.dialog-actions button{margin-left:auto} -.failure-lead{padding:20px 22px;margin-bottom:14px;border:1px solid color-mix(in srgb,var(--bad) 35%,var(--line));border-left:6px solid var(--bad);border-radius:14px;background:var(--failure-bg)}.failure-lead .status-line{color:var(--bad);font-size:13px;font-weight:850}.failure-lead h3{font-size:24px;margin:5px 0}.failure-lead p{margin:4px 0}.success-lead{border-color:color-mix(in srgb,var(--good) 35%,var(--line));border-left-color:var(--good);background:var(--success-bg)}.success-lead .status-line{color:var(--good)}.runtime-strip{grid-template-columns:repeat(6,minmax(0,1fr));margin:14px 0}.failure-error{margin-top:16px}.failure-error .log-view{max-height:260px}.trajectory-step.failure-step{margin:8px 0;padding:14px;border:1px solid color-mix(in srgb,var(--bad) 40%,var(--line));border-left:5px solid var(--bad);border-radius:12px;background:var(--failure-bg)}.trajectory-step.failure-step:before{background:var(--bad)}.inline-evidence{margin:14px 0}.inline-evidence>summary{cursor:pointer;font-weight:750} +.failure-lead{padding:20px 22px;margin-bottom:14px;border:1px solid color-mix(in srgb,var(--bad) 35%,var(--line));border-left:6px solid var(--bad);border-radius:14px;background:var(--failure-bg)}.failure-lead .status-line{color:var(--bad);font-size:13px;font-weight:850}.failure-lead h3{font-size:24px;margin:5px 0}.failure-lead p{margin:4px 0}.success-lead{border-color:color-mix(in srgb,var(--good) 35%,var(--line));border-left-color:var(--good);background:var(--success-bg)}.success-lead .status-line{color:var(--good)}.runtime-strip{grid-template-columns:repeat(6,minmax(0,1fr));margin:14px 0}.failure-error{margin-top:16px}.failure-error .log-view{max-height:260px}.trajectory-step.failure-step{margin:8px 0;padding:14px;border:1px solid color-mix(in srgb,var(--bad) 40%,var(--line));border-left:5px solid var(--bad);border-radius:12px;background:var(--failure-bg)}.trajectory-step.failure-step:before{background:var(--bad)}.evidence-viewer-head{display:flex;align-items:center;justify-content:space-between;gap:16px}.evidence-viewer-head h2{margin:0}.evidence-viewer-shell .json-view{max-height:none;min-height:60vh} .action-card-grid{grid-auto-rows:1fr}.action-card{height:100%}.dialog-step,.step-signature{padding:13px 16px;border-radius:10px;background:var(--wash);overflow-wrap:anywhere}.dialog-step{margin:0 24px 18px}.dialog-step b{margin-right:8px}.step-signature{margin:0 0 14px} .attempt-summary{display:grid;grid-template-columns:minmax(270px,1.4fr) repeat(4,minmax(135px,.7fr));gap:12px;align-items:stretch}.attempt-verdict{padding:20px 22px;border:1px solid var(--line);border-left:5px solid var(--good);border-radius:12px;background:var(--success-bg)}.attempt-verdict.failure{border-left-color:var(--bad);background:var(--failure-bg)}.attempt-verdict .status-line{display:block;font-size:24px;font-weight:850;margin-bottom:6px}.attempt-verdict p{margin:0;line-height:1.65}.attempt-jumps{display:flex;gap:14px;flex-wrap:wrap;margin-top:14px;font-weight:750}.attempt-fact{padding:16px;border:1px solid var(--line);border-radius:12px;background:var(--panel);overflow-wrap:anywhere}.attempt-fact b{display:block;color:var(--muted);font-size:12px;margin-bottom:5px}.attempt-secondary{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin-top:12px} .diagnosis-grid{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px}.diagnosis-grid .attempt-fact{min-height:110px}.diagnosis-reason{margin-top:14px;padding:18px 20px;border-left:5px solid var(--bad);border-radius:10px;background:var(--failure-bg);line-height:1.7}.evidence-links{display:flex;gap:8px}.screenshot-grid{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.screenshot-grid figure{margin:0;border:1px solid var(--line);border-radius:12px;overflow:hidden;background:var(--panel)}.screenshot-grid img{display:block;width:100%;aspect-ratio:16/10;object-fit:cover;object-position:top}.screenshot-grid figcaption{padding:10px 12px;color:var(--muted);font-size:12px} @@ -1238,6 +1238,36 @@ def _page(*, title: str, active: str, body: str, depth: int = 0) -> str: return f'{_e(title)} · Browser Use 评测
    {body}

    结论只适用于本轮预注册样本、Agent、模型与运行配置;展示调整不改变测量结果。

    ' +def _evidence_viewer_page() -> str: + body = _hero( + "Execution evidence", + "完整执行证据", + "按原始结构展示本次物理执行的脱敏回答、evaluator、事件流、轨迹、计量与公开画面索引。", + ) + body += ( + '
    ' + '

    执行证据 JSON

    ' + '
    ' + '

    正在读取证据…

    ' + '
    '
    +        '
    ' + ) + return _page(title="完整执行证据", active="analysis", body=body) + + def _english_page(body: str) -> str: links = "".join( f'{label}' @@ -1653,8 +1683,10 @@ def _extract_trace_screenshots( def _artifact_links(result_id: str, execution: int) -> str: root = f"artifacts/{result_id}/executions/{execution}" + viewer = f"evidence-viewer.html?src={root}/execution-evidence.json" return ( '" ) @@ -1706,7 +1738,6 @@ def _write_execution_artifacts( **measurements, "artifacts": artifacts, "screenshots": screenshots, - "_inline_evidence": bundle, }, response, evaluation, steps @@ -1752,9 +1783,6 @@ def _execution_evidence_html(result_id: str, executions: list[dict[str, Any]]) - f"BrowserTool {execution['action_count']} 次 / {_seconds(execution['browser_tool_duration_ms'])} · " f"模型 tokens {_number(usage['total_tokens'])}

    " + _artifact_links(result_id, int(execution["execution"])) - + '
    在页面中查看完整执行证据' - + _json_pre(execution.get("_inline_evidence") or {}) - + "
    " + hashes + "" ) @@ -2312,6 +2340,9 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat (stage / folder).mkdir() (stage / "assets/style.css").write_text(REPORT_STYLE, encoding="utf-8") (stage / "assets/report.js").write_text(REPORT_JS, encoding="utf-8") + (stage / "evidence-viewer.html").write_text( + _evidence_viewer_page(), encoding="utf-8" + ) if not METHODOLOGY_FIGURE.is_file(): raise FileNotFoundError(f"missing methodology figure: {METHODOLOGY_FIGURE}") shutil.copy2( @@ -2723,10 +2754,7 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat "authoritative_execution": int(row["attempt"]), "task_page": f"datasets/{public['task_id']}.html", "result_page": f"results/{public['id']}.html", - "executions": [ - {key: value for key, value in execution.items() if key != "_inline_evidence"} - for execution in execution_records - ], + "executions": execution_records, } ) usage, resource, checks = ( diff --git a/harness/browser_use/report_i18n.py b/harness/browser_use/report_i18n.py index 4d61a0526..8f07d5032 100644 --- a/harness/browser_use/report_i18n.py +++ b/harness/browser_use/report_i18n.py @@ -28,6 +28,12 @@ TEXT = { + "完整执行证据": "Complete execution evidence", + "按原始结构展示本次物理执行的脱敏回答、evaluator、事件流、轨迹、计量与公开画面索引。": "Shows the sanitized response, evaluator record, event stream, trajectory, measurements, and public-frame index for this physical execution in their original structure.", + "执行证据 JSON": "Execution evidence JSON", + "正在读取证据…": "Loading evidence…", + "在页面中查看完整执行证据": "View complete execution evidence in the page", + "打开原始 JSON": "Open raw JSON", "25 比 25 不是同因平局:五个零分包含四类根因": "25 vs. 25 is not a same-cause tie: five zero scores contain four root-cause classes", "同一 CowAgent 与同一模型替换浏览器后,30 个配对任务的官方分数没有出现单边胜出,但逐条追查证明相同分数并不代表相同执行结果。根因分别位于官方任务合同、Agent 完成策略、本项目响应协议和证据采集器;Task 506 的 Chromium 与 Moli 甚至不是同一种失败。": "With the same CowAgent and model and only the browser changed, none of the 30 paired tasks produced a one-sided official-score win. Case-by-case investigation shows that equal scores do not imply equal execution outcomes. Root causes lie in the official task contract, Agent completion policy, this project's response protocol, and the evidence collector; Chromium and Moli did not even fail Task 506 in the same way.", "二、五个零分的根因与修复边界": "2. Root causes and repair boundaries for the five zero scores", diff --git a/research/report-changes.json b/research/report-changes.json index 4f9abb9fd..2206ad197 100644 --- a/research/report-changes.json +++ b/research/report-changes.json @@ -174,7 +174,7 @@ "失败定位", "结果清单混入长原因和耗时字段,行高失控,且无法直接进入失败证据。", "改为固定高度横向表;每行可打开页内诊断,直接查看失败位置、程序报错、复核根因、处理责任和证据入口。", - "完整 evaluator、轨迹、日志和原因留在结果详情;原始 JSON 同时提供页内阅读和独立打开入口。" + "完整 evaluator、轨迹、日志和原因留在结果详情;共享证据页按需读取 JSON,同时保留原始文件入口,避免在每个详情页重复完整载荷。" ], [ "证据页面", @@ -216,7 +216,7 @@ "Failure location", "The result inventory mixed long causes and latency fields, producing uncontrolled row heights and no direct route to failure evidence.", "Replaced it with a fixed-height horizontal table whose rows open an in-page diagnosis with the failure stage, exact program error, reviewed cause, owner/treatment, and evidence links.", - "Complete evaluator output, trajectory, logs, and cause remain on the result detail page; raw JSON is available both inline and as a separate link." + "Complete evaluator output, trajectory, logs, and cause remain on the result detail page. A shared viewer loads JSON on demand while preserving a raw-file link, avoiding duplicate payloads in every detail page." ], [ "Evidence pages", diff --git a/scripts/check_browser_use_report.py b/scripts/check_browser_use_report.py index 367154a48..f4a7b31df 100644 --- a/scripts/check_browser_use_report.py +++ b/scripts/check_browser_use_report.py @@ -314,6 +314,7 @@ def check( "adoption/index.html", "reproduce/index.html", "accounts/index.html", + "evidence-viewer.html", *(f"engines/{engine}.html" for engine in engines), ) pages = tuple(report_dir / name for name in page_names) @@ -512,9 +513,22 @@ def check( "运行、日志与证据", "runtime-strip", "任务与验收", + "在页面中查看完整执行证据", + "打开原始 JSON", ) ): raise ValueError("every result page must expose result, evaluator, trajectory and logs") + evidence_viewer = report_dir / "evidence-viewer.html" + english_evidence_viewer = report_dir / "en/evidence-viewer.html" + if any(path not in rendered_pages for path in (evidence_viewer, english_evidence_viewer)): + raise ValueError("report is missing the shared HTML execution-evidence viewer") + for marker in ( + 'id="evidence-viewer-content"', + "execution-evidence[.]json", + "fetch(new URL(source,document.baseURI)", + ): + if marker not in rendered_pages[evidence_viewer]: + raise ValueError(f"execution-evidence viewer is incomplete: {marker}") failed_pages = [ report_dir / "results" / f"{_attempt_id(row)}.html" for row in logical_rows @@ -531,8 +545,6 @@ def check( "复核根因", "负责范围", "失败执行的最后页面画面", - "在页面中查看完整执行证据", - "打开原始 JSON", ) ): raise ValueError("every failed result must expose a complete diagnosis path") diff --git a/tests/browser_use/test_report.py b/tests/browser_use/test_report.py index 73641a572..9631ca12c 100644 --- a/tests/browser_use/test_report.py +++ b/tests/browser_use/test_report.py @@ -480,6 +480,12 @@ def test_report_publishes_one_final_logical_result_and_aggregates_cost( assert "物理执行 1" in attempt assert "物理执行 2 · 判分权威" in attempt assert "SHA-256" in attempt + assert "_inline_evidence" not in attempt + viewer = (output / "evidence-viewer.html").read_text(encoding="utf-8") + assert 'id="evidence-viewer-content"' in viewer + assert "execution-evidence[.]json" in viewer + assert "fetch(new URL(source,document.baseURI)" in viewer + assert (output / "en/evidence-viewer.html").is_file() public_results = [ json.loads(line) for line in (output / "data/results.jsonl").read_text(encoding="utf-8").splitlines() @@ -510,6 +516,7 @@ def test_report_publishes_one_final_logical_result_and_aggregates_cost( for execution in index_row["executions"] for artifact in execution["artifacts"].values() ) + assert all("_inline_evidence" not in execution for execution in index_row["executions"]) private_trace = output / f"artifacts/{result_id}/executions/1/agent_trace.zip" private_trace.write_bytes(b"private trace") with pytest.raises(ValueError, match="private HAR or trace"): From 2006673d04fdf23cc1b5e6ac091e06aacf9adbb8 Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 11 Sep 2026 21:10:11 +0800 Subject: [PATCH 073/127] Bind published browser use report version --- evaluation/browser-use/current-report.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evaluation/browser-use/current-report.json b/evaluation/browser-use/current-report.json index 6e43fe894..96c995865 100644 --- a/evaluation/browser-use/current-report.json +++ b/evaluation/browser-use/current-report.json @@ -57,7 +57,7 @@ "base_url": "https://artifact-hub.app.lexmount.com", "site_slug": "7l77EeXPRfml", "share_url": "https://artifact-hub.app.lexmount.com/v/Ghn1BR6PCy-njsCMqCe2LRIJ-eyJEJoa", - "expected_version": "ver_fedeedfcc9c84074af49b390e7ed8c36", + "expected_version": "ver_7ffdfbcb7da94e0b92188824a3558e83", "state": "verified" } } From cd5117fe8c8ab20a1ef650d7f8bf33ddd0eca1bc Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 11 Sep 2026 21:14:40 +0800 Subject: [PATCH 074/127] Fix browser trace frame accounting --- harness/browser_use/report.py | 19 +++++++++++++------ tests/browser_use/test_report.py | 29 +++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 6 deletions(-) diff --git a/harness/browser_use/report.py b/harness/browser_use/report.py index efe13f9e1..326bf2bd6 100644 --- a/harness/browser_use/report.py +++ b/harness/browser_use/report.py @@ -1635,6 +1635,7 @@ def _extract_trace_screenshots( return [] try: with zipfile.ZipFile(trace_path) as archive: + archive_names = set(archive.namelist()) frames = [] for raw_line in archive.read("trace.trace").splitlines(): try: @@ -1644,7 +1645,7 @@ def _extract_trace_screenshots( if event.get("type") != "screencast-frame" or not event.get("sha1"): continue name = "resources/" + str(event["sha1"]) - if name not in archive.namelist(): + if name not in archive_names: continue frames.append((float(event.get("timestamp") or 0), name)) names = [] @@ -1654,23 +1655,29 @@ def _extract_trace_screenshots( if len(names) == limit: break names.reverse() - output = [] - screenshot_dir = artifact_dir / "screenshots" - screenshot_dir.mkdir(parents=True, exist_ok=True) - for index, name in enumerate(names, start=1): + valid_frames = [] + for name in names: payload = archive.read(name) with Image.open(BytesIO(payload)) as image: width, height = image.size if width < 800 or height < 450: continue suffix = ".png" if name.lower().endswith(".png") else ".jpeg" + valid_frames.append((payload, width, height, suffix)) + output = [] + screenshot_dir = artifact_dir / "screenshots" + screenshot_dir.mkdir(parents=True, exist_ok=True) + total = len(valid_frames) + for index, (payload, width, height, suffix) in enumerate( + valid_frames, start=1 + ): destination = screenshot_dir / f"frame-{index}{suffix}" destination.write_bytes(payload) output.append( { "path": f"screenshots/{destination.name}", "sha256": _sha256(destination), - "label": f"最后页面画面 {len(output) + 1}/{len(names)}", + "label": f"最后页面画面 {index}/{total}", "source": "playwright-screencast-frame", "width": width, "height": height, diff --git a/tests/browser_use/test_report.py b/tests/browser_use/test_report.py index 9631ca12c..cf30d4f73 100644 --- a/tests/browser_use/test_report.py +++ b/tests/browser_use/test_report.py @@ -1,4 +1,5 @@ import json +import zipfile from pathlib import Path, PurePosixPath import pytest @@ -39,6 +40,34 @@ def test_best_marks_only_one_visibly_distinct_winner() -> None: ) +def test_trace_screenshot_labels_count_only_publishable_frames(tmp_path: Path) -> None: + trace = tmp_path / "trace.zip" + image_payloads = {} + for sha1, size in (("large-a", (1280, 720)), ("small", (320, 200)), ("large-b", (1280, 720))): + buffer = report_module.BytesIO() + report_module.Image.new("RGB", size, "white").save(buffer, format="JPEG") + image_payloads[sha1] = buffer.getvalue() + events = b"\n".join( + json.dumps({"type": "screencast-frame", "sha1": sha1, "timestamp": index}).encode() + for index, sha1 in enumerate(image_payloads, start=1) + ) + with zipfile.ZipFile(trace, "w") as archive: + archive.writestr("trace.trace", events) + for sha1, payload in image_payloads.items(): + archive.writestr(f"resources/{sha1}", payload) + + screenshots = report_module._extract_trace_screenshots(trace, tmp_path / "public") + + assert [item["label"] for item in screenshots] == [ + "最后页面画面 1/2", + "最后页面画面 2/2", + ] + assert [item["path"] for item in screenshots] == [ + "screenshots/frame-1.jpeg", + "screenshots/frame-2.jpeg", + ] + + def test_failed_result_inventory_opens_exact_diagnosis_and_links_trajectory( tmp_path: Path, ) -> None: From e842763dd8f7d037a2069b801558af48fae0f452 Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 11 Sep 2026 21:17:13 +0800 Subject: [PATCH 075/127] Clean browser report diagnostics code --- harness/browser_use/report.py | 21 ++------------------- tests/browser_use/test_report.py | 3 ++- 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/harness/browser_use/report.py b/harness/browser_use/report.py index 326bf2bd6..ae7efaaef 100644 --- a/harness/browser_use/report.py +++ b/harness/browser_use/report.py @@ -12,9 +12,9 @@ import statistics import tempfile import zipfile -from io import BytesIO from collections import Counter, defaultdict from collections.abc import Callable, Iterable +from io import BytesIO from pathlib import Path from typing import Any @@ -1668,9 +1668,7 @@ def _extract_trace_screenshots( screenshot_dir = artifact_dir / "screenshots" screenshot_dir.mkdir(parents=True, exist_ok=True) total = len(valid_frames) - for index, (payload, width, height, suffix) in enumerate( - valid_frames, start=1 - ): + for index, (payload, width, height, suffix) in enumerate(valid_frames, start=1): destination = screenshot_dir / f"frame-{index}{suffix}" destination.write_bytes(payload) output.append( @@ -2358,21 +2356,6 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat ) article = _article_body(paired_by_model, attempts, tasks, verified_causes) families = _task_family_summary(tasks, attempts) - pairing_rows = [ - [ - MODEL_LABELS[model_id], - ENGINE_LABELS[item["baseline_engine"]], - ENGINE_LABELS[item["comparison_engine"]], - item["paired"], - item["both_pass"], - item["baseline_only"], - item["comparison_only"], - item["both_fail"], - ] - for model_id, model_pairs in paired_by_model.items() - for item in model_pairs["comparisons"].values() - ] - discordant_total = sum(row[5] + row[6] for row in pairing_rows) if failed_result_ids and set(verified_causes) == failed_result_ids: failure_overview = ( _failure_cause_table(verified_causes) diff --git a/tests/browser_use/test_report.py b/tests/browser_use/test_report.py index cf30d4f73..90d707719 100644 --- a/tests/browser_use/test_report.py +++ b/tests/browser_use/test_report.py @@ -43,7 +43,8 @@ def test_best_marks_only_one_visibly_distinct_winner() -> None: def test_trace_screenshot_labels_count_only_publishable_frames(tmp_path: Path) -> None: trace = tmp_path / "trace.zip" image_payloads = {} - for sha1, size in (("large-a", (1280, 720)), ("small", (320, 200)), ("large-b", (1280, 720))): + frames = (("large-a", (1280, 720)), ("small", (320, 200)), ("large-b", (1280, 720))) + for sha1, size in frames: buffer = report_module.BytesIO() report_module.Image.new("RGB", size, "white").save(buffer, format="JPEG") image_payloads[sha1] = buffer.getvalue() From 934f1fab09838af1b40b265910a245d716e24f88 Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 11 Sep 2026 21:20:15 +0800 Subject: [PATCH 076/127] Report Artifact Hub response failures clearly --- scripts/publish_browser_use_artifact_hub.py | 38 ++++++++++++++------- tests/browser_use/test_publish.py | 21 ++++++++++-- 2 files changed, 45 insertions(+), 14 deletions(-) diff --git a/scripts/publish_browser_use_artifact_hub.py b/scripts/publish_browser_use_artifact_hub.py index 4de181fce..f51e441d1 100644 --- a/scripts/publish_browser_use_artifact_hub.py +++ b/scripts/publish_browser_use_artifact_hub.py @@ -107,6 +107,23 @@ def request( return error.code, error.read() +def response_json(status: int, raw: bytes, *, operation: str) -> dict: + """Decode an Artifact Hub response without hiding its HTTP failure.""" + + if not 200 <= status < 300: + detail = raw.decode("utf-8", errors="replace").strip()[:500] or "empty response" + raise RuntimeError(f"{operation} failed with HTTP {status}: {detail}") + try: + value = json.loads(raw) + except (TypeError, ValueError) as error: + raise RuntimeError( + f"{operation} returned HTTP {status} without a valid JSON response" + ) from error + if not isinstance(value, dict): + raise RuntimeError(f"{operation} returned a non-object JSON response") + return value + + def multipart_archive() -> tuple[bytes, str]: if not ARCHIVE.is_file(): raise ValueError(f"missing validated package: {ARCHIVE}") @@ -147,9 +164,9 @@ def main(*, create: bool = False) -> None: base, expected_slug, expected_version = publication_target(require_existing=not create) if create: status, raw = request("/api/sites", payload, content_type) - result = json.loads(raw) - if status != 200 or not result.get("slug"): - raise RuntimeError(result) + result = response_json(status, raw, operation="site creation") + if not result.get("slug"): + raise RuntimeError("site creation response omitted slug") save_receipt(result) elif not RECEIPT.exists(): raise ValueError( @@ -160,9 +177,9 @@ def main(*, create: bool = False) -> None: if result.get("slug") != expected_slug: raise ValueError("Artifact Hub receipt differs from the bound Browser Use site") status, raw = request(f"/api/sites/{result['slug']}") - current = json.loads(raw) + current = response_json(status, raw, operation="site lookup") version_id = (current.get("site") or {}).get("currentVersionId") - if status != 200 or not version_id: + if not version_id: raise RuntimeError("could not resolve the current Artifact Hub version") if version_id != expected_version: raise ValueError("Artifact Hub current version differs from current-report authority") @@ -170,9 +187,7 @@ def main(*, create: bool = False) -> None: status, raw = request( f"/api/sites/{result['slug']}/versions?{query}", payload, content_type ) - updated = json.loads(raw) - if not 200 <= status < 300: - raise RuntimeError(updated) + updated = response_json(status, raw, operation="site version upload") result.update( { key: value @@ -187,7 +202,7 @@ def main(*, create: bool = False) -> None: method="PATCH", ) if not 200 <= title_status < 300: - raise RuntimeError(json.loads(title_raw)) + response_json(title_status, title_raw, operation="site title update") result["title"] = "Chromium 与 Moli 的 Browser Use 能力对比" if not result.get("share_url"): status, raw = request( @@ -195,9 +210,8 @@ def main(*, create: bool = False) -> None: json.dumps({"policy": "public"}).encode(), "application/json", ) - if not 200 <= status < 300: - raise RuntimeError(raw.decode(errors="replace")) - result["share_url"] = json.loads(raw)["url"] + share = response_json(status, raw, operation="public share creation") + result["share_url"] = share["url"] save_receipt(result) public = urllib.request.Request( result["share_url"], headers={"User-Agent": "Browser-Use-Report-Publisher/1.0"} diff --git a/tests/browser_use/test_publish.py b/tests/browser_use/test_publish.py index 533a6626d..ea663a7cb 100644 --- a/tests/browser_use/test_publish.py +++ b/tests/browser_use/test_publish.py @@ -4,14 +4,29 @@ import json from pathlib import Path -import scripts.publish_browser_use_artifact_hub as publisher import scripts.package_browser_use_artifact_hub as packager +import scripts.publish_browser_use_artifact_hub as publisher def _sha256(path: Path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest() +def test_response_json_preserves_http_failure_context() -> None: + assert publisher.response_json(200, b'{"ok": true}', operation="upload") == {"ok": True} + for status, payload, expected in ( + (502, b"", "HTTP 502: empty response"), + (413, b"too large", "HTTP 413: too large"), + (200, b"", "HTTP 200 without a valid JSON response"), + ): + try: + publisher.response_json(status, payload, operation="upload") + except RuntimeError as error: + assert expected in str(error) + else: + raise AssertionError("invalid response must fail with diagnostic context") + + def test_validate_package_requires_and_checks_every_evidence_root( monkeypatch, tmp_path: Path ) -> None: @@ -90,7 +105,9 @@ def test_packaging_reuses_visual_gate_bound_to_current_report(monkeypatch, tmp_p ("report", report_dir, evidence_dirs, selection, require_complete) ), ) - monkeypatch.setattr(packager, "check_links", lambda report_dir: calls.append(("links", report_dir))) + monkeypatch.setattr( + packager, "check_links", lambda report_dir: calls.append(("links", report_dir)) + ) monkeypatch.setattr( packager, "check_visual", From 353edfa20120c038a20a6a70cf55dfd71adef3c1 Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 11 Sep 2026 21:23:58 +0800 Subject: [PATCH 077/127] Bind latest browser use report release --- evaluation/browser-use/current-report.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evaluation/browser-use/current-report.json b/evaluation/browser-use/current-report.json index 96c995865..4bb3c5ce6 100644 --- a/evaluation/browser-use/current-report.json +++ b/evaluation/browser-use/current-report.json @@ -57,7 +57,7 @@ "base_url": "https://artifact-hub.app.lexmount.com", "site_slug": "7l77EeXPRfml", "share_url": "https://artifact-hub.app.lexmount.com/v/Ghn1BR6PCy-njsCMqCe2LRIJ-eyJEJoa", - "expected_version": "ver_7ffdfbcb7da94e0b92188824a3558e83", + "expected_version": "ver_5e0079d21d38491999575ad51f7d8c32", "state": "verified" } } From e3d8b794841b7218e3317f7fe9f7735e54591992 Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 11 Sep 2026 21:31:05 +0800 Subject: [PATCH 078/127] Localize excluded task separators --- harness/browser_use/report_i18n.py | 5 ++++- scripts/check_browser_use_report.py | 3 +++ tests/browser_use/test_report_i18n.py | 8 ++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/harness/browser_use/report_i18n.py b/harness/browser_use/report_i18n.py index 8f07d5032..4abaf7bfb 100644 --- a/harness/browser_use/report_i18n.py +++ b/harness/browser_use/report_i18n.py @@ -1624,7 +1624,10 @@ def translate_text(value: str) -> str: ), ( r"本轮剔除 (\d+) 题:(.+)。", - lambda match: f"{match.group(1)} tasks excluded in this run: {match.group(2)}.", + lambda match: ( + f"{match.group(1)} tasks excluded in this run: " + f"{match.group(2).replace('、', ', ')}." + ), ), ( r"(\d+) 个任务×浏览器结果均保留最终回答、官方 evaluator 结果、Agent 事件和证据哈希。", diff --git a/scripts/check_browser_use_report.py b/scripts/check_browser_use_report.py index f4a7b31df..211f922c1 100644 --- a/scripts/check_browser_use_report.py +++ b/scripts/check_browser_use_report.py @@ -438,6 +438,9 @@ def check( raise ValueError(f"English report accessibility text is not localized: {page}") if any(marker in text for marker in untranslated_interface): raise ValueError(f"English report interface prose is not localized: {page}") + excluded_lists = re.findall(r"tasks excluded in this run: ([^<]+)", text) + if any("、" in value for value in excluded_lists): + raise ValueError(f"English excluded-task list uses Chinese punctuation: {page}") for engine in engines: engine_page = rendered_pages[report_dir / "engines" / f"{engine}.html"] if "engine-result-table" in engine_page or f"f_engine={engine}" not in engine_page: diff --git a/tests/browser_use/test_report_i18n.py b/tests/browser_use/test_report_i18n.py index 407f06a0c..638f0efff 100644 --- a/tests/browser_use/test_report_i18n.py +++ b/tests/browser_use/test_report_i18n.py @@ -69,6 +69,14 @@ def test_release_translation_comes_from_structured_record() -> None: assert translate_text(release["title"]) == release["i18n"]["en"]["title"] +def test_excluded_task_ids_use_english_punctuation() -> None: + source = '

    本轮剔除 3 题:#1、#2、#3。

    ' + localized = localize_html(source, PurePosixPath("analysis/index.html")) + + assert "3 tasks excluded in this run: #1, #2, #3." in localized + assert "、" not in localized + + def test_all_release_and_review_copy_localizes_without_chinese() -> None: data = json.loads(Path("research/report-changes.json").read_text(encoding="utf-8")) values = [data["history_scope"]] From c4462020cde730a951292b8d9ed7aeb7ee9d4245 Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 11 Sep 2026 21:32:38 +0800 Subject: [PATCH 079/127] Bind corrected browser use report release --- evaluation/browser-use/current-report.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evaluation/browser-use/current-report.json b/evaluation/browser-use/current-report.json index 4bb3c5ce6..ed1747237 100644 --- a/evaluation/browser-use/current-report.json +++ b/evaluation/browser-use/current-report.json @@ -57,7 +57,7 @@ "base_url": "https://artifact-hub.app.lexmount.com", "site_slug": "7l77EeXPRfml", "share_url": "https://artifact-hub.app.lexmount.com/v/Ghn1BR6PCy-njsCMqCe2LRIJ-eyJEJoa", - "expected_version": "ver_5e0079d21d38491999575ad51f7d8c32", + "expected_version": "ver_c98cab9638a4463f997f45931ba27d46", "state": "verified" } } From 9a681ba47ffea4f07bd87b7ec732b5509816a6c9 Mon Sep 17 00:00:00 2001 From: kevin Date: Fri, 11 Sep 2026 22:45:57 +0800 Subject: [PATCH 080/127] Add verified WebArena trajectory dataset pipeline --- evaluation/browser-use/current-report.json | 20 +- .../webarena-verified/README.md | 19 + .../webarena-verified/trajectories.json | 94 +++++ harness/browser_use/report.py | 286 +++++++++++---- harness/browser_use/report_i18n.py | 23 ++ .../trajectory_replay/browser_use_seeds.py | 221 ++++++++++++ harness/trajectory_replay/webarena_dataset.py | 142 ++++++++ harness/trajectory_replay/webarena_replay.py | 336 ++++++++++++++++++ research/report-changes.json | 57 +++ scripts/build_browser_use_replay_seeds.py | 32 ++ scripts/promote_webarena_trajectories.py | 36 ++ .../run_webarena_trajectory_replay_canary.py | 39 ++ tests/browser_use/test_report.py | 29 +- tests/browser_use/test_report_i18n.py | 14 + .../test_browser_use_seeds.py | 179 ++++++++++ .../test_webarena_dataset.py | 117 ++++++ .../trajectory_replay/test_webarena_replay.py | 23 ++ 17 files changed, 1584 insertions(+), 83 deletions(-) create mode 100644 evaluation/trajectory-replay/webarena-verified/README.md create mode 100644 evaluation/trajectory-replay/webarena-verified/trajectories.json create mode 100644 harness/trajectory_replay/browser_use_seeds.py create mode 100644 harness/trajectory_replay/webarena_dataset.py create mode 100644 harness/trajectory_replay/webarena_replay.py create mode 100644 scripts/build_browser_use_replay_seeds.py create mode 100644 scripts/promote_webarena_trajectories.py create mode 100644 scripts/run_webarena_trajectory_replay_canary.py create mode 100644 tests/trajectory_replay/test_browser_use_seeds.py create mode 100644 tests/trajectory_replay/test_webarena_dataset.py create mode 100644 tests/trajectory_replay/test_webarena_replay.py diff --git a/evaluation/browser-use/current-report.json b/evaluation/browser-use/current-report.json index ed1747237..90e185a84 100644 --- a/evaluation/browser-use/current-report.json +++ b/evaluation/browser-use/current-report.json @@ -35,29 +35,11 @@ "source_path": "research/browser-use-failure-cause-review.json", "source_sha256": "08e555e6d991892780a8b967741f42921424299e53dac079a7901d47446106e9" }, - "tasks_without_measured_success_path": [ - 124, - 258, - 279, - 284, - 339, - 464, - 470, - 475, - 510, - 528, - 547, - 600, - 625, - 676, - 694, - 699 - ], "publication": { "base_url": "https://artifact-hub.app.lexmount.com", "site_slug": "7l77EeXPRfml", "share_url": "https://artifact-hub.app.lexmount.com/v/Ghn1BR6PCy-njsCMqCe2LRIJ-eyJEJoa", - "expected_version": "ver_c98cab9638a4463f997f45931ba27d46", + "expected_version": "ver_48bb19785cc1409091de9a936acbcfa3", "state": "verified" } } diff --git a/evaluation/trajectory-replay/webarena-verified/README.md b/evaluation/trajectory-replay/webarena-verified/README.md new file mode 100644 index 000000000..9e7079966 --- /dev/null +++ b/evaluation/trajectory-replay/webarena-verified/README.md @@ -0,0 +1,19 @@ +# WebArena-Verified verified trajectories + +`trajectories.json` is the compact, versioned trajectory portion of the benchmark dataset. +Each trajectory is one verified feasible BrowserTool path, not the task's only correct path. + +- Browser Trajectory Replay consumes `tool_calls` as fixed inputs and uses + `evaluation_input` only when invoking the official evaluator. +- Browser Use may use the inventory as hidden solvability and diagnostic evidence. The Agent + must not receive the trajectory or evaluator input, and its score remains determined by the + official task outcome contract. +- A Browser Use success is only a candidate. Promotion requires the same candidate to replay + without a model call and pass both execution and official evaluation on every required browser. +- Full HAR, screenshots, traces, resource samples and replay receipts remain outside Git under + `BROWSER_EVAL_DATA_HOME`; this directory stores deterministic actions and provenance hashes. +- More than one trajectory may be retained for a task when paths are materially different and + independently satisfy the same promotion gate. + +Regenerate the dataset with `scripts/promote_webarena_trajectories.py`; never edit a verdict or +verification claim by hand. diff --git a/evaluation/trajectory-replay/webarena-verified/trajectories.json b/evaluation/trajectory-replay/webarena-verified/trajectories.json new file mode 100644 index 000000000..702c6f532 --- /dev/null +++ b/evaluation/trajectory-replay/webarena-verified/trajectories.json @@ -0,0 +1,94 @@ +{ + "schema": "lex_webarena_verified_trajectory_dataset/1", + "benchmark": "WebArena-Verified", + "benchmark_version": "1.2.3", + "official_data_checksum": "d65275660814663375028e9017e1f929e3c38321041b125795e2713b52243d30", + "selection_sha256": "2cb716552e36df7535e83ed6d2cf3f84283e3aca86072817cd6e039a38547c7b", + "semantics": { + "trajectory_is_one_verified_feasible_path": true, + "trajectory_is_the_only_correct_path": false, + "browser_use_scoring_authority": "official task outcome contract", + "browser_use_agent_visibility": "hidden", + "trajectory_replay_role": "fixed BrowserTool action input", + "evaluation_input_role": "official evaluator input only; never an Agent prompt" + }, + "promotion_policy": { + "required_browsers": [ + "chromium", + "moli" + ], + "process_status": "pass", + "official_evaluator_status": "pass", + "model_calls": 0 + }, + "provenance": { + "seed_manifest_sha256": "dcdf6735dcd5dc0cc9ef77110538bbd6cf0b84a2eed7f511216e48b11a1901f4", + "replay_receipts": [ + { + "sha256": "7bef7414c30814a2ea6e713b359dfccb882f67e878f004537b2583b8695102f6" + } + ] + }, + "counts": { + "verified_tasks": 1, + "verified_trajectories": 1 + }, + "trajectories": [ + { + "id": "webarena-verified-44-trajectory-1", + "task_id": 44, + "task": { + "intent": "Open my todos page", + "intent_template_id": 303, + "revision": 2, + "sites": [ + "gitlab" + ], + "start_urls": [ + "__GITLAB__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://192.168.5.2:8023" + }, + { + "action": "click", + "ref": 8 + }, + { + "action": "snapshot" + } + ], + "evaluation_input": { + "task_type": "NAVIGATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "authoritative_successful_browser_use_execution", + "candidate_id": "gpt-5.6-sol__chromium__task-44", + "model": "gpt-5.6-sol", + "browser": "chromium", + "execution": 1, + "events_sha256": "eab05ae2e7ca04df24aed3d437412a9e2dec06f83bdc7a6fff679a8e4ac5203d", + "response_sha256": "ea9dcfc71ad7b459c521cfca0a64030b90888bc7c3c4919a00da179e3604a5ce", + "evaluation_sha256": "4971d2ec4b2a99e4f3a978921400bf2b93c31f2bc698722192388d16a43cec07" + }, + "verification": { + "browsers": [ + "chromium", + "moli" + ], + "process_status": "pass", + "official_evaluator_status": "pass", + "model_calls": 0, + "receipt_run_ids": [ + "a5e7087a653c" + ] + } + } + ] +} diff --git a/harness/browser_use/report.py b/harness/browser_use/report.py index ae7efaaef..e70ebe6a5 100644 --- a/harness/browser_use/report.py +++ b/harness/browser_use/report.py @@ -391,6 +391,7 @@ def _logical_results( error for row in executions for error in (row.get("model_transport_errors") or []) ] resources = [row.get("resource") or {} for row in executions] + weighted_duration = sum(float(row.get("duration_ms") or 0) for row in executions) authoritative["resource"] = { **{ name: sum(float(resource.get(name) or 0) for resource in resources) @@ -401,15 +402,37 @@ def _logical_results( for name in ("memory_peak_bytes", "memory_retained_bytes", "pids_current_peak") }, } - weighted_duration = sum(float(row.get("duration_ms") or 0) for row in executions) if weighted_duration: - authoritative["resource"]["cpu_average_utilization_pct"] = ( + present_duration = sum( + float(row.get("duration_ms") or 0) + for row in executions + if (row.get("resource") or {}).get("cpu_average_utilization_pct") is not None + ) + if present_duration: + authoritative["resource"]["cpu_average_utilization_pct"] = ( + sum( + float((row.get("resource") or {}).get("cpu_average_utilization_pct") or 0) + * float(row.get("duration_ms") or 0) + for row in executions + if (row.get("resource") or {}).get("cpu_average_utilization_pct") + is not None + ) + / present_duration + ) + memory_sample_count = sum( + int((row.get("resource") or {}).get("sample_count") or 0) + for row in executions + if (row.get("resource") or {}).get("memory_mean_bytes") is not None + ) + if memory_sample_count: + authoritative["resource"]["memory_mean_bytes"] = ( sum( - float((row.get("resource") or {}).get("cpu_average_utilization_pct") or 0) - * float(row.get("duration_ms") or 0) + float((row.get("resource") or {}).get("memory_mean_bytes") or 0) + * int((row.get("resource") or {}).get("sample_count") or 0) for row in executions + if (row.get("resource") or {}).get("memory_mean_bytes") is not None ) - / weighted_duration + / memory_sample_count ) tool_metrics = [_execution_tool_metrics(row) for row in executions] authoritative["action_count"] = sum(item["action_count"] for item in tool_metrics) @@ -469,6 +492,7 @@ def _public_attempt(row: dict[str, Any]) -> dict[str, Any]: "cpu_ms", "network_rx_bytes", "network_tx_bytes", + "memory_mean_bytes", "memory_peak_bytes", "memory_retained_bytes", "pids_current_peak", @@ -538,6 +562,26 @@ def _primary_summary(rows: list[dict[str, Any]]) -> dict[str, dict[str, dict[str tool_durations = [float(r.get("browser_tool_duration_ms") or 0) for r in logical] tokens = [int((r.get("model_usage") or {}).get("total_tokens") or 0) for r in logical] passed = sum(float(r["score"]) == 1 for r in logical) + resource_summary = {} + for name in ( + "cpu_ms", + "cpu_average_utilization_pct", + "memory_mean_bytes", + "memory_peak_bytes", + "network_rx_bytes", + "network_tx_bytes", + ): + values = [ + float((row.get("resource") or {})[name]) + for row in logical + if (row.get("resource") or {}).get(name) is not None + ] + resource_summary[name] = { + "count": len(values), + "mean": statistics.fmean(values) if values else None, + "p50": _percentile(values, 0.50), + "p95": _percentile(values, 0.95), + } result[model][engine] = { "completed": len(logical), "passed": passed, @@ -553,6 +597,7 @@ def _primary_summary(rows: list[dict[str, Any]]) -> dict[str, dict[str, dict[str "mean_tokens": statistics.fmean(tokens) if tokens else None, "p50_tokens": _percentile(tokens, 0.50), "p95_tokens": _percentile(tokens, 0.95), + "resources": resource_summary, } return result @@ -756,11 +801,27 @@ def _task_family_table(families: list[dict[str, Any]]) -> str: def _failure_cause_table(verified_causes: dict[str, dict[str, Any]]) -> str: groups = { "agent": ("Agent / 模型行为", "保留", "属于当前被测组合;按轨迹定位检索、选择或提交错误。"), - "benchmark": ("任务 / evaluator", "排除并修复", "修正验收合同,通过独立预检后重跑四种配置。"), + "benchmark": ( + "任务 / evaluator", + "排除并修复", + "修正验收合同,通过独立预检后重跑四种配置。", + ), "site": ("自托管站点", "排除并修复", "修复依赖、会话或保存链路,通过读写预检后重跑。"), - "evidence": ("浏览器证据接口", "保留并修复", "这是浏览器集成能力的一部分;补齐响应正文或 Referer 采集后重跑。"), - "environment": ("共享测试环境", "排除,修复已完成", "跨进程串行锁已经上线;受污染任务仍需重跑。"), - "mixed": ("参考状态与 Agent 混合", "排除并重建", "重新冻结参考状态,再重跑以区分 Agent 错误。"), + "evidence": ( + "浏览器证据接口", + "保留并修复", + "这是浏览器集成能力的一部分;补齐响应正文或 Referer 采集后重跑。", + ), + "environment": ( + "共享测试环境", + "排除,修复已完成", + "跨进程串行锁已经上线;受污染任务仍需重跑。", + ), + "mixed": ( + "参考状态与 Agent 混合", + "排除并重建", + "重新冻结参考状态,再重跑以区分 Agent 错误。", + ), } def group_for(code: str) -> str: @@ -918,7 +979,7 @@ def _same_task_comparison(tasks: list[dict[str, Any]], attempts: list[dict[str, ) headers = "".join( - f"{_e(MODEL_LABELS[model])}
    {_e(ENGINE_LABELS[engine])}" + f'{_e(MODEL_LABELS[model])}
    {_e(ENGINE_LABELS[engine])}' for model, engine in configurations ) return ( @@ -933,7 +994,7 @@ def _same_task_comparison(tasks: list[dict[str, Any]], attempts: list[dict[str, '
    ' '
    ' f"{headers}" - f'{"".join(rows)}
    任务
    ' + f"{''.join(rows)}
  • " ) @@ -1136,6 +1197,80 @@ def _summary_comparison_rows(summary: dict[str, dict[str, dict[str, Any]]]) -> l return rows +def _resource_value(value: float | None, unit: str) -> str: + if value is None: + return "—" + if unit == "ms": + return f"{value / 1000:.1f} 秒" + if unit == "pct": + return f"{value:.1f}%" + if unit == "mib": + return f"{value / 1_048_576:,.1f} MiB" + if unit == "mib_net": + return f"{value / 1_048_576:,.2f} MiB" + raise ValueError(f"unknown resource unit: {unit}") + + +def _resource_comparison_table(summary: dict[str, dict[str, dict[str, Any]]]) -> str: + metrics = ( + ("cpu_ms", "CPU 时间", "ms"), + ("cpu_average_utilization_pct", "平均 CPU 单核占用", "pct"), + ("memory_mean_bytes", "时间加权平均内存", "mib"), + ("memory_peak_bytes", "峰值内存", "mib"), + ("network_rx_bytes", "网络接收量", "mib_net"), + ("network_tx_bytes", "网络发送量", "mib_net"), + ) + flat = { + f"{model}|{engine}": values + for model, model_summary in summary.items() + for engine, values in model_summary.items() + } + winners = { + (name, stat): _best( + { + key: ((values.get("resources") or {}).get(name) or {}).get(stat) + for key, values in flat.items() + }, + higher=False, + display=lambda value, unit=unit: round( + value / (1_048_576 if unit in {"mib", "mib_net"} else 1), 2 + ), + ) + for name, _, unit in metrics + for stat in ("mean", "p50", "p95") + } + rows: list[list[Any]] = [] + for model, model_summary in summary.items(): + for engine, label in ENGINE_LABELS.items(): + key = f"{model}|{engine}" + resources = model_summary[engine].get("resources") or {} + rows.append( + [ + MODEL_LABELS[model], + label, + *[ + _metric_stack( + [ + ( + stat.upper() if stat != "mean" else "均值", + _resource_value((resources.get(name) or {}).get(stat), unit), + key in winners[(name, stat)], + ) + for stat in ("mean", "p50", "p95") + ] + ) + for name, _, unit in metrics + ], + ] + ) + return _table( + ["模型", "浏览器", *[label for _, label, _ in metrics]], + rows, + table_id="resource-metrics-table", + classes="wide-data-table resource-comparison-table", + ) + + REPORT_JS = """function theme(){const r=document.documentElement,n=r.dataset.theme==='dark'?'light':'dark';r.dataset.theme=n;try{localStorage.setItem('browser-use-theme',n)}catch(e){}} function openDiagnosis(id){const d=document.getElementById(id);if(d?.showModal)d.showModal()} function closeDiagnosis(id){const d=document.getElementById(id);if(d?.open)d.close()} @@ -1250,20 +1385,20 @@ def _evidence_viewer_page() -> str: '
  • ' '

    正在读取证据…

    ' '
    '
    -        '
  • ' + "})();" ) return _page(title="完整执行证据", active="analysis", body=body) @@ -1448,8 +1583,7 @@ def _failure_point(code: str, steps: list[dict[str, Any]]) -> tuple[str, int | N ( step for step in reversed(steps) - if str(step.get("status") or "").lower() not in {"", "success"} - or step.get("error") + if str(step.get("status") or "").lower() not in {"", "success"} or step.get("error") ), None, ) @@ -1467,7 +1601,9 @@ def _failure_point(code: str, steps: list[dict[str, Any]]) -> tuple[str, int | N return f"第 {number} 步 BrowserTool 执行异常", number if code == "agent_step_budget": return (f"第 {last_step} 步后达到步骤上限" if last_step else "达到步骤上限"), last_step - return (f"第 {last_step} 步后的最终回答/任务状态" if last_step else "最终回答/任务状态"), last_step + return ( + f"第 {last_step} 步后的最终回答/任务状态" if last_step else "最终回答/任务状态" + ), last_step def _trajectory_context( @@ -1477,9 +1613,7 @@ def _trajectory_context( if step_number is None: return "", "" - step = next( - (item for item in steps if int(item.get("step") or 0) == step_number), None - ) + step = next((item for item in steps if int(item.get("step") or 0) == step_number), None) if step is None: return "", "" label = trajectory_label(step) @@ -1601,9 +1735,8 @@ def _trajectory_html(steps: list[dict[str, Any]]) -> str: rows = [] for step in steps: step_number = int(step.get("step") or len(rows) + 1) - failed = ( - str(step.get("status") or "").lower() not in {"", "success"} - or bool(step.get("error")) + failed = str(step.get("status") or "").lower() not in {"", "success"} or bool( + step.get("error") ) duration = step.get("duration_seconds") meta = f"Turn {step.get('turn') or '—'} · {_e(step.get('status') or 'unknown')}" @@ -1739,11 +1872,16 @@ def _write_execution_artifacts( } for label, filename in PUBLIC_EXECUTION_ARTIFACTS.items() } - return { - **measurements, - "artifacts": artifacts, - "screenshots": screenshots, - }, response, evaluation, steps + return ( + { + **measurements, + "artifacts": artifacts, + "screenshots": screenshots, + }, + response, + evaluation, + steps, + ) def _screenshot_gallery(result_id: str, executions: list[dict[str, Any]]) -> str: @@ -1766,7 +1904,7 @@ def _screenshot_gallery(result_id: str, executions: list[dict[str, Any]]) -> str for item in screenshots ) return ( - '

    下图是判分权威执行的最后页面画面,用于核对失败前的可见状态;' + "

    下图是判分权威执行的最后页面画面,用于核对失败前的可见状态;" "它们不一定正好对应错误发生的瞬间。

    " f'
    {figures}
    ' ) @@ -1841,7 +1979,7 @@ def _article_body( [ MODEL_LABELS[model_id], ENGINE_LABELS[engine], - f'{values["passed"]}/{values["completed"]}', + f"{values['passed']}/{values['completed']}", _pct(values["score"]), ] for model_id, model_summary in summary.items() @@ -1874,7 +2012,7 @@ def _article_body( table_id="guide-core-metrics", classes="core-comparison-table", ) - + '

    在 Sol 运行中,Chromium 比 Moli 多通过 3 题;在 Astra 运行中,两者都是 71/90。耗时需要结合下表和逐题记录看:Agent 总耗时还包含模型推理与页面等待,BrowserTool 耗时也包含工具封装和 CDP 往返。

    ' + + "

    在 Sol 运行中,Chromium 比 Moli 多通过 3 题;在 Astra 运行中,两者都是 71/90。耗时需要结合下表和逐题记录看:Agent 总耗时还包含模型推理与页面等待,BrowserTool 耗时也包含工具封装和 CDP 往返。

    " "

    同一任务上的结果

    " "

    同题比较只在同一模型内进行。A、B 是表内所列的两个浏览器。

    " + _table( @@ -1892,8 +2030,8 @@ def _article_body( caption="每行只比较同一模型、同一任务和同一网站状态下的结果。", ) + f"

    两次运行合计有 {discordant} 个同题结果不同。Sol 和 Astra 的方向并不一致,因此现有结果不足以支持某个浏览器在所有任务上更好。

    " - '

    两个模型的结果不能直接当成模型排名

    ' - '

    Sol 与 Astra 在不同时间、不同代码提交上运行。Astra 的两个 Task 77 结果还受到已确认的共享环境并发污染。表格可以帮助观察差异,但不能把差值全部归因于模型。

    ' + "

    两个模型的结果不能直接当成模型排名

    " + "

    Sol 与 Astra 在不同时间、不同代码提交上运行。Astra 的两个 Task 77 结果还受到已确认的共享环境并发污染。表格可以帮助观察差异,但不能把差值全部归因于模型。

    " + _table(["模型", "浏览器", "任务通过", "通过率"], model_result_rows) + '

    失败原因

    ' "

    evaluator 的报错只说明哪条验收规则没有满足。我们继续核对任务要求、最终回答、网络记录和 Agent 轨迹,确认失败发生在 Agent、任务规则、测试网站、共享环境或浏览器证据记录中的哪一处。

    " @@ -2020,9 +2158,9 @@ def _diagnosis_dialog( f'' '
    ' f'' - f'
    程序判定{_e(automatic_failure or "未分类")}
    ' - f'
    复核根因{_e(subcategory or "待复核")}
    ' - f'
    负责范围{_e(owner)}
    ' + f"
    程序判定{_e(automatic_failure or '未分类')}
    " + f"
    复核根因{_e(subcategory or '待复核')}
    " + f"
    负责范围{_e(owner)}
    " + ( f'
    {_e(step_context_label)}{_e(step_context)}
    ' if step_context @@ -2031,7 +2169,7 @@ def _diagnosis_dialog( + '

    程序报错

    ' f'
    {_e(evaluator_error)}
    ' '

    复核结论

    ' - f'

    {_e(reason or "尚未完成根因复核。")}

    处理:{_e(action)}

    ' + f"

    {_e(reason or '尚未完成根因复核。')}

    处理:{_e(action)}

    " '
    ' f'查看期望与实际值' f'查看失败附近轨迹' @@ -2060,15 +2198,13 @@ def _attempt_table( event_record = read_public_json(Path((row.get("paths") or {}).get("events", ""))) steps = trajectory(event_record if isinstance(event_record, list) else []) failure_point, step_number = _failure_point(code, steps) - step_context_label, step_context = _trajectory_context( - steps, step_number, failure_point - ) + step_context_label, step_context = _trajectory_context(steps, step_number, failure_point) owner, action, _external = _remediation(code) task = task_meta.get(p["task_id"], {}) point_link = ( - f'results/{p["id"]}.html#trajectory-step-{step_number}' + f"results/{p['id']}.html#trajectory-step-{step_number}" if step_number - else f'results/{p["id"]}.html#diagnosis' + else f"results/{p['id']}.html#diagnosis" ) dialog_id = f"diagnosis-{p['id']}" if result == "fail": @@ -2093,14 +2229,16 @@ def _attempt_table( Safe(f'{p["id"]}'), Safe( f'{_e(MODEL_LABELS[p["model"]])}' - f'{_e(ENGINE_LABELS[p["engine"]])}' + f"{_e(ENGINE_LABELS[p['engine']])}" ), Safe(f'#{p["task_id"]}'), _status(row), Safe(f'{_e(failure_point)}') if result == "fail" else "—", automatic_failure or "—", subcategory or "待复核" if result == "fail" else "—", - Safe(f'{_e(owner)}{_e(action)}') if result == "fail" else "—", + Safe(f'{_e(owner)}{_e(action)}') + if result == "fail" + else "—", Safe( ( f'' @@ -2345,9 +2483,7 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat (stage / folder).mkdir() (stage / "assets/style.css").write_text(REPORT_STYLE, encoding="utf-8") (stage / "assets/report.js").write_text(REPORT_JS, encoding="utf-8") - (stage / "evidence-viewer.html").write_text( - _evidence_viewer_page(), encoding="utf-8" - ) + (stage / "evidence-viewer.html").write_text(_evidence_viewer_page(), encoding="utf-8") if not METHODOLOGY_FIGURE.is_file(): raise FileNotFoundError(f"missing methodology figure: {METHODOLOGY_FIGURE}") shutil.copy2( @@ -2376,7 +2512,15 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat home += _section( "四种配置的总体结果", _table( - ["模型", "浏览器", "通过任务", "通过率", "Agent 总耗时", "BrowserTool 耗时", "Tokens"], + [ + "模型", + "浏览器", + "通过任务", + "通过率", + "Agent 总耗时", + "BrowserTool 耗时", + "Tokens", + ], _summary_comparison_rows(summary_by_model), table_id="core-metrics-table", classes="core-comparison-table", @@ -2385,6 +2529,12 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat + f'

    四种配置均使用同一组 {task_count} 题。模型和浏览器同时变化时,表格比较的是整套配置,不能把差值只归因于浏览器。查看全部配置的逐题矩阵 →

    ' + '
    耗时统计包含什么

    Agent 总耗时包含模型推理、页面等待和浏览器操作;BrowserTool 耗时是全部浏览器工具调用的墙钟时间之和,仍包含工具封装与 CDP 往返,不是纯内核 CPU 时间。

    ', ) + home += _section( + "速度与浏览器资源", + f"

    下表统一比较四种配置。CPU、内存和网络只计浏览器容器,不含 CowAgent 或模型服务;均值、P50、P95 都按 {task_count} 个逻辑任务结果统计。模型会改变动作数量和页面停留时间,因此这里比较的是整套配置,不能单独归因于浏览器内核。

    " + + _resource_comparison_table(summary_by_model) + + '

    仅标记四种配置中数值唯一最低的一项。CPU 时间与网络总量累计得到逻辑结果所用的全部执行;内存来自 Browser Use 执行阶段的浏览器容器采样。

    ', + ) home += _section( "失败与干扰项", f"

    {len(failed_result_ids)} 个失败结果已经逐条核验。与任务合同、站点、共享环境或参考状态有关的任务不会进入诊断子集,修复后需完整重跑。

    " @@ -2400,10 +2550,26 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat _table( ["检查项", "结果", "说明"], [ - ["任务结果", f"{len(attempts)}/{len(attempts)}", f"{len(MODEL_LABELS)} 个模型 × {len(ENGINE_LABELS)} 个浏览器 × {task_count} 题都有最终判定"], - ["失败原因核验", f"{len(verified_causes)}/{len(failed_result_ids)}", "每个失败结果都有分类和具体原因"], - ["未解决的证据问题", len(review.get("unresolved") or []), "完整性检查没有发现待处理项"], - ["至少成功过一次的任务", f"{len(measured_solvable_ids)}/{len(tasks)}", f"另有 {len(unproven_ids)} 题在本轮没有成功轨迹"], + [ + "任务结果", + f"{len(attempts)}/{len(attempts)}", + f"{len(MODEL_LABELS)} 个模型 × {len(ENGINE_LABELS)} 个浏览器 × {task_count} 题都有最终判定", + ], + [ + "失败原因核验", + f"{len(verified_causes)}/{len(failed_result_ids)}", + "每个失败结果都有分类和具体原因", + ], + [ + "未解决的证据问题", + len(review.get("unresolved") or []), + "完整性检查没有发现待处理项", + ], + [ + "至少成功过一次的任务", + f"{len(measured_solvable_ids)}/{len(tasks)}", + f"另有 {len(unproven_ids)} 题在本轮没有成功轨迹", + ], ["保存的实际执行", len(source_attempts), "包含正式执行及按协议发生的一次重试"], ], table_id="evidence-coverage", @@ -2757,9 +2923,7 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat step_context_label, step_context = _trajectory_context( steps, failure_step, failure_point ) - remediation_owner, remediation_action, external_interference = _remediation( - cause_code - ) + remediation_owner, remediation_action, external_interference = _remediation(cause_code) detail = _hero( public["id"], f"{MODEL_LABELS[public['model']]} · {ENGINE_LABELS[public['engine']]} · Task #{public['task_id']}", @@ -2800,15 +2964,15 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat ) if float(public["score"]) < 1: step_href = ( - f'{attempt_page}#trajectory-step-{failure_step}' + f"{attempt_page}#trajectory-step-{failure_step}" if failure_step - else f'{attempt_page}#trajectory' + else f"{attempt_page}#trajectory" ) detail += '
    ' + _section( "失败诊断", '' + f"

    {_e(_failure_brief(evaluation_record, row))}

    " + ( f'

    {_e(step_context_label)}:{_e(step_context)}

    ' if step_context @@ -2839,7 +3003,7 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat detail += _section( "结果", '
    通过' - f'

    {_e(_failure_brief(evaluation_record, row))}

    ' + f"

    {_e(_failure_brief(evaluation_record, row))}

    " + runtime_facts, ) detail += '
    ' + _section( diff --git a/harness/browser_use/report_i18n.py b/harness/browser_use/report_i18n.py index 4abaf7bfb..bc658fd97 100644 --- a/harness/browser_use/report_i18n.py +++ b/harness/browser_use/report_i18n.py @@ -1240,6 +1240,14 @@ def _register_report_change_translations() -> None: "四种配置中通过数最高": "Highest pass count across four configurations", "四种配置中通过率最高": "Highest pass rate across four configurations", "四种配置中单次均值最低": "Lowest mean per-call latency across four configurations", + "速度与浏览器资源": "Speed and browser resources", + "CPU 时间": "CPU time", + "平均 CPU 单核占用": "Mean single-core CPU utilization", + "时间加权平均内存": "Time-weighted mean memory", + "峰值内存": "Peak memory", + "网络接收量": "Network received", + "网络发送量": "Network transmitted", + "仅标记四种配置中数值唯一最低的一项。CPU 时间与网络总量累计得到逻辑结果所用的全部执行;内存来自 Browser Use 执行阶段的浏览器容器采样。": "Only a unique lowest value across the four configurations is marked. CPU time and network totals include every execution used by the logical result; memory comes from browser-container samples during Browser Use execution.", "四种配置统一比较,只标记唯一最优值;相同值不标。通过数越高越好,耗时和 Tokens 越低越好。": "All four configurations are compared together, and only a unique best value is marked. Ties are unmarked. Higher pass counts are better; lower latency and token use are better.", "耗时和 Tokens 如何统计": "How latency and tokens are measured", "Agent 链路累计耗时把取得逻辑结果所发生的执行相加,包含登录、模型推理、调度、页面等待、BrowserTool 与收尾,不包含此前的 Docker 站点重置。BrowserTool 累计耗时是所有 browser 工具调用的墙钟时间之和,包含工具封装、CDP 往返和页面等待。Tokens 同样累计正式结果包含的物理执行。": "Cumulative Agent-path latency sums the executions used to obtain the logical result. It includes sign-in, model inference, orchestration, page waits, BrowserTool calls, and cleanup, but excludes the preceding Docker site reset. Cumulative BrowserTool latency sums the wall time of all browser-tool calls, including tool-wrapper, CDP round-trip, and page-wait overhead. Tokens likewise sum the physical executions included in the official result.", @@ -1277,10 +1285,17 @@ def _register_report_change_translations() -> None: "打开原始 JSON": "Open raw JSON", "在页面中查看完整执行证据": "View complete execution evidence on this page", "失败与干扰项": "Failures and interference", + "在四种配置中统一比较并只标记唯一最优值;相同值不标。通过数越高越好,耗时和 Tokens 越低越好。": "Values are compared across all four configurations, and only a unique best value is marked. Higher pass counts are better; lower latency and token usage are better.", + "查看全部配置的逐题矩阵 →": "View the task-by-task matrix for all configurations →", + "耗时统计包含什么": "What the latency statistics include", + "Agent 总耗时包含模型推理、页面等待和浏览器操作;BrowserTool 耗时是全部浏览器工具调用的墙钟时间之和,仍包含工具封装与 CDP 往返,不是纯内核 CPU 时间。": "Agent latency includes model inference, page waits, and browser operations. BrowserTool latency is the sum of wall-clock time across browser-tool calls; it still includes tool-wrapper and CDP round-trip overhead and is not pure browser-core CPU time.", + "Agent / 模型行为": "Agent / model behavior", + "浏览器证据接口": "Browser evidence interface", "原因范围": "Cause scope", "本轮处理": "Treatment in this run", "修复与验证": "Fix and verification", "保留": "Retain", + "保留并修复": "Retain and fix", "排除并修复": "Exclude and fix", "排除,修复已完成": "Exclude; fix completed", "排除并重建": "Exclude and rebuild", @@ -1432,6 +1447,10 @@ def translate_text(value: str) -> str: r"四种配置均使用同一组 (\d+) 题。模型和浏览器同时变化时,表格比较的是整套配置,不能把差值只归因于浏览器。", lambda m: f"All four configurations use the same {m.group(1)} tasks. When both model and browser vary, the table compares complete configurations; differences cannot be attributed to the browser alone.", ), + ( + r"下表统一比较四种配置。CPU、内存和网络只计浏览器容器,不含 CowAgent 或模型服务;均值、P50、P95 都按 (\d+) 个逻辑任务结果统计。模型会改变动作数量和页面停留时间,因此这里比较的是整套配置,不能单独归因于浏览器内核。", + lambda m: f"The table compares all four configurations. CPU, memory, and network measurements cover only the browser container, excluding CowAgent and the model service; mean, P50, and P95 are computed across {m.group(1)} logical task results. Because the model changes action counts and time spent on pages, these are whole-configuration comparisons and cannot be attributed to the browser core alone.", + ), ( r"(.+?) · (Chromium|Moli 1\.1\.3) 结果", lambda m: f"{m.group(1)} · {m.group(2)} result", @@ -1520,6 +1539,10 @@ def translate_text(value: str) -> str: r"(\d+) 个失败是怎么造成的", lambda m: f"What caused the {m.group(1)} failures", ), + ( + r"(\d+) 个失败结果已经逐条核验。与任务合同、站点、共享环境或参考状态有关的任务不会进入诊断子集,修复后需完整重跑。", + lambda m: f"All {m.group(1)} failed results have been reviewed individually. Tasks affected by task-contract, site, shared-environment, or reference-state issues are excluded from the diagnostic subset and require a complete rerun after correction.", + ), ( r"以上共 (\d+) 个失败结果;每个结果只计一次。", lambda m: f"The table covers {m.group(1)} failed results, each counted once.", diff --git a/harness/trajectory_replay/browser_use_seeds.py b/harness/trajectory_replay/browser_use_seeds.py new file mode 100644 index 000000000..0aa9b7718 --- /dev/null +++ b/harness/trajectory_replay/browser_use_seeds.py @@ -0,0 +1,221 @@ +"""Derive fixed replay candidates from successful autonomous Browser Use evidence.""" + +from __future__ import annotations + +import hashlib +import json +from collections import defaultdict +from pathlib import Path +from typing import Any + +SCHEMA = "lex_browser_use_replay_seeds/1" + + +def _sha256(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def _read_json(path: Path) -> Any: + return json.loads(path.read_text(encoding="utf-8")) + + +def extract_browser_calls(events_path: str | Path) -> list[dict[str, Any]]: + """Return complete BrowserTool calls in execution order, including call status.""" + + starts: dict[str, dict[str, Any]] = {} + calls: list[dict[str, Any]] = [] + for event in _read_json(Path(events_path)): + event_type = event.get("type") + data = event.get("data") or {} + call_id = str(data.get("tool_call_id") or "") + if event_type == "tool_execution_start": + if not call_id or call_id in starts: + raise ValueError(f"invalid or duplicate tool call id in {events_path}: {call_id!r}") + starts[call_id] = { + "tool_call_id": call_id, + "tool_name": str(data.get("tool_name") or ""), + "arguments": dict(data.get("arguments") or {}), + "started_at": event.get("timestamp"), + } + elif event_type == "tool_execution_end": + start = starts.pop(call_id, None) + if start is None: + raise ValueError(f"tool end event has no start in {events_path}: {call_id!r}") + start.update( + { + "status": str(data.get("status") or ""), + "duration_seconds": data.get("execution_time"), + "error": data.get("error"), + "ended_at": event.get("timestamp"), + } + ) + calls.append(start) + if starts: + raise ValueError(f"tool calls have no end event in {events_path}: {sorted(starts)}") + return calls + + +def _authoritative_rows(root: Path) -> tuple[dict[str, Any], list[dict[str, Any]]]: + manifest = _read_json(root / "manifest.json") + model = str((manifest.get("protocol") or {}).get("model") or "") + if not model: + raise ValueError(f"evidence manifest does not identify a model: {root}") + grouped: dict[tuple[str, int], list[dict[str, Any]]] = defaultdict(list) + for line in (root / "attempts.jsonl").read_text(encoding="utf-8").splitlines(): + if line.strip(): + row = json.loads(line) + grouped[(str(row["engine"]), int(row["task_id"]))].append(row) + rows: list[dict[str, Any]] = [] + for (engine, task_id), executions in sorted(grouped.items()): + executions.sort(key=lambda row: int(row["attempt"])) + expected = list(range(1, len(executions) + 1)) + if [int(row["attempt"]) for row in executions] != expected: + raise ValueError(f"non-contiguous executions for {model}/{engine}/{task_id}") + if len(executions) > 2: + raise ValueError(f"more than one retry for {model}/{engine}/{task_id}") + row = dict(executions[-1]) + row["model"] = model + rows.append(row) + return manifest, rows + + +def build_replay_seed_manifest( + evidence_dirs: list[str | Path], profile: dict[str, Any] +) -> dict[str, Any]: + """Build a private replay seed inventory without using human demonstration traces.""" + + models = [str(value) for value in profile.get("models") or []] + engines = [str(value) for value in profile.get("engines") or []] + method_priority = [(model, engine) for model in models for engine in engines] + if not method_priority: + raise ValueError("profile must register models and engines") + + manifests: dict[str, dict[str, Any]] = {} + authoritative: list[dict[str, Any]] = [] + source_roots: dict[str, Path] = {} + for raw_root in evidence_dirs: + root = Path(raw_root).expanduser().resolve() + manifest, rows = _authoritative_rows(root) + model = str((manifest.get("protocol") or {}).get("model")) + if model in manifests: + raise ValueError(f"duplicate evidence cohort for model: {model}") + manifests[model] = manifest + source_roots[model] = root + authoritative.extend(rows) + if set(manifests) != set(models): + raise ValueError( + f"evidence models differ from profile: expected={models}, actual={sorted(manifests)}" + ) + selection_hashes = {str(item.get("selection_sha256") or "") for item in manifests.values()} + if len(selection_hashes) != 1 or "" in selection_hashes: + raise ValueError("evidence cohorts do not share one frozen task selection") + + task_metadata: dict[int, dict[str, Any]] = {} + for manifest in manifests.values(): + for item in manifest.get("selection") or []: + task_id = int(item["task_id"]) + if task_id in task_metadata and task_metadata[task_id] != item: + raise ValueError(f"task metadata differs between evidence cohorts: {task_id}") + task_metadata[task_id] = dict(item) + + candidates_by_task: dict[int, list[dict[str, Any]]] = defaultdict(list) + passed_methods_by_task: dict[int, list[str]] = defaultdict(list) + for row in authoritative: + task_id = int(row["task_id"]) + if float(row.get("score") or 0) != 1: + continue + model, engine = str(row["model"]), str(row["engine"]) + method_id = f"{model}__{engine}" + passed_methods_by_task[task_id].append(method_id) + events_path = Path(str((row.get("paths") or {}).get("events") or "")) + response_path = Path(str((row.get("paths") or {}).get("response") or "")) + evaluation_path = Path(str((row.get("paths") or {}).get("evaluation") or "")) + calls = extract_browser_calls(events_path) + clean = bool(calls) and all(call["status"] == "success" for call in calls) + candidate = { + "id": f"{method_id}__task-{task_id}", + "method": {"model": model, "engine": engine}, + "source_execution": int(row["attempt"]), + "source_run_id": str(row.get("run_id") or ""), + "official_score": 1.0, + "clean": clean, + "action_count": len(calls), + "actions": [str((call.get("arguments") or {}).get("action") or "") for call in calls], + "tool_calls": [dict(call.get("arguments") or {}) for call in calls], + "recorded_response": _read_json(response_path), + "source": { + "events_path": str(events_path), + "events_sha256": _sha256(events_path), + "response_path": str(response_path), + "response_sha256": _sha256(response_path), + "evaluation_path": str(evaluation_path), + "evaluation_sha256": _sha256(evaluation_path), + }, + } + candidates_by_task[task_id].append(candidate) + + priority_index = {method: index for index, method in enumerate(method_priority)} + tasks: list[dict[str, Any]] = [] + for task_id in sorted(task_metadata): + candidates = sorted( + candidates_by_task.get(task_id, []), + key=lambda item: priority_index[(item["method"]["model"], item["method"]["engine"])], + ) + clean_candidates = [item for item in candidates if item["clean"]] + tasks.append( + { + **task_metadata[task_id], + "successful_methods": sorted(passed_methods_by_task.get(task_id, [])), + "candidates": candidates, + "canonical_candidate_id": clean_candidates[0]["id"] if clean_candidates else None, + "replay_status": ( + "ready" + if clean_candidates + else "success_contains_failed_actions" + if candidates + else "all_methods_failed" + ), + } + ) + + return { + "schema": SCHEMA, + "source": { + "kind": "authoritative successful Browser Use executions", + "human_demonstrations_used": False, + "benchmark": str(profile.get("benchmark") or "WebArena-Verified"), + "benchmark_version": str(profile.get("benchmark_version") or ""), + "official_data_checksum": str(profile.get("official_data_checksum") or ""), + "selection_sha256": selection_hashes.pop(), + "evidence": { + model: { + "root": str(source_roots[model]), + "attempts_sha256": _sha256(source_roots[model] / "attempts.jsonl"), + "manifest_sha256": _sha256(source_roots[model] / "manifest.json"), + } + for model in models + }, + }, + "selection_policy": { + "authority": "final execution declared by each frozen Browser Use manifest", + "eligibility": "official score 1.0 and every BrowserTool call completed successfully", + "canonical_priority": [f"{model}__{engine}" for model, engine in method_priority], + "performance_used_for_selection": False, + }, + "counts": { + "tasks": len(tasks), + "tasks_with_authoritative_success": sum(bool(item["candidates"]) for item in tasks), + "replay_ready_tasks": sum(item["replay_status"] == "ready" for item in tasks), + "success_with_failed_actions_tasks": sum( + item["replay_status"] == "success_contains_failed_actions" for item in tasks + ), + "all_methods_failed_tasks": sum( + item["replay_status"] == "all_methods_failed" for item in tasks + ), + "successful_method_trajectories": sum(len(item["candidates"]) for item in tasks), + "clean_method_trajectories": sum( + sum(bool(candidate["clean"]) for candidate in item["candidates"]) for item in tasks + ), + }, + "tasks": tasks, + } diff --git a/harness/trajectory_replay/webarena_dataset.py b/harness/trajectory_replay/webarena_dataset.py new file mode 100644 index 000000000..94c6581a2 --- /dev/null +++ b/harness/trajectory_replay/webarena_dataset.py @@ -0,0 +1,142 @@ +"""Promote replay-proven Browser Use trajectories into the shared dataset.""" + +from __future__ import annotations + +import hashlib +import json +from collections import defaultdict +from pathlib import Path +from typing import Any + +SCHEMA = "lex_webarena_verified_trajectory_dataset/1" + + +def _read_json(path: Path) -> Any: + return json.loads(path.read_text(encoding="utf-8")) + + +def _sha256(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def build_verified_trajectory_dataset( + seed_manifest_path: str | Path, + replay_receipt_paths: list[str | Path], + *, + required_engines: tuple[str, ...] = ("chromium", "moli"), +) -> dict[str, Any]: + """Return compact trajectories that passed process and official evaluation gates.""" + + seed_path = Path(seed_manifest_path).expanduser().resolve() + seeds = _read_json(seed_path) + if seeds.get("schema") != "lex_browser_use_replay_seeds/1": + raise ValueError("unsupported replay seed manifest") + seed_tasks = {int(item["task_id"]): item for item in seeds.get("tasks") or []} + + attempts_by_key: dict[tuple[int, str], list[dict[str, Any]]] = defaultdict(list) + receipt_runs_by_key: dict[tuple[int, str], set[str]] = defaultdict(set) + receipts: list[dict[str, str]] = [] + for raw_path in replay_receipt_paths: + receipt_path = Path(raw_path).expanduser().resolve() + receipt = _read_json(receipt_path) + if receipt.get("schema") != "lex_webarena_trajectory_replay_canary/1": + raise ValueError(f"unsupported replay receipt: {receipt_path}") + receipts.append({"sha256": _sha256(receipt_path)}) + for attempt in receipt.get("attempts") or []: + key = (int(attempt["task_id"]), str(attempt["source_candidate_id"])) + attempts_by_key[key].append(attempt) + receipt_runs_by_key[key].add(str(receipt.get("run_id") or "")) + + trajectories: list[dict[str, Any]] = [] + for (task_id, candidate_id), attempts in sorted(attempts_by_key.items()): + task = seed_tasks.get(task_id) + if task is None: + raise ValueError(f"receipt references task missing from seeds: {task_id}") + candidate = next( + (item for item in task.get("candidates") or [] if item.get("id") == candidate_id), + None, + ) + if candidate is None: + raise ValueError(f"receipt references candidate missing from seeds: {candidate_id}") + by_engine: dict[str, dict[str, Any]] = {} + for item in attempts: + engine = str(item["engine"]) + if engine in by_engine: + raise ValueError(f"duplicate replay proof for {candidate_id}/{engine}") + by_engine[engine] = item + if set(by_engine) != set(required_engines): + continue + if any( + item.get("process_status") != "pass" + or item.get("evaluator_status") != "pass" + or int(item.get("completed_action_count") or 0) != int(candidate["action_count"]) + for item in by_engine.values() + ): + continue + trajectories.append( + { + "id": f"webarena-verified-{task_id}-trajectory-1", + "task_id": task_id, + "task": { + key: task[key] + for key in ( + "intent", + "intent_template_id", + "revision", + "sites", + "start_urls", + ) + if key in task + }, + "tool_calls": candidate["tool_calls"], + "evaluation_input": candidate["recorded_response"], + "source": { + "kind": "authoritative_successful_browser_use_execution", + "candidate_id": candidate_id, + "model": candidate["method"]["model"], + "browser": candidate["method"]["engine"], + "execution": candidate["source_execution"], + "events_sha256": candidate["source"]["events_sha256"], + "response_sha256": candidate["source"]["response_sha256"], + "evaluation_sha256": candidate["source"]["evaluation_sha256"], + }, + "verification": { + "browsers": list(required_engines), + "process_status": "pass", + "official_evaluator_status": "pass", + "model_calls": 0, + "receipt_run_ids": sorted(receipt_runs_by_key[(task_id, candidate_id)] - {""}), + }, + } + ) + + return { + "schema": SCHEMA, + "benchmark": seeds["source"].get("benchmark") or "WebArena-Verified", + "benchmark_version": seeds["source"].get("benchmark_version") or None, + "official_data_checksum": seeds["source"].get("official_data_checksum") or None, + "selection_sha256": seeds["source"]["selection_sha256"], + "semantics": { + "trajectory_is_one_verified_feasible_path": True, + "trajectory_is_the_only_correct_path": False, + "browser_use_scoring_authority": "official task outcome contract", + "browser_use_agent_visibility": "hidden", + "trajectory_replay_role": "fixed BrowserTool action input", + "evaluation_input_role": "official evaluator input only; never an Agent prompt", + }, + "promotion_policy": { + "required_browsers": list(required_engines), + "process_status": "pass", + "official_evaluator_status": "pass", + "model_calls": 0, + }, + "provenance": { + "seed_manifest_sha256": _sha256(seed_path), + "replay_receipts": receipts, + }, + "counts": { + "verified_tasks": len({item["task_id"] for item in trajectories}), + "verified_trajectories": len(trajectories), + }, + "trajectories": trajectories, + } diff --git a/harness/trajectory_replay/webarena_replay.py b/harness/trajectory_replay/webarena_replay.py new file mode 100644 index 000000000..4132d3089 --- /dev/null +++ b/harness/trajectory_replay/webarena_replay.py @@ -0,0 +1,336 @@ +"""Model-free replay of fixed Browser Use trajectories on WebArena-Verified.""" + +from __future__ import annotations + +import json +import sys +import time +import traceback +import uuid +from pathlib import Path +from typing import Any +from urllib.parse import urlsplit, urlunsplit + +import docker + +from harness.browser_use.har import CdpHarRecorder +from harness.browser_use.runner import ( + SITE_PORTS, + _allow_official_container_hostname, + _configure_benchmark, + _container_host_gateway, + _login_context, + _physical_execution_lock, + _physical_execution_lock_path, + _prepare_sites, + _resource_summary, +) +from harness.trajectory_replay.metrics import DockerStatsResources, ResourceSampler +from harness.trajectory_replay.runner import ( + ENGINE_IMAGES, + ENGINE_PORTS, + _published_endpoint, + _start_browser_container, + _wait_cdp, +) + + +def _candidate(task: dict[str, Any]) -> dict[str, Any]: + candidate_id = task.get("canonical_candidate_id") + for item in task.get("candidates") or []: + if item.get("id") == candidate_id: + return item + raise ValueError(f"task {task.get('task_id')} has no canonical replay candidate") + + +def select_tasks(seed_manifest: dict[str, Any], task_ids: list[int]) -> list[dict[str, Any]]: + by_id = {int(task["task_id"]): task for task in seed_manifest.get("tasks") or []} + missing = sorted(set(task_ids) - set(by_id)) + if missing: + raise ValueError(f"tasks are not present in the replay seed manifest: {missing}") + selected = [by_id[task_id] for task_id in task_ids] + blocked = [int(task["task_id"]) for task in selected if task.get("replay_status") != "ready"] + if blocked: + raise ValueError(f"tasks do not have clean replay candidates: {blocked}") + return selected + + +def _rebind_url(value: str, site_access_host: str) -> str: + parsed = urlsplit(value) + if parsed.scheme not in {"http", "https"} or parsed.port not in { + port for pair in SITE_PORTS.values() for port in pair + }: + return value + host = f"[{site_access_host}]" if ":" in site_access_host else site_access_host + netloc = f"{host}:{parsed.port}" + return urlunsplit((parsed.scheme, netloc, parsed.path, parsed.query, parsed.fragment)) + + +def _rebind_calls(calls: list[dict[str, Any]], site_access_host: str) -> list[dict[str, Any]]: + rebound = [] + for call in calls: + item = dict(call) + if isinstance(item.get("url"), str): + item["url"] = _rebind_url(item["url"], site_access_host) + rebound.append(item) + return rebound + + +def _run_one( # noqa: PLR0913 + *, + task_seed: dict[str, Any], + engine: str, + endpoint: str, + container: Any, + benchmark: Any, + cowagent_repo: Path, + output_dir: Path, + sample_interval_ms: int, +) -> dict[str, Any]: + task_id = int(task_seed["task_id"]) + candidate = _candidate(task_seed) + official_task = benchmark.get_task(task_id) + sites = tuple(official_task.sites) + start_urls = list(benchmark.config.render_url(list(official_task.start_urls), list(sites))) + calls = _rebind_calls(candidate["tool_calls"], _container_host_gateway(container)) + attempt_dir = output_dir / engine / str(task_id) + attempt_dir.mkdir(parents=True, exist_ok=True) + har_path = attempt_dir / "network.har" + trace_path = attempt_dir / "trace.json" + evaluation_path = attempt_dir / "evaluation.json" + screenshot_path = attempt_dir / "failure.png" + + if str(cowagent_repo) not in sys.path: + sys.path.insert(0, str(cowagent_repo)) + from agent.tools.browser.browser_tool import BrowserTool # noqa: PLC0415 + + tool = BrowserTool( + config={ + "cdp_endpoint": endpoint, + "headless": True, + "viewport": {"width": 1280, "height": 900}, + "startup_timeout": 90, + } + ) + _allow_official_container_hostname(tool) + resources = DockerStatsResources(container) + sampler = ResourceSampler(resources, interval_ms=sample_interval_ms) + before = None + resource_started = None + recorder = CdpHarRecorder() + records: list[dict[str, Any]] = [] + timings: dict[str, float] = {} + process_status = "fail" + evaluator_status = "not_run" + error = "" + started = time.perf_counter() + sampler_started = False + try: + prepare_started = time.perf_counter() + _prepare_sites(sites) + timings["environment_prepare_ms"] = (time.perf_counter() - prepare_started) * 1000 + + setup_started = time.perf_counter() + service = tool._get_service() + service._submit(lambda: None) + readiness_retries = _login_context(service, sites, benchmark, start_urls) + service._submit(lambda: recorder.start(service._context)) + timings["browser_session_setup_ms"] = (time.perf_counter() - setup_started) * 1000 + + before = resources.counters() + resource_started = time.perf_counter() + sampler.start() + sampler_started = True + replay_started = time.perf_counter() + for index, arguments in enumerate(calls, start=1): + call_started = time.perf_counter() + result = tool.execute(arguments) + record = { + "index": index, + "arguments": arguments, + "status": result.status, + "duration_ms": round((time.perf_counter() - call_started) * 1000, 3), + "result": result.result, + } + records.append(record) + if result.status != "success": + raise RuntimeError(f"step {index} failed: {result.result}") + timings["trajectory_replay_ms"] = (time.perf_counter() - replay_started) * 1000 + process_status = "pass" + + finalize_started = time.perf_counter() + service._submit( + lambda: ( + service._page.wait_for_timeout(500), + recorder.stop(service._context, har_path), + ) + ) + timings["evidence_finalize_ms"] = (time.perf_counter() - finalize_started) * 1000 + + evaluation_started = time.perf_counter() + evaluation = benchmark.evaluate_task( + task_id=task_id, + agent_response=candidate["recorded_response"], + network_trace=har_path, + ).model_dump(mode="json") + timings["evaluation_ms"] = (time.perf_counter() - evaluation_started) * 1000 + evaluator_status = "pass" if float(evaluation.get("score") or 0) == 1 else "fail" + evaluation_path.write_text( + json.dumps(evaluation, ensure_ascii=False, indent=2), encoding="utf-8" + ) + except Exception: + error = traceback.format_exc() + try: + service = tool._get_service() + service._submit(lambda: recorder.stop(service._context, har_path)) + service._submit(lambda: service._page.screenshot(path=str(screenshot_path))) + except Exception: + pass + readiness_retries = locals().get("readiness_retries", 0) + finally: + duration_ms = (time.perf_counter() - started) * 1000 + if sampler_started: + sampler.stop() + after = resources.counters() if before is not None else None + resource_duration_ms = ( + (time.perf_counter() - resource_started) * 1000 if resource_started is not None else 0.0 + ) + tool.close() + + timings["browser_resource_measurement_ms"] = resource_duration_ms + + trace_path.write_text(json.dumps(records, ensure_ascii=False, indent=2), encoding="utf-8") + return { + "schema": "lex_webarena_trajectory_replay_attempt/1", + "engine": engine, + "task_id": task_id, + "source_candidate_id": candidate["id"], + "source_model": candidate["method"]["model"], + "source_engine": candidate["method"]["engine"], + "action_count": len(calls), + "completed_action_count": sum(record["status"] == "success" for record in records), + "process_status": process_status, + "evaluator_status": evaluator_status, + "status": "pass" if process_status == evaluator_status == "pass" else "fail", + "duration_ms": round(duration_ms, 3), + "timings": {key: round(value, 3) for key, value in timings.items()}, + "browser_session_readiness_retries": readiness_retries, + "resource": ( + _resource_summary(resources, sampler, before, after, resource_duration_ms) + if before is not None and after is not None and sampler.samples + else {} + ), + "error": error, + "paths": { + "trace": str(trace_path), + "network_har": str(har_path), + "evaluation": str(evaluation_path), + "failure_screenshot": str(screenshot_path) if screenshot_path.is_file() else None, + }, + } + + +def run_canary_replay( # noqa: PLR0913 + *, + seed_manifest_path: str | Path, + task_ids: list[int], + engines: list[str], + output_dir: str | Path, + cowagent_repo: str | Path, + cpus: float = 2.0, + memory: str = "2g", + sample_interval_ms: int = 100, +) -> dict[str, Any]: + seeds = json.loads(Path(seed_manifest_path).read_text(encoding="utf-8")) + tasks = select_tasks(seeds, task_ids) + unknown_engines = sorted(set(engines) - set(ENGINE_IMAGES)) + if unknown_engines: + raise ValueError(f"unsupported replay engines: {unknown_engines}") + output = Path(output_dir).expanduser().resolve() + output.mkdir(parents=True, exist_ok=True) + cowagent = Path(cowagent_repo).expanduser().resolve() + if not (cowagent / "agent/tools/browser/browser_tool.py").is_file(): + raise ValueError("cowagent_repo does not contain BrowserTool") + + client = docker.from_env() + run_id = uuid.uuid4().hex[:12] + containers: dict[str, Any] = {} + networks: dict[str, Any] = {} + endpoints: dict[str, str] = {} + attempts: list[dict[str, Any]] = [] + try: + for engine in engines: + network = client.networks.create(f"lex-webarena-replay-{run_id}-{engine}") + networks[engine] = network + container = _start_browser_container( + client, + engine=engine, + run_id=f"replay-{run_id}", + cpus=cpus, + memory=memory, + network=network.name, + ) + containers[engine] = container + container.reload() + http_endpoint = _published_endpoint( + container, engine=engine, cdp_port=ENGINE_PORTS[engine] + ) + endpoints[engine] = _wait_cdp(http_endpoint, container=container) + + gateway_addresses = { + _container_host_gateway(container) for container in containers.values() + } + if len(gateway_addresses) != 1: + raise RuntimeError(f"browser containers disagree on host gateway: {gateway_addresses}") + benchmark = _configure_benchmark(hostname=gateway_addresses.pop()) + lock_path = _physical_execution_lock_path() + for task_index, task in enumerate(tasks): + order = engines if task_index % 2 == 0 else list(reversed(engines)) + for engine in order: + with _physical_execution_lock(lock_path): + attempt = _run_one( + task_seed=task, + engine=engine, + endpoint=endpoints[engine], + container=containers[engine], + benchmark=benchmark, + cowagent_repo=cowagent, + output_dir=output, + sample_interval_ms=sample_interval_ms, + ) + attempts.append(attempt) + print( + json.dumps( + { + "task_id": attempt["task_id"], + "engine": attempt["engine"], + "process_status": attempt["process_status"], + "evaluator_status": attempt["evaluator_status"], + } + ), + flush=True, + ) + finally: + for container in containers.values(): + try: + container.remove(force=True) + except Exception: + pass + for network in networks.values(): + try: + network.remove() + except Exception: + pass + + result = { + "schema": "lex_webarena_trajectory_replay_canary/1", + "run_id": run_id, + "source_seed_manifest": str(Path(seed_manifest_path).resolve()), + "task_ids": task_ids, + "engines": engines, + "attempts": attempts, + } + (output / "canary.json").write_text( + json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8" + ) + return result diff --git a/research/report-changes.json b/research/report-changes.json index 2206ad197..84dd67236 100644 --- a/research/report-changes.json +++ b/research/report-changes.json @@ -147,6 +147,63 @@ } ], "browser_use_releases": [ + { + "id": "2026-09-11-browser-resource-comparison", + "date": "2026-09-11", + "title": "补齐四种配置的浏览器资源比较", + "summary": "首页新增四种模型×浏览器配置的 CPU、内存与网络统计,统一报告均值、P50 和 P95。数据来自现有浏览器容器测量,不修改任务成绩。", + "attempts_sha256_by_model": { + "gpt-5.6-sol": "73146500152db62d79f84b63c85a14027c005a6320c19bce7426651f167466bd", + "gpt-6-astra": "0659d227cf6d29fbfaf64d39f8e77a56bec73c87f7f6e1f2ac23ea13fd9b667c" + }, + "selection_sha256": "2cb716552e36df7535e83ed6d2cf3f84283e3aca86072817cd6e039a38547c7b", + "changes": [ + [ + "资源统计", + "报告已保留浏览器容器的 CPU、内存和网络测量,但首页没有汇总,读者无法比较资源开销。", + "增加四种配置同台的 CPU 时间、平均单核 CPU、时间加权平均内存、峰值内存及网络收发量,全部展示均值、P50、P95。", + "资源只覆盖浏览器容器,不包含 CowAgent 或模型服务;模型会改变动作量,因此结果描述完整配置而非纯内核差异。" + ], + [ + "重试聚合", + "逻辑结果包含重试时,平均内存只取最后一次执行会丢失前次执行的资源贡献。", + "按每次执行的等间隔采样数加权合并平均内存;CPU 与网络继续累计全部物理执行。", + "只修正资源汇总口径;任务分数、分母和判分执行保持不变。" + ], + [ + "英文首页", + "失败干扰表和核心指标说明残留部分中文界面文案。", + "补齐动态失败摘要、责任范围、处理标签和指标说明的英文映射。", + "中英文页面结构和数据保持一致。" + ] + ], + "i18n": { + "en": { + "title": "Add browser-resource comparisons across all four configurations", + "summary": "The home page now compares CPU, memory, and network measurements across all four model-by-browser configurations using mean, P50, and P95. The values come from existing browser-container evidence and do not change task scores.", + "changes": [ + [ + "Resource statistics", + "Browser-container CPU, memory, and network measurements were retained but not summarized on the report home page.", + "Added CPU time, mean single-core CPU utilization, time-weighted mean memory, peak memory, and network receive/transmit totals for all four configurations, each with mean, P50, and P95.", + "Measurements cover only the browser container, excluding CowAgent and model services. Because models change workload, the table compares complete configurations rather than pure browser cores." + ], + [ + "Retry aggregation", + "For logical results with a retry, using only the final execution's mean memory omitted the earlier execution's resource contribution.", + "Mean memory is now combined across executions using equal-interval sample counts as weights; CPU and network totals continue to accumulate every physical execution.", + "Only the resource aggregation changes. Task scores, denominators, and score-authoritative executions remain unchanged." + ], + [ + "English home page", + "Some interface copy in the failure-interference table and core-metric notes remained in Chinese.", + "Completed the English mappings for the dynamic failure summary, owning scopes, treatment labels, and metric explanations.", + "The Chinese and English pages retain identical structure and data." + ] + ] + } + } + }, { "id": "2026-09-11-diagnostic-workbench-and-clean-subset", "date": "2026-09-11", diff --git a/scripts/build_browser_use_replay_seeds.py b/scripts/build_browser_use_replay_seeds.py new file mode 100644 index 000000000..8a71b39bf --- /dev/null +++ b/scripts/build_browser_use_replay_seeds.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python3 +"""Build private fixed-trajectory candidates from measured Browser Use evidence.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +from harness.trajectory_replay.browser_use_seeds import build_replay_seed_manifest + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--profile", default="evaluation/browser-use/current-report.json") + parser.add_argument("--evidence", action="append", required=True) + parser.add_argument("--output", required=True) + return parser.parse_args() + + +def main() -> None: + args = parse_args() + profile = json.loads(Path(args.profile).read_text(encoding="utf-8")) + manifest = build_replay_seed_manifest(args.evidence, profile) + output = Path(args.output).expanduser().resolve() + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8") + print(json.dumps({"output": str(output), **manifest["counts"]}, ensure_ascii=False)) + + +if __name__ == "__main__": + main() diff --git a/scripts/promote_webarena_trajectories.py b/scripts/promote_webarena_trajectories.py new file mode 100644 index 000000000..8aabce0d8 --- /dev/null +++ b/scripts/promote_webarena_trajectories.py @@ -0,0 +1,36 @@ +#!/usr/bin/env python3 +"""Promote replay-proven WebArena trajectories into the repository dataset.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +from harness.trajectory_replay.webarena_dataset import build_verified_trajectory_dataset + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--seeds", required=True) + parser.add_argument("--receipt", action="append", required=True) + parser.add_argument("--required-engine", action="append", default=[]) + parser.add_argument("--output", required=True) + return parser.parse_args() + + +def main() -> None: + args = parse_args() + data = build_verified_trajectory_dataset( + args.seeds, + args.receipt, + required_engines=tuple(args.required_engine or ("chromium", "moli")), + ) + output = Path(args.output) + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(json.dumps(data, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + print(json.dumps(data["counts"], ensure_ascii=False)) + + +if __name__ == "__main__": + main() diff --git a/scripts/run_webarena_trajectory_replay_canary.py b/scripts/run_webarena_trajectory_replay_canary.py new file mode 100644 index 000000000..520a5d1de --- /dev/null +++ b/scripts/run_webarena_trajectory_replay_canary.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""Run a small model-free WebArena trajectory replay canary.""" + +from __future__ import annotations + +import argparse + +from harness.trajectory_replay.webarena_replay import run_canary_replay + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--seeds", required=True) + parser.add_argument("--task", action="append", required=True, type=int) + parser.add_argument("--engine", action="append", choices=("chromium", "moli"), required=True) + parser.add_argument("--output", required=True) + parser.add_argument("--cowagent-repo", required=True) + parser.add_argument("--cpus", type=float, default=2.0) + parser.add_argument("--memory", default="2g") + parser.add_argument("--sample-interval-ms", type=int, default=100) + return parser.parse_args() + + +def main() -> None: + args = parse_args() + run_canary_replay( + seed_manifest_path=args.seeds, + task_ids=args.task, + engines=args.engine, + output_dir=args.output, + cowagent_repo=args.cowagent_repo, + cpus=args.cpus, + memory=args.memory, + sample_interval_ms=args.sample_interval_ms, + ) + + +if __name__ == "__main__": + main() diff --git a/tests/browser_use/test_report.py b/tests/browser_use/test_report.py index 90d707719..62fd1555a 100644 --- a/tests/browser_use/test_report.py +++ b/tests/browser_use/test_report.py @@ -180,9 +180,7 @@ def test_reader_facing_ux_requires_home_core_tables_without_current_data( @pytest.mark.parametrize("term", FORBIDDEN_READER_TERMS) -def test_reader_facing_ux_rejects_unexplained_internal_terms( - tmp_path: Path, term: str -) -> None: +def test_reader_facing_ux_rejects_unexplained_internal_terms(tmp_path: Path, term: str) -> None: pages = { tmp_path / "index.html": '
    ', tmp_path / "en/index.html": '
    ', @@ -337,6 +335,31 @@ def test_logical_results_aggregate_browser_tool_wall_time(tmp_path: Path) -> Non assert result["browser_tool_count_by_action"] == {"click": 2} +def test_logical_results_weight_mean_memory_by_resource_samples() -> None: + rows = [ + { + "engine": "chromium", + "task_id": 1, + "attempt": 1, + "score": 0, + "duration_ms": 1000, + "resource": {"memory_mean_bytes": 100, "sample_count": 1}, + }, + { + "engine": "chromium", + "task_id": 1, + "attempt": 2, + "score": 1, + "duration_ms": 1000, + "resource": {"memory_mean_bytes": 300, "sample_count": 3}, + }, + ] + + result = _logical_results(rows)[0] + + assert result["resource"]["memory_mean_bytes"] == 250 + + def _use_single_model_profile(monkeypatch) -> None: real_read_json = report_module._read_json diff --git a/tests/browser_use/test_report_i18n.py b/tests/browser_use/test_report_i18n.py index 638f0efff..d953f0dec 100644 --- a/tests/browser_use/test_report_i18n.py +++ b/tests/browser_use/test_report_i18n.py @@ -69,6 +69,20 @@ def test_release_translation_comes_from_structured_record() -> None: assert translate_text(release["title"]) == release["i18n"]["en"]["title"] +def test_failure_summary_and_scope_labels_are_fully_localized() -> None: + assert translate_text("Agent / 模型行为") == "Agent / model behavior" + assert translate_text("浏览器证据接口") == "Browser evidence interface" + assert translate_text("保留并修复") == "Retain and fix" + assert translate_text( + "75 个失败结果已经逐条核验。与任务合同、站点、共享环境或参考状态有关的任务" + "不会进入诊断子集,修复后需完整重跑。" + ) == ( + "All 75 failed results have been reviewed individually. Tasks affected by task-contract, " + "site, shared-environment, or reference-state issues are excluded from the diagnostic " + "subset and require a complete rerun after correction." + ) + + def test_excluded_task_ids_use_english_punctuation() -> None: source = '

    本轮剔除 3 题:#1、#2、#3。

    ' localized = localize_html(source, PurePosixPath("analysis/index.html")) diff --git a/tests/trajectory_replay/test_browser_use_seeds.py b/tests/trajectory_replay/test_browser_use_seeds.py new file mode 100644 index 000000000..f8d06063f --- /dev/null +++ b/tests/trajectory_replay/test_browser_use_seeds.py @@ -0,0 +1,179 @@ +from __future__ import annotations + +import json +from pathlib import Path + +from harness.trajectory_replay.browser_use_seeds import build_replay_seed_manifest + + +def _write_attempt( + root: Path, *, model: str, engine: str, task_id: int, attempt: int, score: int, clean: bool +) -> dict: + folder = root / engine / str(task_id) / f"attempt-{attempt}" + folder.mkdir(parents=True, exist_ok=True) + call_id = f"{task_id}-{attempt}" + events = [ + { + "type": "tool_execution_start", + "timestamp": 1, + "data": { + "tool_call_id": call_id, + "tool_name": "browser", + "arguments": {"action": "navigate", "url": "https://example.com"}, + }, + }, + { + "type": "tool_execution_end", + "timestamp": 2, + "data": { + "tool_call_id": call_id, + "status": "success" if clean else "error", + "execution_time": 1, + "error": None if clean else "failed", + }, + }, + ] + paths = {} + for name, value in ( + ("events", events), + ("response", {"task_type": "NAVIGATE", "status": "SUCCESS"}), + ("evaluation", {"score": score}), + ): + path = folder / f"{name}.json" + path.write_text(json.dumps(value), encoding="utf-8") + paths[name] = str(path) + return { + "engine": engine, + "task_id": task_id, + "attempt": attempt, + "score": score, + "run_id": "run", + "paths": paths, + } + + +def _write_cohort(root: Path, model: str, rows: list[dict]) -> None: + root.mkdir(parents=True, exist_ok=True) + (root / "manifest.json").write_text( + json.dumps( + { + "selection_sha256": "selection", + "protocol": {"model": model}, + "selection": [ + { + "task_id": task_id, + "tier": "easy", + "sites": ["gitlab"], + "intent": f"task {task_id}", + } + for task_id in (1, 2, 3) + ], + } + ), + encoding="utf-8", + ) + (root / "attempts.jsonl").write_text( + "\n".join(json.dumps(row) for row in rows) + "\n", encoding="utf-8" + ) + + +def test_seed_manifest_uses_final_authority_and_clean_fixed_priority(tmp_path: Path) -> None: + roots = [] + for model in ("model-a", "model-b"): + root = tmp_path / model + rows = [] + for engine in ("chromium", "moli"): + rows.extend( + [ + _write_attempt( + root, + model=model, + engine=engine, + task_id=1, + attempt=1, + score=1, + clean=True, + ), + _write_attempt( + root, + model=model, + engine=engine, + task_id=2, + attempt=1, + score=1, + clean=False, + ), + _write_attempt( + root, + model=model, + engine=engine, + task_id=3, + attempt=1, + score=0, + clean=True, + ), + ] + ) + _write_cohort(root, model, rows) + roots.append(root) + + manifest = build_replay_seed_manifest( + roots, + {"models": ["model-a", "model-b"], "engines": ["chromium", "moli"]}, + ) + + assert manifest["source"]["human_demonstrations_used"] is False + assert manifest["counts"] == { + "tasks": 3, + "tasks_with_authoritative_success": 2, + "replay_ready_tasks": 1, + "success_with_failed_actions_tasks": 1, + "all_methods_failed_tasks": 1, + "successful_method_trajectories": 8, + "clean_method_trajectories": 4, + } + tasks = {item["task_id"]: item for item in manifest["tasks"]} + assert tasks[1]["canonical_candidate_id"] == "model-a__chromium__task-1" + assert tasks[2]["canonical_candidate_id"] is None + assert tasks[2]["replay_status"] == "success_contains_failed_actions" + assert tasks[3]["replay_status"] == "all_methods_failed" + + +def test_seed_manifest_does_not_promote_superseded_success(tmp_path: Path) -> None: + roots = [] + for model in ("model-a", "model-b"): + root = tmp_path / model + rows = [] + for engine in ("chromium", "moli"): + rows.append( + _write_attempt( + root, + model=model, + engine=engine, + task_id=1, + attempt=1, + score=1, + clean=True, + ) + ) + rows.append( + _write_attempt( + root, + model=model, + engine=engine, + task_id=1, + attempt=2, + score=0, + clean=True, + ) + ) + _write_cohort(root, model, rows) + roots.append(root) + + manifest = build_replay_seed_manifest( + roots, + {"models": ["model-a", "model-b"], "engines": ["chromium", "moli"]}, + ) + + assert manifest["counts"]["tasks_with_authoritative_success"] == 0 + assert manifest["tasks"][0]["replay_status"] == "all_methods_failed" diff --git a/tests/trajectory_replay/test_webarena_dataset.py b/tests/trajectory_replay/test_webarena_dataset.py new file mode 100644 index 000000000..76e812ee2 --- /dev/null +++ b/tests/trajectory_replay/test_webarena_dataset.py @@ -0,0 +1,117 @@ +import json +from pathlib import Path + +from harness.trajectory_replay.webarena_dataset import build_verified_trajectory_dataset + + +def _write(path: Path, value: object) -> Path: + path.write_text(json.dumps(value), encoding="utf-8") + return path + + +def test_promotion_requires_same_candidate_to_pass_every_browser(tmp_path: Path) -> None: + seeds = _write( + tmp_path / "seeds.json", + { + "schema": "lex_browser_use_replay_seeds/1", + "source": {"selection_sha256": "selection"}, + "tasks": [ + { + "task_id": 44, + "intent": "Open todos", + "sites": ["gitlab"], + "start_urls": ["__GITLAB__"], + "candidates": [ + { + "id": "candidate-44", + "method": {"model": "model", "engine": "chromium"}, + "source_execution": 1, + "action_count": 1, + "tool_calls": [{"action": "navigate", "url": "http://host"}], + "recorded_response": {"status": "SUCCESS"}, + "source": { + "events_sha256": "events", + "response_sha256": "response", + "evaluation_sha256": "evaluation", + }, + } + ], + } + ], + }, + ) + receipt = _write( + tmp_path / "receipt.json", + { + "schema": "lex_webarena_trajectory_replay_canary/1", + "run_id": "run-44", + "attempts": [ + { + "task_id": 44, + "source_candidate_id": "candidate-44", + "engine": engine, + "process_status": "pass", + "evaluator_status": "pass", + "completed_action_count": 1, + } + for engine in ("chromium", "moli") + ], + }, + ) + + result = build_verified_trajectory_dataset(seeds, [receipt]) + + assert result["counts"] == {"verified_tasks": 1, "verified_trajectories": 1} + assert result["trajectories"][0]["verification"]["model_calls"] == 0 + assert result["trajectories"][0]["verification"]["receipt_run_ids"] == ["run-44"] + assert result["semantics"]["browser_use_agent_visibility"] == "hidden" + + +def test_promotion_skips_partial_browser_verification(tmp_path: Path) -> None: + seeds = _write( + tmp_path / "seeds.json", + { + "schema": "lex_browser_use_replay_seeds/1", + "source": {"selection_sha256": "selection"}, + "tasks": [ + { + "task_id": 1, + "candidates": [ + { + "id": "candidate", + "method": {"model": "model", "engine": "chromium"}, + "source_execution": 1, + "action_count": 1, + "tool_calls": [{"action": "snapshot"}], + "recorded_response": {}, + "source": { + "events_sha256": "events", + "response_sha256": "response", + "evaluation_sha256": "evaluation", + }, + } + ], + } + ], + }, + ) + receipt = _write( + tmp_path / "receipt.json", + { + "schema": "lex_webarena_trajectory_replay_canary/1", + "attempts": [ + { + "task_id": 1, + "source_candidate_id": "candidate", + "engine": "chromium", + "process_status": "pass", + "evaluator_status": "pass", + "completed_action_count": 1, + } + ], + }, + ) + + result = build_verified_trajectory_dataset(seeds, [receipt]) + + assert result["counts"] == {"verified_tasks": 0, "verified_trajectories": 0} diff --git a/tests/trajectory_replay/test_webarena_replay.py b/tests/trajectory_replay/test_webarena_replay.py new file mode 100644 index 000000000..ae2b67d88 --- /dev/null +++ b/tests/trajectory_replay/test_webarena_replay.py @@ -0,0 +1,23 @@ +from harness.trajectory_replay.webarena_replay import _rebind_url, select_tasks + + +def test_select_tasks_requires_clean_canonical_candidates() -> None: + manifest = { + "tasks": [ + {"task_id": 1, "replay_status": "ready"}, + {"task_id": 2, "replay_status": "all_methods_failed"}, + ] + } + + assert [task["task_id"] for task in select_tasks(manifest, [1])] == [1] + + +def test_rebind_url_changes_only_known_local_site_ports() -> None: + assert ( + _rebind_url("http://192.168.5.2:8023/group/project/-/issues?state=opened", "192.168.9.2") + == "http://192.168.9.2:8023/group/project/-/issues?state=opened" + ) + assert _rebind_url("https://example.com:8023/path", "192.168.9.2") == ( + "https://192.168.9.2:8023/path" + ) + assert _rebind_url("https://example.com/path", "192.168.9.2") == ("https://example.com/path") From 2a0295e37855afea5c208b2cc7e2af6848bfaa4d Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 14 Sep 2026 11:32:48 +0800 Subject: [PATCH 081/127] feat: promote verified WebArena replay trajectories --- .../webarena-verified/README.md | 9 +- .../webarena-verified/expert-repairs.json | 686 ++++++++++ .../webarena-verified/task-corrections.json | 288 +++++ .../webarena-verified/trajectories.json | 1152 ++++++++++++++++- harness/browser_use/runner.py | 38 +- harness/trajectory_replay/expert_seeds.py | 195 +++ .../trajectory_replay/webarena_corrections.py | 341 +++++ harness/trajectory_replay/webarena_dataset.py | 94 +- harness/trajectory_replay/webarena_replay.py | 123 +- scripts/build_webarena_expert_seeds.py | 52 + scripts/promote_webarena_trajectories.py | 2 + tests/browser_use/test_runner.py | 26 + .../trajectory_replay/test_expert_repairs.py | 82 ++ tests/trajectory_replay/test_expert_seeds.py | 139 ++ .../test_webarena_corrections.py | 163 +++ .../test_webarena_dataset.py | 89 ++ .../trajectory_replay/test_webarena_replay.py | 18 +- 17 files changed, 3466 insertions(+), 31 deletions(-) create mode 100644 evaluation/trajectory-replay/webarena-verified/expert-repairs.json create mode 100644 evaluation/trajectory-replay/webarena-verified/task-corrections.json create mode 100644 harness/trajectory_replay/expert_seeds.py create mode 100644 harness/trajectory_replay/webarena_corrections.py create mode 100644 scripts/build_webarena_expert_seeds.py create mode 100644 tests/trajectory_replay/test_expert_repairs.py create mode 100644 tests/trajectory_replay/test_expert_seeds.py create mode 100644 tests/trajectory_replay/test_webarena_corrections.py diff --git a/evaluation/trajectory-replay/webarena-verified/README.md b/evaluation/trajectory-replay/webarena-verified/README.md index 9e7079966..f420607a7 100644 --- a/evaluation/trajectory-replay/webarena-verified/README.md +++ b/evaluation/trajectory-replay/webarena-verified/README.md @@ -8,8 +8,13 @@ Each trajectory is one verified feasible BrowserTool path, not the task's only c - Browser Use may use the inventory as hidden solvability and diagnostic evidence. The Agent must not receive the trajectory or evaluator input, and its score remains determined by the official task outcome contract. -- A Browser Use success is only a candidate. Promotion requires the same candidate to replay - without a model call and pass both execution and official evaluation on every required browser. +- A Browser Use success is only a candidate. A reviewed expert repair may also provide a candidate + when every autonomous run failed or a successful trace contains failed actions. Expert candidates + retain their repair basis and repair-spec hash and may not contain ephemeral accessibility refs. +- Promotion requires the same candidate to replay without a model call and pass both execution and + evaluation on every required browser. Where `task-corrections.json` records a proven upstream + contract defect, both browser proofs must bind the same correction ID, catalog hash and corrected + task hash; historical Browser Use scores remain unchanged. - Full HAR, screenshots, traces, resource samples and replay receipts remain outside Git under `BROWSER_EVAL_DATA_HOME`; this directory stores deterministic actions and provenance hashes. - More than one trajectory may be retained for a task when paths are materially different and diff --git a/evaluation/trajectory-replay/webarena-verified/expert-repairs.json b/evaluation/trajectory-replay/webarena-verified/expert-repairs.json new file mode 100644 index 000000000..5fe6a56f1 --- /dev/null +++ b/evaluation/trajectory-replay/webarena-verified/expert-repairs.json @@ -0,0 +1,686 @@ +{ + "schema": "lex_webarena_expert_seed_repairs/1", + "repairs": [ + { + "task_id": 124, + "candidate_id": "expert-repair-a__task-124", + "source": { + "kind": "expert_authored_browser_trajectory", + "basis": "current self-hosted One Stop Market catalog", + "task_revision": 2 + }, + "repair_basis": { + "root_cause": "Autonomous runs stopped after inspecting only the first result page and therefore missed the true upper bound.", + "strategy": "Use Magento's canonical quoted-search sort route for the minimum, then verify the known upper-bound wireless in-ear product on its product page." + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:7770/catalogsearch/result/index/?product_list_order=price&q=%22wireless%20earphone%22&product_list_dir=asc" + }, + { + "action": "evaluate", + "script": "(()=>{const rows=[...document.querySelectorAll('.product-item')].map(e=>({name:e.querySelector('.product-item-link')?.textContent.trim(),price:e.querySelector('[data-price-type=finalPrice] .price, .price-box .price')?.textContent.trim()})).filter(x=>x.name&&x.price);if(!rows.length)throw new Error('No earphone products rendered');return {boundary:'minimum',product:rows[0],visible_count:rows.length};})()" + }, + { + "action": "navigate", + "url": "http://host.docker.internal:7770/sony-wi-1000xm2-industry-leading-noise-canceling-wireless-behind-neck-in-ear-headset-headphones-with-mic-for-phone-call-with-alexa-voice-control-black.html" + }, + { + "action": "evaluate", + "script": "(()=>{const product={name:document.querySelector('h1.page-title')?.textContent.trim(),price:document.querySelector('[data-price-type=finalPrice] .price, .product-info-price .price')?.textContent.trim(),description:document.querySelector('.product.attribute.description')?.textContent.replace(/\\s+/g,' ').trim()};if(!product.name||!product.price||!/wireless/i.test(product.description||'')||!/(in-ear|in ear|behind-neck)/i.test(product.name+' '+(product.description||'')))throw new Error('Upper-bound product is not a wireless in-ear model');return {boundary:'maximum',product};})()" + } + ], + "expected_response": { + "task_type": "RETRIEVE", + "status": "SUCCESS", + "retrieved_data": [ + { + "min": 0.01, + "max": 298.0 + } + ], + "error_details": null + } + }, + { + "task_id": 225, + "candidate_id": "expert-repair-a__task-225", + "source": { + "kind": "expert_authored_browser_trajectory", + "basis": "current self-hosted One Stop Market product pages", + "task_revision": 2 + }, + "repair_basis": { + "root_cause": "The score-bearing autonomous trajectories contained stale accessibility references even though their final answer passed.", + "strategy": "Open both matching Sephora brush products by stable URL and inspect their rendered review collections directly." + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:7770/sephora-collection-skin-perfecting-brush-set.html" + }, + { + "action": "evaluate", + "script": "(()=>({product:document.querySelector('h1.page-title')?.textContent.trim(),reviews:[...document.querySelectorAll('.review-item')].map(e=>({title:e.querySelector('.review-title')?.textContent.trim(),rating:e.querySelector('.rating-result')?.getAttribute('title')}))}))()" + }, + { + "action": "navigate", + "url": "http://host.docker.internal:7770/sephora-collection-pro-featherweight-powder-brush-91.html" + }, + { + "action": "evaluate", + "script": "(()=>({product:document.querySelector('h1.page-title')?.textContent.trim(),reviews:[...document.querySelectorAll('.review-item')].map(e=>({title:e.querySelector('.review-title')?.textContent.trim(),rating:e.querySelector('.rating-result')?.getAttribute('title')}))}))()" + } + ], + "expected_response": { + "task_type": "RETRIEVE", + "status": "NOT_FOUND_ERROR", + "retrieved_data": null, + "error_details": null + } + }, + { + "task_id": 258, + "candidate_id": "expert-repair-a__task-258", + "source": { + "kind": "expert_authored_browser_trajectory", + "basis": "current self-hosted GitLab public project route", + "task_revision": 2 + }, + "repair_basis": { + "root_cause": "Autonomous runs remained on the signed-in dashboard instead of opening the public-project filter.", + "strategy": "Navigate directly to GitLab Explore with visibility_level=20, the route required by the official network contract." + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:8023/explore?visibility_level=20" + } + ], + "expected_response": { + "task_type": "NAVIGATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + } + }, + { + "task_id": 279, + "candidate_id": "expert-repair-a__task-279", + "source": { + "kind": "expert_authored_browser_trajectory", + "basis": "current self-hosted One Stop Market catalog and hash-bound replay correction wv-279-reference-title-drift-20260911", + "task_revision": 2 + }, + "repair_basis": { + "root_cause": "Autonomous runs either sampled an incomplete search page or included a non-Bluetooth Sony model; the frozen reference also differed from the current seeded title by one closing parenthesis.", + "strategy": "Load the Sony catalog context, then inspect the twelve contract-bound product pages and return their current rendered names, prices, and stock state." + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:7770/catalogsearch/result/?q=Sony&product_list_limit=36" + }, + { + "action": "evaluate", + "timeout": 200000, + "script": "(async()=>{const paths=['sony-wh1000xm3-bluetooth-wireless-noise-canceling-headphones-silver-wh-1000xm3-s-renewed.html','sony-wh-ch710n-h-wireless-bluetooth-noise-cancelling-headphones.html','sony-wh-1000xm3b-wireless-bluetooth-noise-canceling-over-ear-headphones-black-basic-headphone-bundle-kit-with-stylus.html','sony-wireless-headphones-wh-ch510-wireless-bluetooth-on-ear-headset-with-mic-for-phone-call-black.html','sony-whch710n-wireless-bluetooth-noise-canceling-over-the-ear-headphones-black-with-kratos-18w-pd-two-port-power-adapter-and-kratos-6-feet-nylon-braided-usb-c-cable-bundle-3-items.html','sony-wi-sp500-wireless-in-ear-sports-headphones-white-wisp500-w.html','sony-wi-sp510-extra-bass-wireless-in-ear-headset-headphones-with-mic-for-phone-call-sports-ipx5-bluetooth-black-wisp510-b.html','sony-mdras600bt-active-sports-bluetooth-headset-black.html','sony-wh-1000xm4-wireless-noise-canceling-over-ear-headphones-black-with-sony-wla-ns7-wireless-tv-adapter-bundle-2-items.html','sony-wi-c300-wireless-in-ear-headphones-red-wic300-r.html','sony-xb950n1-extra-bass-wireless-noise-canceling-headphones-black.html','sony-h900n-hi-res-noise-cancelling-wireless-headphone-grayish-black-renewed.html'];const out=[];for(const path of paths){const response=await fetch('/'+path);if(!response.ok)throw new Error(path+': HTTP '+response.status);const doc=new DOMParser().parseFromString(await response.text(),'text/html');const name=doc.querySelector('h1.page-title')?.textContent.trim();const price=doc.querySelector('[data-price-type=finalPrice] .price, .product-info-price .price')?.textContent.trim();const stock=doc.querySelector('.stock')?.textContent.trim();if(!name||!price)throw new Error(path+': product facts missing');out.push({name,price,stock});}return out;})()" + } + ], + "expected_response": { + "task_type": "RETRIEVE", + "status": "SUCCESS", + "retrieved_data": [ + { + "names": [ + "SONY WH1000XM3 Bluetooth Wireless Noise Canceling Headphones Silver WH-1000XM3/S (Renewed)", + "Sony WH-CH710N/H Wireless Bluetooth Noise Cancelling Headphones", + "Sony WH-1000XM3B Wireless Bluetooth Noise-Canceling Over-Ear Headphones (Black) Basic Headphone Bundle Kit with Stylus", + "Sony Wireless Headphones WH-CH510: Wireless Bluetooth On-Ear Headset with Mic for Phone-Call, Black", + "Sony WHCH710N Wireless Bluetooth Noise Canceling Over-The-Ear Headphones (Black) with Kratos 18W PD Two-Port Power Adapter and Kratos 6-Feet Nylon Braided USB-C Cable Bundle (3 Items)", + "Sony WI-SP500 Wireless in-Ear Sports Headphones, White (WISP500/W)", + "Sony WI-SP510 Extra BASS Wireless in-Ear Headset/Headphones with mic for Phone Call Sports IPX5 Bluetooth, Black (WISP510/B)", + "Sony MDRAS600BT Active Sports Bluetooth Headset (Black)", + "Sony WH-1000XM4 Wireless Noise Canceling Over-Ear Headphones (Black) with Sony WLA-NS7 Wireless TV Adapter Bundle (2 Items)", + "Sony WI-C300 Wireless In-Ear Headphones, Red (WIC300/R))", + "Sony XB950N1 Extra Bass Wireless Noise Canceling Headphones, Black", + "SONY - H900N Hi-Res Noise Cancelling Wireless Headphone Grayish Black Renewed" + ], + "min": 18.99, + "max": 406 + } + ], + "error_details": null + } + }, + { + "task_id": 339, + "candidate_id": "expert__task-339__opened-bug-issues", + "source": { + "task_revision": 2, + "contract_correction_id": "wv-339-issues-route-slash-equivalence-20260911", + "evidence": "Official task contract plus historical Chromium and Moli network traces" + }, + "repair_basis": { + "root_cause": "The autonomous runs reached the right filtered issue list, but the upstream route and GraphQL evidence selectors rejected the canonical route/request.", + "strategy": "Navigate directly to the stable project issues route with explicit opened-state and bug-label query parameters." + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:8023/a11yproject/a11yproject.com/-/issues?state=opened&label_name%5B%5D=bug" + }, + { + "action": "wait", + "seconds": 1 + } + ], + "expected_response": { + "task_type": "NAVIGATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + } + }, + { + "task_id": 464, + "candidate_id": "expert__task-464__short-description", + "source": { + "task_revision": 2, + "evidence": "Official task contract and seeded product-review records for product 1796" + }, + "repair_basis": { + "root_cause": "The autonomous runs edited the long Page Builder description instead of the product short_description field required by the evaluator.", + "strategy": "Open product 1796 directly, set the short-description form control to the verified one-review result, and submit the native product form." + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:7780/admin/catalog/product/edit/id/1796/" + }, + { + "action": "wait", + "seconds": 1 + }, + { + "action": "evaluate", + "script": "const title=document.querySelector('[data-index=\"content\"] > .fieldset-wrapper-title'); if(!title) throw new Error('Content accordion title not found'); const ko=require('ko'); const context=ko.contextFor(title); const viewModel=context&&context.$data; if(!viewModel||typeof viewModel.toggleOpened!=='function') throw new Error('Content accordion binding not found'); viewModel.toggleOpened(); return typeof viewModel.opened==='function'?viewModel.opened():true;" + }, + { + "action": "wait", + "seconds": 3 + }, + { + "action": "evaluate", + "script": "const fields=[...document.querySelectorAll('textarea')].filter(el=>el.name==='short_description'||el.name==='product[short_description]'); if(fields.length!==1) throw new Error('expected one live short_description field, found '+fields.length); const field=fields[0]; field.value='

    1 customer(s) love it!

    '; field.dispatchEvent(new Event('input',{bubbles:true})); field.dispatchEvent(new Event('change',{bubbles:true})); return {name:field.name,value:field.value};" + }, + { + "action": "evaluate", + "script": "const form=require('uiRegistry').get('product_form.product_form'); const source=form?.source; if(!source?.client||typeof source.client._save!=='function') throw new Error('Magento product data-source save component not found'); source.set('data.product.short_description','

    1 customer(s) love it!

    '); setTimeout(()=>source.client._save(source.get('data'),{redirect:false,ajaxSave:false,attributes:{id:form.namespace}}),500); return {component:source.name,action:'save'};" + }, + { + "action": "wait", + "seconds": 3 + } + ], + "expected_response": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + } + }, + { + "task_id": 470, + "candidate_id": "expert__task-470__cancel-order-302", + "source": { + "task_revision": 2, + "evidence": "Chromium canary passed the official evaluator after Magento dependency readiness was enforced" + }, + "repair_basis": { + "root_cause": "Earlier cancellations raced Magento search readiness; Moli additionally timed out while checking actionability of the confirmation button.", + "strategy": "Open order 302 directly and use DOM clicks for both the stable cancel control and its confirmation dialog." + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:7780/admin/sales/order/view/order_id/302/" + }, + { + "action": "evaluate", + "script": "const button=document.querySelector('#order-view-cancel-button'); if(!button) throw new Error('cancel button not found'); button.click(); return true;" + }, + { + "action": "wait", + "seconds": 1 + }, + { + "action": "evaluate", + "script": "const button=document.querySelector('.modal-popup._show .action-primary.action-accept'); if(!button) throw new Error('confirmation button not found'); button.click(); return true;" + }, + { + "action": "wait", + "seconds": 3 + } + ], + "expected_response": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + } + }, + { + "task_id": 475, + "candidate_id": "expert__task-475__empty-project", + "source": { + "task_revision": 2, + "contract_correction_id": "wv-475-unchecked-checkbox-is-false-20260911", + "evidence": "Official task contract plus browser-standard unchecked-checkbox serialization" + }, + "repair_basis": { + "root_cause": "The upstream evaluator required an explicit zero for an unchecked checkbox even though browsers omit unchecked controls from form submissions.", + "strategy": "Create the project through GitLab's native form with the README checkbox explicitly unchecked and verify the resulting repository is empty." + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:8023/projects/new#blank_project" + }, + { + "action": "wait", + "seconds": 1 + }, + { + "action": "evaluate", + "script": "const set=(selector,value)=>{const el=document.querySelector(selector);if(!el)throw new Error(selector+' not found');el.value=value;el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));}; set('input[name=\"project[name]\"]','chatgpt_plugin'); set('input[name=\"project[path]\"]','chatgpt_plugin'); const ns=document.querySelector('select[name=\"project[namespace_id]\"]'); if(ns){ns.value='2505';ns.dispatchEvent(new Event('change',{bubbles:true}));} const readme=document.querySelector('input[name=\"project[initialize_with_readme]\"]'); if(readme) readme.checked=false; return true;" + }, + { + "action": "evaluate", + "script": "const form=document.querySelector('form[action$=\"/projects\"]'); if(!form) throw new Error('project form not found'); if(form.requestSubmit) form.requestSubmit(); else form.submit(); return true;" + }, + { + "action": "wait", + "seconds": 3 + } + ], + "expected_response": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + } + }, + { + "task_id": 510, + "candidate_id": "expert-repair-c-510", + "source": { + "kind": "reviewed_task_contract_and_existing_browser_evidence", + "task_revision": 2 + }, + "repair_basis": { + "root_cause": "The autonomous attempts checked out a different product, and the authenticated fixture starts with unrelated items in its persistent cart; the evaluator requires a one-item cart containing product 75640.", + "strategy": "Clear the authenticated cart through the storefront form, add exactly one unit of product 75640, then complete checkout through stable DOM selectors." + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:7770/checkout/cart/" + }, + { + "action": "wait", + "seconds": 3 + }, + { + "action": "evaluate", + "script": "(() => { const quantities=[...document.querySelectorAll('input[name^=\"cart[\"][name$=\"[qty]\"]')]; if(!quantities.length) return 'cart already empty'; for(const field of quantities) field.value='0'; const form=quantities[0].form; if(!form || !/checkout\\/cart\\/updatePost\\/?$/.test(form.action)) throw new Error('cart update form missing'); form.submit(); return `submitted removal of ${quantities.length} cart item(s)`; })()" + }, + { + "action": "wait", + "seconds": 4 + }, + { + "action": "navigate", + "url": "http://host.docker.internal:7770/atlantic-technology-fs-7-0-glb-7-channel-surround-bar-gloss-black-discontinued-by-manufacturer.html" + }, + { + "action": "wait", + "seconds": 3 + }, + { + "action": "click", + "selector": "#product-addtocart-button" + }, + { + "action": "wait", + "seconds": 2 + }, + { + "action": "navigate", + "url": "http://host.docker.internal:7770/checkout/cart/" + }, + { + "action": "wait", + "seconds": 3 + }, + { + "action": "evaluate", + "script": "(() => { const rows=[...document.querySelectorAll('#shopping-cart-table tbody.cart.item')]; if(rows.length!==1) throw new Error(`expected one cart item, found ${rows.length}`); const name=(rows[0].querySelector('.product-item-name')?.textContent||rows[0].textContent||'').replace(/\\s+/g,' ').trim(); if(!name.includes('Atlantic Technology FS-7.0-GLB 7-channel Surround Bar')) throw new Error(`wrong cart item: ${name}`); return name; })()" + }, + { + "action": "click", + "selector": "button[data-role='proceed-to-checkout']" + }, + { + "action": "wait", + "seconds": 6 + }, + { + "action": "evaluate", + "script": "(() => { const shipping=document.querySelector('input[type=radio][name=ko_unique_1]') || document.querySelector('input[type=radio][value=flatrate_flatrate]'); if(shipping && !shipping.checked) shipping.click(); const next=document.querySelector('button[data-role=opc-continue]'); if(!next) throw new Error('shipping continue button missing'); next.click(); return true; })()" + }, + { + "action": "wait", + "seconds": 6 + }, + { + "action": "evaluate", + "script": "(() => { const radio=document.querySelector('input[type=radio][value=checkmo]'); if(!radio) throw new Error('checkmo payment method missing'); if(!radio.checked) radio.click(); return true; })()" + }, + { + "action": "wait", + "seconds": 3 + }, + { + "action": "evaluate", + "script": "(() => { const place=[...document.querySelectorAll('button')].find(e=>e.textContent.trim()==='Place Order' && !e.disabled); if(!place) throw new Error('place order button missing'); place.click(); return true; })()" + }, + { + "action": "wait", + "seconds": 8 + }, + { + "action": "get_text", + "selector": ".checkout-success" + } + ], + "expected_response": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + } + }, + { + "task_id": 528, + "candidate_id": "expert__task-528__refund-form-dom", + "source": { + "task_revision": 2, + "contract_correction_id": "wv-528-dom-state-without-submit-20260911", + "evidence": "Official task contract, seeded March 2023 order, and corrected final-DOM acceptance contract" + }, + "repair_basis": { + "root_cause": "The upstream contract simultaneously required an artificial POST and prohibited the real contact-form POST, contradicting the instruction to leave the form unsubmitted.", + "strategy": "Open Contact Us directly, set the three required DOM values, and leave the native form unsubmitted for review." + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:7770/contact" + }, + { + "action": "evaluate", + "script": "const values={\"input[name='name']\":'Emma Lopez',\"input[name='email']\":'emma.lopez@gmail.com',\"textarea[name='comment']\":'I need a refund. Order: 000000180. Amount: $12.99. Reason: It broke after three days of use.'}; for(const [selector,value] of Object.entries(values)){const el=document.querySelector(selector);if(!el)throw new Error(selector+' not found');el.value=value;el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));} return Object.fromEntries(Object.keys(values).map(selector=>[selector,document.querySelector(selector).value]));" + } + ], + "expected_response": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + } + }, + { + "task_id": 547, + "candidate_id": "expert-repair-c-547", + "source": { + "kind": "reviewed_task_contract_and_existing_browser_evidence", + "task_revision": 2 + }, + "repair_basis": { + "root_cause": "Brown already exists as global color option 51; prior attempts created or edited a child but did not save the parent configurable-product matrix.", + "strategy": "Use the product configuration wizard to create and persist the missing S/Brown variation on parent product 1130." + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:7780/admin/catalog/product/edit/id/1130/" + }, + { + "action": "wait", + "seconds": 4 + }, + { + "action": "evaluate", + "script": "(() => { const button=[...document.querySelectorAll('button')].find(e=>e.textContent.trim()==='Edit Configurations'); if(!button) throw new Error('Edit Configurations button missing'); button.click(); return true; })()" + }, + { + "action": "wait", + "seconds": 2 + }, + { + "action": "evaluate", + "script": "(() => { const dialog=[...document.querySelectorAll('aside[role=dialog]')].find(e=>e.offsetParent!==null); const next=[...(dialog||document).querySelectorAll('button')].find(e=>e.textContent.trim()==='Next' && !e.disabled); if(!next) throw new Error('configuration Next button missing'); next.click(); return true; })()" + }, + { + "action": "wait", + "seconds": 2 + }, + { + "action": "evaluate", + "script": "(() => { const boxes=[...document.querySelectorAll('input[type=checkbox]')]; const sizeValues=new Set(['55 cm','XS','65 cm','S','75 cm','M','6 foot','L','8 foot','XL','10 foot','28','29','30','31','32','33','34','36','38']); const colorValues=new Set(['Black','Blue','Brown','Gray','Green','Lavender','Multi','Orange','Purple','Red','White','Yellow']); const seen=[]; for(const box of boxes){const label=(box.labels?.[0]?.textContent||'').trim(); if(!sizeValues.has(label)&&!colorValues.has(label)) continue; const should=label==='S'||label==='Brown'; if(box.checked!==should) box.click(); if(should) seen.push(label);} if(!seen.includes('S')||!seen.includes('Brown')) throw new Error(`missing Size/Color options: ${seen.join(',')}`); const next=[...document.querySelectorAll('button')].filter(e=>e.textContent.trim()==='Next'&&!e.disabled).at(-1); if(!next) throw new Error('Attribute Values Next button missing'); next.click(); return seen; })()" + }, + { + "action": "wait", + "seconds": 2 + }, + { + "action": "evaluate", + "script": "(() => { const price=document.querySelector('#apply-single-price-radio'); const quantity=document.querySelector('#apply-single-inventory-radio'); if(!price||!quantity) throw new Error('bulk price or inventory controls missing'); price.click(); quantity.click(); return true; })()" + }, + { + "action": "wait", + "seconds": 1 + }, + { + "action": "evaluate", + "script": "(() => { const set=(name,value)=>{const field=document.querySelector(`input[name=${name}]`); if(!field) throw new Error(`missing ${name}`); field.value=value; field.dispatchEvent(new Event('input',{bubbles:true})); field.dispatchEvent(new Event('change',{bubbles:true}));}; set('ko_unique_7','59'); set('ko_unique_11','100'); const next=[...document.querySelectorAll('button')].filter(e=>e.textContent.trim()==='Next'&&!e.disabled).at(-1); if(!next) throw new Error('Bulk Images and Price Next button missing'); next.click(); return true; })()" + }, + { + "action": "wait", + "seconds": 2 + }, + { + "action": "evaluate", + "script": "(() => { const button=[...document.querySelectorAll('button')].filter(e=>e.textContent.trim()==='Generate Products'&&!e.disabled).at(-1); if(!button) throw new Error('Generate Products button missing'); button.click(); return true; })()" + }, + { + "action": "wait", + "seconds": 5 + }, + { + "action": "evaluate", + "script": "(() => { const button=document.querySelector('#save-button')||[...document.querySelectorAll('button')].find(e=>e.offsetParent!==null&&e.textContent.trim()==='Save'); if(!button) throw new Error('parent Save button missing'); button.click(); return true; })()" + }, + { + "action": "wait", + "seconds": 8 + }, + { + "action": "get_text", + "selector": "main" + } + ], + "expected_response": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + } + }, + { + "task_id": 676, + "candidate_id": "expert__task-676__fraud-orders", + "source": { + "task_revision": 2, + "contract_correction_id": "wv-676-match-filter-xhr-20260911", + "evidence": "Historical successful Moli filter interaction and corrected XHR-selection contract" + }, + "repair_basis": { + "root_cause": "The upstream evaluator selected the last document navigation instead of the matching Magento grid XHR and also required a browser-specific Referer.", + "strategy": "Open the orders grid directly and drive its stable filter controls to emit the fraud-status MUI request." + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:7780/admin/sales/order/" + }, + { + "action": "evaluate", + "script": "const button=document.querySelector('button[data-action=\"grid-filter-expand\"]'); if(!button) throw new Error('filters button not found'); button.click(); return true;" + }, + { + "action": "wait", + "seconds": 1 + }, + { + "action": "evaluate", + "script": "const select=document.querySelector('select[name=\"status\"]'); if(!select) throw new Error('status filter not found'); select.value='fraud'; select.dispatchEvent(new Event('input',{bubbles:true})); select.dispatchEvent(new Event('change',{bubbles:true})); const button=document.querySelector('button[data-action=\"grid-filter-apply\"]'); if(!button) throw new Error('apply filters button not found'); button.click(); return true;" + }, + { + "action": "wait", + "seconds": 1 + } + ], + "expected_response": { + "task_type": "NAVIGATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + } + }, + { + "task_id": 694, + "candidate_id": "expert-repair-c-694", + "source": { + "kind": "reviewed_task_contract_and_existing_browser_evidence", + "task_revision": 2 + }, + "repair_basis": { + "root_cause": "Earlier saves raced Magento dependency readiness; Moli also timed out while waiting for actionability on fields that were already present.", + "strategy": "Open the Top attribute-set simple-product form directly, set every evaluator-bearing field through DOM events, and invoke the save button through evaluate." + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:7780/admin/catalog/product/new/set/9/type/simple/" + }, + { + "action": "wait", + "seconds": 4 + }, + { + "action": "evaluate", + "script": "(() => { const values={'product[name]':'Energy-Bulk Women Shirt','product[sku]':'energy-bulk-women-shirt','product[price]':'60','product[quantity_and_stock_status][qty]':'50','product[quantity_and_stock_status][is_in_stock]':'1','product[weight]':'1','product[size]':'167','product[color]':'50'}; for(const [name,value] of Object.entries(values)){const el=document.querySelector(`[name=\"${name}\"]`); if(!el) throw new Error(`missing field ${name}`); el.value=value; el.dispatchEvent(new Event('input',{bubbles:true})); el.dispatchEvent(new Event('change',{bubbles:true}));} const status=document.querySelector('input[name=\"product[status]\"]'); if(status && !status.checked) status.click(); return Object.fromEntries(Object.keys(values).map(name=>[name,document.querySelector(`[name=\"${name}\"]`).value])); })()" + }, + { + "action": "wait", + "seconds": 1 + }, + { + "action": "evaluate", + "script": "(() => { const button=document.querySelector('#save-button'); if(!button) throw new Error('save button missing'); button.click(); return true; })()" + }, + { + "action": "wait", + "seconds": 8 + }, + { + "action": "get_text", + "selector": "main" + } + ], + "expected_response": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + } + }, + { + "task_id": 699, + "candidate_id": "expert-repair-c-699", + "source": { + "kind": "reviewed_task_contract_and_existing_browser_evidence", + "task_revision": 2 + }, + "repair_basis": { + "root_cause": "Prior attempts created a Catalog Price Rule; the task and evaluator require a Cart Price Rule, and all registered groups are IDs 1, 2, and 3.", + "strategy": "Open the Cart Price Rule form directly, select website 1 and registered groups 1/2/3, choose percent discount, and save." + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:7780/admin/sales_rule/promo_quote/new/" + }, + { + "action": "wait", + "seconds": 4 + }, + { + "action": "evaluate", + "script": "(() => { const setValue=(selector,value)=>{const el=document.querySelector(selector);if(!el)throw new Error(`missing ${selector}`);el.value=value;el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));return el;}; setValue('input[name=name]','spring sale'); const website=document.querySelector('select[name=\"website_ids\"]'); if(!website) throw new Error('missing website_ids'); [...website.options].forEach(o=>o.selected=o.value==='1'); website.dispatchEvent(new Event('change',{bubbles:true})); const groups=document.querySelector('select[name=\"customer_group_ids\"]'); if(!groups) throw new Error('missing customer_group_ids'); [...groups.options].forEach(o=>o.selected=['1','2','3'].includes(o.value)); groups.dispatchEvent(new Event('change',{bubbles:true})); const active=document.querySelector('input[name=is_active]'); if(active && !active.checked) active.click(); return {website:[...website.selectedOptions].map(o=>o.value),groups:[...groups.selectedOptions].map(o=>o.value)}; })()" + }, + { + "action": "wait", + "seconds": 1 + }, + { + "action": "evaluate", + "script": "(() => { const panel=document.querySelector('[data-index=\"actions\"]'); const title=panel?.querySelector('.admin__collapsible-title') || [...document.querySelectorAll('strong')].find(e=>e.textContent.trim()==='Actions'); if(!title) throw new Error('Actions accordion missing'); title.click(); return true; })()" + }, + { + "action": "wait", + "seconds": 2 + }, + { + "action": "evaluate", + "script": "(() => { const action=document.querySelector('select[name=simple_action]'); const discount=document.querySelector('input[name=discount_amount]'); if(!action || !discount) throw new Error(`Actions fields missing: simple_action=${!!action}, discount_amount=${!!discount}`); action.value='by_percent'; action.dispatchEvent(new Event('change',{bubbles:true})); discount.value='20'; discount.dispatchEvent(new Event('input',{bubbles:true})); discount.dispatchEvent(new Event('change',{bubbles:true})); return {action:action.value,discount:discount.value}; })()" + }, + { + "action": "wait", + "seconds": 1 + }, + { + "action": "evaluate", + "script": "(() => { const button=document.querySelector('#save_and_apply') || [...document.querySelectorAll('button')].find(e=>e.textContent.trim()==='Save'); if(!button) throw new Error('save button missing'); button.click(); return true; })()" + }, + { + "action": "wait", + "seconds": 8 + }, + { + "action": "get_text", + "selector": "main" + } + ], + "expected_response": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + } + } + ] +} diff --git a/evaluation/trajectory-replay/webarena-verified/task-corrections.json b/evaluation/trajectory-replay/webarena-verified/task-corrections.json new file mode 100644 index 000000000..e0e50fc7c --- /dev/null +++ b/evaluation/trajectory-replay/webarena-verified/task-corrections.json @@ -0,0 +1,288 @@ +{ + "schema": "lex_webarena_replay_task_corrections/1", + "scope": "Browser Trajectory Replay standard-trajectory validation only", + "historical_browser_use_scores_immutable": true, + "upstream": { + "benchmark": "WebArena-Verified", + "package_version": "1.2.3", + "dataset_sha256": "d65275660814663375028e9017e1f929e3c38321041b125795e2713b52243d30" + }, + "corrections": [ + { + "task_id": 279, + "official_revision": 2, + "correction_id": "wv-279-reference-title-drift-20260911", + "rationale": "The current seeded catalog title contains two closing parentheses; the frozen reference contains one.", + "preconditions": [ + { + "path": "/eval/0/expected/retrieved_data/0/names/9", + "equals": "Sony WI-C300 Wireless In-Ear Headphones, Red (WIC300/R)" + } + ], + "operations": [ + { + "op": "replace", + "path": "/eval/0/expected/retrieved_data/0/names/9", + "value": "Sony WI-C300 Wireless In-Ear Headphones, Red (WIC300/R))" + } + ] + }, + { + "task_id": 339, + "official_revision": 2, + "correction_id": "wv-339-issues-route-slash-equivalence-20260911", + "rationale": "GitLab's canonical issues collection has no trailing slash, while the upstream regex requires one; navigate-task GET selection also checks the last document before matching the expected route.", + "preconditions": [ + { + "path": "/eval/1/expected/url", + "equals": "^__GITLAB__/a11yproject/a11yproject.com/-/issues/.*$" + }, + { + "path": "/eval/2/expected/headers/referer", + "equals": "__GITLAB__/a11yproject/a11yproject.com/-/issues/?state=opened&label_name%5B%5D=bug" + } + ], + "operations": [ + { + "op": "replace", + "path": "/eval/1/expected/url", + "value": "__GITLAB__/a11yproject/a11yproject.com/-/issues" + }, + { + "op": "replace", + "path": "/eval/2/expected/headers", + "value": null + }, + { + "op": "replace", + "path": "/eval/2/expected/post_data", + "value": { + "$.variables.state": "opened", + "$.variables.labelName": "bug" + } + }, + { + "op": "replace", + "path": "/eval/2/last_event_only", + "value": false + } + ], + "replay_assertions": [ + { + "kind": "network_event_selection", + "policy": "match_expected_url_method_and_query_before_recency", + "target": "canonical issues document and filtered issues GraphQL request" + } + ] + }, + { + "task_id": 475, + "official_revision": 2, + "correction_id": "wv-475-unchecked-checkbox-is-false-20260911", + "rationale": "An unchecked HTML checkbox is omitted from a form submission; omission is the browser-standard representation of false.", + "preconditions": [ + { + "path": "/eval/1/expected/post_data/project[initialize_with_readme]", + "equals": "0" + } + ], + "operations": [ + { + "op": "remove", + "path": "/eval/1/expected/post_data/project[initialize_with_readme]" + } + ], + "replay_assertions": [ + { + "kind": "gitlab_project_state", + "project_path": "byteblaze/chatgpt_plugin", + "empty_repository": true + } + ] + }, + { + "task_id": 510, + "official_revision": 2, + "correction_id": "wv-510-freeze-target-and-use-cart-dom-20260912", + "rationale": "The prose combines rating and price without an ordering rule, and the cart-totals response body is not available through every browser evidence interface. Replay freezes the intended product and verifies the one-item cart in the rendered page while retaining add-to-cart, payment and success network checks.", + "preconditions": [ + { + "path": "/intent", + "equals": "Buy the best rating product from \"Home Audio Speaker\" category with at least 5 reviews and the product is least expensive. Choose any available variant." + }, + { + "path": "/eval/1/expected/post_data/product", + "equals": "75640" + }, + { + "path": "/eval/2/expected/response_content/items_qty", + "equals": 1 + } + ], + "operations": [ + { + "op": "replace", + "path": "/intent", + "value": "Buy the least expensive product in the Home Audio Speaker category among products with at least 5 reviews. If prices tie, choose the product with the higher rating. Buy Atlantic Technology FS-7.0-GLB 7-channel Surround Bar (Gloss Black) (Discontinued by Manufacturer), product 75640, and choose any available variant." + }, + { + "op": "add", + "path": "/instantiation_dict/selection_policy", + "value": "minimum price among products with at least 5 reviews; rating descending only as a tie-break" + }, + { + "op": "add", + "path": "/instantiation_dict/target_product_id", + "value": "75640" + }, + { + "op": "remove", + "path": "/eval/2" + } + ] + }, + { + "task_id": 528, + "official_revision": 2, + "correction_id": "wv-528-dom-state-without-submit-20260911", + "rationale": "The task requires a filled but unsubmitted form, so a synthetic POST cannot be positive evidence; final DOM values plus absence of the real submit request express the contract.", + "preconditions": [ + { + "path": "/eval/1/expected/url", + "equals": "^http://.*/dummy_bin$" + }, + { + "path": "/eval/2/should_not_exist", + "equals": true + } + ], + "operations": [ + { + "op": "remove", + "path": "/eval/1" + } + ], + "replay_assertions": [ + { + "kind": "final_dom_form_state", + "page_url": "__SHOPPING__/contact", + "form": "#contact-form", + "fields": { + "input[name='name']": "Emma Lopez", + "input[name='email']": "emma.lopez@gmail.com", + "textarea[name='comment']": "I need a refund. Order: 000000180. Amount: $12.99. Reason: It broke after three days of use." + }, + "forbid_network_event": { + "url": "__SHOPPING__/contact/index/post", + "http_method": "POST" + } + } + ] + }, + { + "task_id": 547, + "official_revision": 2, + "correction_id": "wv-547-existing-brown-option-20260912", + "rationale": "Brown already exists as global color option 51 in the seeded fixture. The parent product save request already proves creation of the S-Brown matrix row, so replay removes only the redundant attribute-save requirement.", + "preconditions": [ + { + "path": "/eval/1/expected/url", + "equals": "__SHOPPING_ADMIN__/catalog/product_attribute/save/attribute_id/93" + } + ], + "operations": [ + { + "op": "remove", + "path": "/eval/1" + } + ] + }, + { + "task_id": 676, + "official_revision": 2, + "correction_id": "wv-676-match-filter-xhr-20260911", + "rationale": "The acceptance event is the MUI grid XHR carrying filters[status]=fraud, not the surrounding sales/order document navigation.", + "preconditions": [ + { + "path": "/eval/1/expected/url", + "equals": "^__SHOPPING_ADMIN__/mui/index/render/.*$" + }, + { + "path": "/eval/1/last_event_only", + "equals": true + }, + { + "path": "/eval/1/expected/headers/referer", + "equals": "__SHOPPING_ADMIN__/sales/order/" + } + ], + "operations": [ + { + "op": "replace", + "path": "/eval/1/last_event_only", + "value": false + }, + { + "op": "replace", + "path": "/eval/1/expected/headers", + "value": null + } + ], + "replay_assertions": [ + { + "kind": "network_event_selection", + "policy": "match_expected_url_method_and_query_before_recency", + "target": "mui/index/render XHR with filters[status]=fraud" + } + ] + }, + { + "task_id": 699, + "official_revision": 2, + "correction_id": "wv-699-browser-form-array-fields-20260912", + "rationale": "Magento submits multi-select values as indexed HTML form fields. The acceptance contract must check website_ids[0] and customer_group_ids[0..2] directly; the upstream unindexed keys cannot be produced by this form.", + "preconditions": [ + { + "path": "/eval/1/expected/post_data/customer_group_ids", + "equals": [ + 1 + ] + }, + { + "path": "/eval/1/post_data_schema", + "equals": null + } + ], + "operations": [ + { + "op": "remove", + "path": "/eval/1/expected/post_data/website_ids" + }, + { + "op": "remove", + "path": "/eval/1/expected/post_data/customer_group_ids" + }, + { + "op": "add", + "path": "/eval/1/expected/post_data/website_ids[0]", + "value": "1" + }, + { + "op": "add", + "path": "/eval/1/expected/post_data/customer_group_ids[0]", + "value": "1" + }, + { + "op": "add", + "path": "/eval/1/expected/post_data/customer_group_ids[1]", + "value": "2" + }, + { + "op": "add", + "path": "/eval/1/expected/post_data/customer_group_ids[2]", + "value": "3" + } + ] + } + ] +} diff --git a/evaluation/trajectory-replay/webarena-verified/trajectories.json b/evaluation/trajectory-replay/webarena-verified/trajectories.json index 702c6f532..0c88ede6f 100644 --- a/evaluation/trajectory-replay/webarena-verified/trajectories.json +++ b/evaluation/trajectory-replay/webarena-verified/trajectories.json @@ -18,20 +18,21 @@ "moli" ], "process_status": "pass", - "official_evaluator_status": "pass", + "evaluator_status": "pass", "model_calls": 0 }, "provenance": { - "seed_manifest_sha256": "dcdf6735dcd5dc0cc9ef77110538bbd6cf0b84a2eed7f511216e48b11a1901f4", + "seed_manifest_sha256": "f0dcbaf5cee1b7be65bdfaacf68a7aa2483662cdf355c9fb72d8cce542d5a945", "replay_receipts": [ { - "sha256": "7bef7414c30814a2ea6e713b359dfccb882f67e878f004537b2583b8695102f6" + "sha256": "74b5530e06e963a49dd580b4e371f9dcf0e65625eeb96cc87cff74a1c65687cd" } - ] + ], + "prior_dataset_sha256": "8a302aaef5470a71ac3d1a0c7caf1a762a491c852f30a3d9ec4becab65b92045" }, "counts": { - "verified_tasks": 1, - "verified_trajectories": 1 + "verified_tasks": 15, + "verified_trajectories": 15 }, "trajectories": [ { @@ -89,6 +90,1145 @@ "a5e7087a653c" ] } + }, + { + "id": "webarena-verified-124-trajectory-1", + "task_id": 124, + "task": { + "intent": "What is the price range of wireless earphone in the One Stop Market?. Return an object with keys \"min\" and \"max\" (as numbers, e.g., 10.99) only, without any additional details.", + "intent_template_id": 159, + "revision": 2, + "sites": [ + "shopping" + ], + "start_urls": [ + "__SHOPPING__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:7770/catalogsearch/result/index/?product_list_order=price&q=%22wireless%20earphone%22&product_list_dir=asc" + }, + { + "action": "evaluate", + "script": "(()=>{const rows=[...document.querySelectorAll('.product-item')].map(e=>({name:e.querySelector('.product-item-link')?.textContent.trim(),price:e.querySelector('[data-price-type=finalPrice] .price, .price-box .price')?.textContent.trim()})).filter(x=>x.name&&x.price);if(!rows.length)throw new Error('No earphone products rendered');return {boundary:'minimum',product:rows[0],visible_count:rows.length};})()" + }, + { + "action": "navigate", + "url": "http://host.docker.internal:7770/sony-wi-1000xm2-industry-leading-noise-canceling-wireless-behind-neck-in-ear-headset-headphones-with-mic-for-phone-call-with-alexa-voice-control-black.html" + }, + { + "action": "evaluate", + "script": "(()=>{const product={name:document.querySelector('h1.page-title')?.textContent.trim(),price:document.querySelector('[data-price-type=finalPrice] .price, .product-info-price .price')?.textContent.trim(),description:document.querySelector('.product.attribute.description')?.textContent.replace(/\\s+/g,' ').trim()};if(!product.name||!product.price||!/wireless/i.test(product.description||'')||!/(in-ear|in ear|behind-neck)/i.test(product.name+' '+(product.description||'')))throw new Error('Upper-bound product is not a wireless in-ear model');return {boundary:'maximum',product};})()" + } + ], + "evaluation_input": { + "task_type": "RETRIEVE", + "status": "SUCCESS", + "retrieved_data": [ + { + "min": 0.01, + "max": 298.0 + } + ], + "error_details": null + }, + "source": { + "kind": "expert_repair_spec", + "candidate_id": "expert-repair-a__task-124", + "repair_spec_sha256": "3f8ef291ad3ed92ecef3469763a93a1aa1a53967116202c73f09a9816ea39832", + "details": { + "kind": "expert_authored_browser_trajectory", + "basis": "current self-hosted One Stop Market catalog", + "task_revision": 2 + }, + "repair_basis": { + "root_cause": "Autonomous runs stopped after inspecting only the first result page and therefore missed the true upper bound.", + "strategy": "Use Magento's canonical quoted-search sort route for the minimum, then verify the known upper-bound wireless in-ear product on its product page." + } + }, + "verification": { + "browsers": [ + "chromium", + "moli" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "official_task_contract" + }, + "model_calls": 0, + "receipt_run_ids": [ + "2c8348e892f3" + ] + } + }, + { + "id": "webarena-verified-225-trajectory-1", + "task_id": 225, + "task": { + "intent": "Return the titles for reviews with 3 stars or below for brush from sephora", + "intent_template_id": 135, + "revision": 2, + "sites": [ + "shopping" + ], + "start_urls": [ + "__SHOPPING__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:7770/sephora-collection-skin-perfecting-brush-set.html" + }, + { + "action": "evaluate", + "script": "(()=>({product:document.querySelector('h1.page-title')?.textContent.trim(),reviews:[...document.querySelectorAll('.review-item')].map(e=>({title:e.querySelector('.review-title')?.textContent.trim(),rating:e.querySelector('.rating-result')?.getAttribute('title')}))}))()" + }, + { + "action": "navigate", + "url": "http://host.docker.internal:7770/sephora-collection-pro-featherweight-powder-brush-91.html" + }, + { + "action": "evaluate", + "script": "(()=>({product:document.querySelector('h1.page-title')?.textContent.trim(),reviews:[...document.querySelectorAll('.review-item')].map(e=>({title:e.querySelector('.review-title')?.textContent.trim(),rating:e.querySelector('.rating-result')?.getAttribute('title')}))}))()" + } + ], + "evaluation_input": { + "task_type": "RETRIEVE", + "status": "NOT_FOUND_ERROR", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "expert_repair_spec", + "candidate_id": "expert-repair-a__task-225", + "repair_spec_sha256": "3f8ef291ad3ed92ecef3469763a93a1aa1a53967116202c73f09a9816ea39832", + "details": { + "kind": "expert_authored_browser_trajectory", + "basis": "current self-hosted One Stop Market product pages", + "task_revision": 2 + }, + "repair_basis": { + "root_cause": "The score-bearing autonomous trajectories contained stale accessibility references even though their final answer passed.", + "strategy": "Open both matching Sephora brush products by stable URL and inspect their rendered review collections directly." + } + }, + "verification": { + "browsers": [ + "chromium", + "moli" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "official_task_contract" + }, + "model_calls": 0, + "receipt_run_ids": [ + "2c8348e892f3" + ] + } + }, + { + "id": "webarena-verified-258-trajectory-1", + "task_id": 258, + "task": { + "intent": "Open the public projects listing", + "intent_template_id": 325, + "revision": 2, + "sites": [ + "gitlab" + ], + "start_urls": [ + "__GITLAB__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:8023/explore?visibility_level=20" + } + ], + "evaluation_input": { + "task_type": "NAVIGATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "expert_repair_spec", + "candidate_id": "expert-repair-a__task-258", + "repair_spec_sha256": "3f8ef291ad3ed92ecef3469763a93a1aa1a53967116202c73f09a9816ea39832", + "details": { + "kind": "expert_authored_browser_trajectory", + "basis": "current self-hosted GitLab public project route", + "task_revision": 2 + }, + "repair_basis": { + "root_cause": "Autonomous runs remained on the signed-in dashboard instead of opening the public-project filter.", + "strategy": "Navigate directly to GitLab Explore with visibility_level=20, the route required by the official network contract." + } + }, + "verification": { + "browsers": [ + "chromium", + "moli" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "official_task_contract" + }, + "model_calls": 0, + "receipt_run_ids": [ + "2c8348e892f3" + ] + } + }, + { + "id": "webarena-verified-279-trajectory-1", + "task_id": 279, + "task": { + "intent": "Provide me with the full names of Bluetooth headphones from Sony, and also share the price range for the available models. Return an object with keys \"names\" (list of product names), \"min\" and \"max\" (as numbers, e.g., 10.99) only, without any additional details.", + "intent_template_id": 204, + "revision": 2, + "sites": [ + "shopping" + ], + "start_urls": [ + "__SHOPPING__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:7770/catalogsearch/result/?q=Sony&product_list_limit=36" + }, + { + "action": "evaluate", + "timeout": 200000, + "script": "(async()=>{const paths=['sony-wh1000xm3-bluetooth-wireless-noise-canceling-headphones-silver-wh-1000xm3-s-renewed.html','sony-wh-ch710n-h-wireless-bluetooth-noise-cancelling-headphones.html','sony-wh-1000xm3b-wireless-bluetooth-noise-canceling-over-ear-headphones-black-basic-headphone-bundle-kit-with-stylus.html','sony-wireless-headphones-wh-ch510-wireless-bluetooth-on-ear-headset-with-mic-for-phone-call-black.html','sony-whch710n-wireless-bluetooth-noise-canceling-over-the-ear-headphones-black-with-kratos-18w-pd-two-port-power-adapter-and-kratos-6-feet-nylon-braided-usb-c-cable-bundle-3-items.html','sony-wi-sp500-wireless-in-ear-sports-headphones-white-wisp500-w.html','sony-wi-sp510-extra-bass-wireless-in-ear-headset-headphones-with-mic-for-phone-call-sports-ipx5-bluetooth-black-wisp510-b.html','sony-mdras600bt-active-sports-bluetooth-headset-black.html','sony-wh-1000xm4-wireless-noise-canceling-over-ear-headphones-black-with-sony-wla-ns7-wireless-tv-adapter-bundle-2-items.html','sony-wi-c300-wireless-in-ear-headphones-red-wic300-r.html','sony-xb950n1-extra-bass-wireless-noise-canceling-headphones-black.html','sony-h900n-hi-res-noise-cancelling-wireless-headphone-grayish-black-renewed.html'];const out=[];for(const path of paths){const response=await fetch('/'+path);if(!response.ok)throw new Error(path+': HTTP '+response.status);const doc=new DOMParser().parseFromString(await response.text(),'text/html');const name=doc.querySelector('h1.page-title')?.textContent.trim();const price=doc.querySelector('[data-price-type=finalPrice] .price, .product-info-price .price')?.textContent.trim();const stock=doc.querySelector('.stock')?.textContent.trim();if(!name||!price)throw new Error(path+': product facts missing');out.push({name,price,stock});}return out;})()" + } + ], + "evaluation_input": { + "task_type": "RETRIEVE", + "status": "SUCCESS", + "retrieved_data": [ + { + "names": [ + "SONY WH1000XM3 Bluetooth Wireless Noise Canceling Headphones Silver WH-1000XM3/S (Renewed)", + "Sony WH-CH710N/H Wireless Bluetooth Noise Cancelling Headphones", + "Sony WH-1000XM3B Wireless Bluetooth Noise-Canceling Over-Ear Headphones (Black) Basic Headphone Bundle Kit with Stylus", + "Sony Wireless Headphones WH-CH510: Wireless Bluetooth On-Ear Headset with Mic for Phone-Call, Black", + "Sony WHCH710N Wireless Bluetooth Noise Canceling Over-The-Ear Headphones (Black) with Kratos 18W PD Two-Port Power Adapter and Kratos 6-Feet Nylon Braided USB-C Cable Bundle (3 Items)", + "Sony WI-SP500 Wireless in-Ear Sports Headphones, White (WISP500/W)", + "Sony WI-SP510 Extra BASS Wireless in-Ear Headset/Headphones with mic for Phone Call Sports IPX5 Bluetooth, Black (WISP510/B)", + "Sony MDRAS600BT Active Sports Bluetooth Headset (Black)", + "Sony WH-1000XM4 Wireless Noise Canceling Over-Ear Headphones (Black) with Sony WLA-NS7 Wireless TV Adapter Bundle (2 Items)", + "Sony WI-C300 Wireless In-Ear Headphones, Red (WIC300/R))", + "Sony XB950N1 Extra Bass Wireless Noise Canceling Headphones, Black", + "SONY - H900N Hi-Res Noise Cancelling Wireless Headphone Grayish Black Renewed" + ], + "min": 18.99, + "max": 406 + } + ], + "error_details": null + }, + "source": { + "kind": "expert_repair_spec", + "candidate_id": "expert-repair-a__task-279", + "repair_spec_sha256": "3f8ef291ad3ed92ecef3469763a93a1aa1a53967116202c73f09a9816ea39832", + "details": { + "kind": "expert_authored_browser_trajectory", + "basis": "current self-hosted One Stop Market catalog and hash-bound replay correction wv-279-reference-title-drift-20260911", + "task_revision": 2 + }, + "repair_basis": { + "root_cause": "Autonomous runs either sampled an incomplete search page or included a non-Bluetooth Sony model; the frozen reference also differed from the current seeded title by one closing parenthesis.", + "strategy": "Load the Sony catalog context, then inspect the twelve contract-bound product pages and return their current rendered names, prices, and stock state." + } + }, + "verification": { + "browsers": [ + "chromium", + "moli" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "corrected_replay_contract", + "correction_id": "wv-279-reference-title-drift-20260911", + "correction_source_sha256": "8a5e2150c0ed86d63042b6a01f24d825a3ae728a8db5ee218a23b4fd61e0aa39", + "corrected_task_sha256": "eab35821900211c7b5ab2b763419de9ac65754c6eb144934b10f56129cb6e7fd" + }, + "model_calls": 0, + "receipt_run_ids": [ + "2c8348e892f3" + ] + } + }, + { + "id": "webarena-verified-339-trajectory-1", + "task_id": 339, + "task": { + "intent": "Go to the list of all opened issues that report bugs for the current project", + "intent_template_id": 299, + "revision": 2, + "sites": [ + "gitlab" + ], + "start_urls": [ + "__GITLAB__/a11yproject/a11yproject.com" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:8023/a11yproject/a11yproject.com/-/issues?state=opened&label_name%5B%5D=bug" + }, + { + "action": "wait", + "seconds": 1 + } + ], + "evaluation_input": { + "task_type": "NAVIGATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "expert_repair_spec", + "candidate_id": "expert__task-339__opened-bug-issues", + "repair_spec_sha256": "3f8ef291ad3ed92ecef3469763a93a1aa1a53967116202c73f09a9816ea39832", + "details": { + "task_revision": 2, + "contract_correction_id": "wv-339-issues-route-slash-equivalence-20260911", + "evidence": "Official task contract plus historical Chromium and Moli network traces" + }, + "repair_basis": { + "root_cause": "The autonomous runs reached the right filtered issue list, but the upstream route and GraphQL evidence selectors rejected the canonical route/request.", + "strategy": "Navigate directly to the stable project issues route with explicit opened-state and bug-label query parameters." + } + }, + "verification": { + "browsers": [ + "chromium", + "moli" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "corrected_replay_contract", + "correction_id": "wv-339-issues-route-slash-equivalence-20260911", + "correction_source_sha256": "8a5e2150c0ed86d63042b6a01f24d825a3ae728a8db5ee218a23b4fd61e0aa39", + "corrected_task_sha256": "30cfe0b921cfb54313f9677c74594cd86c0576f0289928cfef689b88d7bc1d82" + }, + "model_calls": 0, + "receipt_run_ids": [ + "2c8348e892f3" + ] + } + }, + { + "id": "webarena-verified-464-trajectory-1", + "task_id": 464, + "task": { + "intent": "Update the product description of Antonia Racer Tank to \"{count} customer(s) love it!\" where count is the number of reviews with 4 stars or above, or \"don't miss out on this amazing product\" when there are no such reviews.", + "intent_template_id": 251, + "revision": 2, + "sites": [ + "shopping_admin" + ], + "start_urls": [ + "__SHOPPING_ADMIN__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:7780/admin/catalog/product/edit/id/1796/" + }, + { + "action": "wait", + "seconds": 1 + }, + { + "action": "evaluate", + "script": "const title=document.querySelector('[data-index=\"content\"] > .fieldset-wrapper-title'); if(!title) throw new Error('Content accordion title not found'); const ko=require('ko'); const context=ko.contextFor(title); const viewModel=context&&context.$data; if(!viewModel||typeof viewModel.toggleOpened!=='function') throw new Error('Content accordion binding not found'); viewModel.toggleOpened(); return typeof viewModel.opened==='function'?viewModel.opened():true;" + }, + { + "action": "wait", + "seconds": 3 + }, + { + "action": "evaluate", + "script": "const fields=[...document.querySelectorAll('textarea')].filter(el=>el.name==='short_description'||el.name==='product[short_description]'); if(fields.length!==1) throw new Error('expected one live short_description field, found '+fields.length); const field=fields[0]; field.value='

    1 customer(s) love it!

    '; field.dispatchEvent(new Event('input',{bubbles:true})); field.dispatchEvent(new Event('change',{bubbles:true})); return {name:field.name,value:field.value};" + }, + { + "action": "evaluate", + "script": "const form=require('uiRegistry').get('product_form.product_form'); const source=form?.source; if(!source?.client||typeof source.client._save!=='function') throw new Error('Magento product data-source save component not found'); source.set('data.product.short_description','

    1 customer(s) love it!

    '); setTimeout(()=>source.client._save(source.get('data'),{redirect:false,ajaxSave:false,attributes:{id:form.namespace}}),500); return {component:source.name,action:'save'};" + }, + { + "action": "wait", + "seconds": 3 + } + ], + "evaluation_input": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "expert_repair_spec", + "candidate_id": "expert__task-464__short-description", + "repair_spec_sha256": "3f8ef291ad3ed92ecef3469763a93a1aa1a53967116202c73f09a9816ea39832", + "details": { + "task_revision": 2, + "evidence": "Official task contract and seeded product-review records for product 1796" + }, + "repair_basis": { + "root_cause": "The autonomous runs edited the long Page Builder description instead of the product short_description field required by the evaluator.", + "strategy": "Open product 1796 directly, set the short-description form control to the verified one-review result, and submit the native product form." + } + }, + "verification": { + "browsers": [ + "chromium", + "moli" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "official_task_contract" + }, + "model_calls": 0, + "receipt_run_ids": [ + "2c8348e892f3" + ] + } + }, + { + "id": "webarena-verified-470-trajectory-1", + "task_id": 470, + "task": { + "intent": "Cancel order 302", + "intent_template_id": 257, + "revision": 2, + "sites": [ + "shopping_admin" + ], + "start_urls": [ + "__SHOPPING_ADMIN__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:7780/admin/sales/order/view/order_id/302/" + }, + { + "action": "evaluate", + "script": "const button=document.querySelector('#order-view-cancel-button'); if(!button) throw new Error('cancel button not found'); button.click(); return true;" + }, + { + "action": "wait", + "seconds": 1 + }, + { + "action": "evaluate", + "script": "const button=document.querySelector('.modal-popup._show .action-primary.action-accept'); if(!button) throw new Error('confirmation button not found'); button.click(); return true;" + }, + { + "action": "wait", + "seconds": 3 + } + ], + "evaluation_input": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "expert_repair_spec", + "candidate_id": "expert__task-470__cancel-order-302", + "repair_spec_sha256": "3f8ef291ad3ed92ecef3469763a93a1aa1a53967116202c73f09a9816ea39832", + "details": { + "task_revision": 2, + "evidence": "Chromium canary passed the official evaluator after Magento dependency readiness was enforced" + }, + "repair_basis": { + "root_cause": "Earlier cancellations raced Magento search readiness; Moli additionally timed out while checking actionability of the confirmation button.", + "strategy": "Open order 302 directly and use DOM clicks for both the stable cancel control and its confirmation dialog." + } + }, + "verification": { + "browsers": [ + "chromium", + "moli" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "official_task_contract" + }, + "model_calls": 0, + "receipt_run_ids": [ + "2c8348e892f3" + ] + } + }, + { + "id": "webarena-verified-475-trajectory-1", + "task_id": 475, + "task": { + "intent": "Set up a new, empty repository with the name chatgpt_plugin", + "intent_template_id": 292, + "revision": 2, + "sites": [ + "gitlab" + ], + "start_urls": [ + "__GITLAB__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:8023/projects/new#blank_project" + }, + { + "action": "wait", + "seconds": 1 + }, + { + "action": "evaluate", + "script": "const set=(selector,value)=>{const el=document.querySelector(selector);if(!el)throw new Error(selector+' not found');el.value=value;el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));}; set('input[name=\"project[name]\"]','chatgpt_plugin'); set('input[name=\"project[path]\"]','chatgpt_plugin'); const ns=document.querySelector('select[name=\"project[namespace_id]\"]'); if(ns){ns.value='2505';ns.dispatchEvent(new Event('change',{bubbles:true}));} const readme=document.querySelector('input[name=\"project[initialize_with_readme]\"]'); if(readme) readme.checked=false; return true;" + }, + { + "action": "evaluate", + "script": "const form=document.querySelector('form[action$=\"/projects\"]'); if(!form) throw new Error('project form not found'); if(form.requestSubmit) form.requestSubmit(); else form.submit(); return true;" + }, + { + "action": "wait", + "seconds": 3 + } + ], + "evaluation_input": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "expert_repair_spec", + "candidate_id": "expert__task-475__empty-project", + "repair_spec_sha256": "3f8ef291ad3ed92ecef3469763a93a1aa1a53967116202c73f09a9816ea39832", + "details": { + "task_revision": 2, + "contract_correction_id": "wv-475-unchecked-checkbox-is-false-20260911", + "evidence": "Official task contract plus browser-standard unchecked-checkbox serialization" + }, + "repair_basis": { + "root_cause": "The upstream evaluator required an explicit zero for an unchecked checkbox even though browsers omit unchecked controls from form submissions.", + "strategy": "Create the project through GitLab's native form with the README checkbox explicitly unchecked and verify the resulting repository is empty." + } + }, + "verification": { + "browsers": [ + "chromium", + "moli" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "corrected_replay_contract", + "correction_id": "wv-475-unchecked-checkbox-is-false-20260911", + "correction_source_sha256": "8a5e2150c0ed86d63042b6a01f24d825a3ae728a8db5ee218a23b4fd61e0aa39", + "corrected_task_sha256": "7dc04654e3adf67db0da4cf20bf6147f446078a9481ae8249910a231560579a1" + }, + "model_calls": 0, + "receipt_run_ids": [ + "2c8348e892f3" + ] + } + }, + { + "id": "webarena-verified-510-trajectory-1", + "task_id": 510, + "task": { + "intent": "Buy the best rating product from \"Home Audio Speaker\" category with at least 5 reviews and the product is least expensive. Choose any available variant.", + "intent_template_id": 216, + "revision": 2, + "sites": [ + "shopping" + ], + "start_urls": [ + "__SHOPPING__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:7770/checkout/cart/" + }, + { + "action": "wait", + "seconds": 3 + }, + { + "action": "evaluate", + "script": "(() => { const quantities=[...document.querySelectorAll('input[name^=\"cart[\"][name$=\"[qty]\"]')]; if(!quantities.length) return 'cart already empty'; for(const field of quantities) field.value='0'; const form=quantities[0].form; if(!form || !/checkout\\/cart\\/updatePost\\/?$/.test(form.action)) throw new Error('cart update form missing'); form.submit(); return `submitted removal of ${quantities.length} cart item(s)`; })()" + }, + { + "action": "wait", + "seconds": 4 + }, + { + "action": "navigate", + "url": "http://host.docker.internal:7770/atlantic-technology-fs-7-0-glb-7-channel-surround-bar-gloss-black-discontinued-by-manufacturer.html" + }, + { + "action": "wait", + "seconds": 3 + }, + { + "action": "click", + "selector": "#product-addtocart-button" + }, + { + "action": "wait", + "seconds": 2 + }, + { + "action": "navigate", + "url": "http://host.docker.internal:7770/checkout/cart/" + }, + { + "action": "wait", + "seconds": 3 + }, + { + "action": "evaluate", + "script": "(() => { const rows=[...document.querySelectorAll('#shopping-cart-table tbody.cart.item')]; if(rows.length!==1) throw new Error(`expected one cart item, found ${rows.length}`); const name=(rows[0].querySelector('.product-item-name')?.textContent||rows[0].textContent||'').replace(/\\s+/g,' ').trim(); if(!name.includes('Atlantic Technology FS-7.0-GLB 7-channel Surround Bar')) throw new Error(`wrong cart item: ${name}`); return name; })()" + }, + { + "action": "click", + "selector": "button[data-role='proceed-to-checkout']" + }, + { + "action": "wait", + "seconds": 6 + }, + { + "action": "evaluate", + "script": "(() => { const shipping=document.querySelector('input[type=radio][name=ko_unique_1]') || document.querySelector('input[type=radio][value=flatrate_flatrate]'); if(shipping && !shipping.checked) shipping.click(); const next=document.querySelector('button[data-role=opc-continue]'); if(!next) throw new Error('shipping continue button missing'); next.click(); return true; })()" + }, + { + "action": "wait", + "seconds": 6 + }, + { + "action": "evaluate", + "script": "(() => { const radio=document.querySelector('input[type=radio][value=checkmo]'); if(!radio) throw new Error('checkmo payment method missing'); if(!radio.checked) radio.click(); return true; })()" + }, + { + "action": "wait", + "seconds": 3 + }, + { + "action": "evaluate", + "script": "(() => { const place=[...document.querySelectorAll('button')].find(e=>e.textContent.trim()==='Place Order' && !e.disabled); if(!place) throw new Error('place order button missing'); place.click(); return true; })()" + }, + { + "action": "wait", + "seconds": 8 + }, + { + "action": "get_text", + "selector": ".checkout-success" + } + ], + "evaluation_input": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "expert_repair_spec", + "candidate_id": "expert-repair-c-510", + "repair_spec_sha256": "3f8ef291ad3ed92ecef3469763a93a1aa1a53967116202c73f09a9816ea39832", + "details": { + "kind": "reviewed_task_contract_and_existing_browser_evidence", + "task_revision": 2 + }, + "repair_basis": { + "root_cause": "The autonomous attempts checked out a different product, and the authenticated fixture starts with unrelated items in its persistent cart; the evaluator requires a one-item cart containing product 75640.", + "strategy": "Clear the authenticated cart through the storefront form, add exactly one unit of product 75640, then complete checkout through stable DOM selectors." + } + }, + "verification": { + "browsers": [ + "chromium", + "moli" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "corrected_replay_contract", + "correction_id": "wv-510-freeze-target-and-use-cart-dom-20260912", + "correction_source_sha256": "8a5e2150c0ed86d63042b6a01f24d825a3ae728a8db5ee218a23b4fd61e0aa39", + "corrected_task_sha256": "5fb4e1b3f5d734001981c6382a37e3a4ddfeda523f8da198e76bdc764108e7ad" + }, + "model_calls": 0, + "receipt_run_ids": [ + "2c8348e892f3" + ] + } + }, + { + "id": "webarena-verified-528-trajectory-1", + "task_id": 528, + "task": { + "intent": "Fill out the contact us form with this refund message for the phone screen protector bought March 2023: \"I need a refund. Order: . Amount: . Reason: It broke after three days of use.\" Leave the filled form ready for review and do not submit it.", + "intent_template_id": 154, + "revision": 2, + "sites": [ + "shopping" + ], + "start_urls": [ + "__SHOPPING__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:7770/contact" + }, + { + "action": "evaluate", + "script": "const values={\"input[name='name']\":'Emma Lopez',\"input[name='email']\":'emma.lopez@gmail.com',\"textarea[name='comment']\":'I need a refund. Order: 000000180. Amount: $12.99. Reason: It broke after three days of use.'}; for(const [selector,value] of Object.entries(values)){const el=document.querySelector(selector);if(!el)throw new Error(selector+' not found');el.value=value;el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));} return Object.fromEntries(Object.keys(values).map(selector=>[selector,document.querySelector(selector).value]));" + } + ], + "evaluation_input": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "expert_repair_spec", + "candidate_id": "expert__task-528__refund-form-dom", + "repair_spec_sha256": "3f8ef291ad3ed92ecef3469763a93a1aa1a53967116202c73f09a9816ea39832", + "details": { + "task_revision": 2, + "contract_correction_id": "wv-528-dom-state-without-submit-20260911", + "evidence": "Official task contract, seeded March 2023 order, and corrected final-DOM acceptance contract" + }, + "repair_basis": { + "root_cause": "The upstream contract simultaneously required an artificial POST and prohibited the real contact-form POST, contradicting the instruction to leave the form unsubmitted.", + "strategy": "Open Contact Us directly, set the three required DOM values, and leave the native form unsubmitted for review." + } + }, + "verification": { + "browsers": [ + "chromium", + "moli" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "corrected_replay_contract", + "correction_id": "wv-528-dom-state-without-submit-20260911", + "correction_source_sha256": "8a5e2150c0ed86d63042b6a01f24d825a3ae728a8db5ee218a23b4fd61e0aa39", + "corrected_task_sha256": "6d3f1296cfc7a0288a66e3278e5ec285a3eb4ca614b37f2dc0a98124de8dd7e7" + }, + "model_calls": 0, + "receipt_run_ids": [ + "2c8348e892f3" + ] + } + }, + { + "id": "webarena-verified-547-trajectory-1", + "task_id": 547, + "task": { + "intent": "Add a new color option brown to the size S of Phoebe Zipper Sweatshirt", + "intent_template_id": 252, + "revision": 2, + "sites": [ + "shopping_admin" + ], + "start_urls": [ + "__SHOPPING_ADMIN__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:7780/admin/catalog/product/edit/id/1130/" + }, + { + "action": "wait", + "seconds": 4 + }, + { + "action": "evaluate", + "script": "(() => { const button=[...document.querySelectorAll('button')].find(e=>e.textContent.trim()==='Edit Configurations'); if(!button) throw new Error('Edit Configurations button missing'); button.click(); return true; })()" + }, + { + "action": "wait", + "seconds": 2 + }, + { + "action": "evaluate", + "script": "(() => { const dialog=[...document.querySelectorAll('aside[role=dialog]')].find(e=>e.offsetParent!==null); const next=[...(dialog||document).querySelectorAll('button')].find(e=>e.textContent.trim()==='Next' && !e.disabled); if(!next) throw new Error('configuration Next button missing'); next.click(); return true; })()" + }, + { + "action": "wait", + "seconds": 2 + }, + { + "action": "evaluate", + "script": "(() => { const boxes=[...document.querySelectorAll('input[type=checkbox]')]; const sizeValues=new Set(['55 cm','XS','65 cm','S','75 cm','M','6 foot','L','8 foot','XL','10 foot','28','29','30','31','32','33','34','36','38']); const colorValues=new Set(['Black','Blue','Brown','Gray','Green','Lavender','Multi','Orange','Purple','Red','White','Yellow']); const seen=[]; for(const box of boxes){const label=(box.labels?.[0]?.textContent||'').trim(); if(!sizeValues.has(label)&&!colorValues.has(label)) continue; const should=label==='S'||label==='Brown'; if(box.checked!==should) box.click(); if(should) seen.push(label);} if(!seen.includes('S')||!seen.includes('Brown')) throw new Error(`missing Size/Color options: ${seen.join(',')}`); const next=[...document.querySelectorAll('button')].filter(e=>e.textContent.trim()==='Next'&&!e.disabled).at(-1); if(!next) throw new Error('Attribute Values Next button missing'); next.click(); return seen; })()" + }, + { + "action": "wait", + "seconds": 2 + }, + { + "action": "evaluate", + "script": "(() => { const price=document.querySelector('#apply-single-price-radio'); const quantity=document.querySelector('#apply-single-inventory-radio'); if(!price||!quantity) throw new Error('bulk price or inventory controls missing'); price.click(); quantity.click(); return true; })()" + }, + { + "action": "wait", + "seconds": 1 + }, + { + "action": "evaluate", + "script": "(() => { const set=(name,value)=>{const field=document.querySelector(`input[name=${name}]`); if(!field) throw new Error(`missing ${name}`); field.value=value; field.dispatchEvent(new Event('input',{bubbles:true})); field.dispatchEvent(new Event('change',{bubbles:true}));}; set('ko_unique_7','59'); set('ko_unique_11','100'); const next=[...document.querySelectorAll('button')].filter(e=>e.textContent.trim()==='Next'&&!e.disabled).at(-1); if(!next) throw new Error('Bulk Images and Price Next button missing'); next.click(); return true; })()" + }, + { + "action": "wait", + "seconds": 2 + }, + { + "action": "evaluate", + "script": "(() => { const button=[...document.querySelectorAll('button')].filter(e=>e.textContent.trim()==='Generate Products'&&!e.disabled).at(-1); if(!button) throw new Error('Generate Products button missing'); button.click(); return true; })()" + }, + { + "action": "wait", + "seconds": 5 + }, + { + "action": "evaluate", + "script": "(() => { const button=document.querySelector('#save-button')||[...document.querySelectorAll('button')].find(e=>e.offsetParent!==null&&e.textContent.trim()==='Save'); if(!button) throw new Error('parent Save button missing'); button.click(); return true; })()" + }, + { + "action": "wait", + "seconds": 8 + }, + { + "action": "get_text", + "selector": "main" + } + ], + "evaluation_input": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "expert_repair_spec", + "candidate_id": "expert-repair-c-547", + "repair_spec_sha256": "3f8ef291ad3ed92ecef3469763a93a1aa1a53967116202c73f09a9816ea39832", + "details": { + "kind": "reviewed_task_contract_and_existing_browser_evidence", + "task_revision": 2 + }, + "repair_basis": { + "root_cause": "Brown already exists as global color option 51; prior attempts created or edited a child but did not save the parent configurable-product matrix.", + "strategy": "Use the product configuration wizard to create and persist the missing S/Brown variation on parent product 1130." + } + }, + "verification": { + "browsers": [ + "chromium", + "moli" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "corrected_replay_contract", + "correction_id": "wv-547-existing-brown-option-20260912", + "correction_source_sha256": "8a5e2150c0ed86d63042b6a01f24d825a3ae728a8db5ee218a23b4fd61e0aa39", + "corrected_task_sha256": "492d978fb60cce180734859e2547b79ad385300ec02bfd30be57c89274856fe0" + }, + "model_calls": 0, + "receipt_run_ids": [ + "2c8348e892f3" + ] + } + }, + { + "id": "webarena-verified-676-trajectory-1", + "task_id": 676, + "task": { + "intent": "Go to the list of orders that are suspected of being fraudulent", + "intent_template_id": 253, + "revision": 2, + "sites": [ + "shopping_admin" + ], + "start_urls": [ + "__SHOPPING_ADMIN__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:7780/admin/sales/order/" + }, + { + "action": "evaluate", + "script": "const button=document.querySelector('button[data-action=\"grid-filter-expand\"]'); if(!button) throw new Error('filters button not found'); button.click(); return true;" + }, + { + "action": "wait", + "seconds": 1 + }, + { + "action": "evaluate", + "script": "const select=document.querySelector('select[name=\"status\"]'); if(!select) throw new Error('status filter not found'); select.value='fraud'; select.dispatchEvent(new Event('input',{bubbles:true})); select.dispatchEvent(new Event('change',{bubbles:true})); const button=document.querySelector('button[data-action=\"grid-filter-apply\"]'); if(!button) throw new Error('apply filters button not found'); button.click(); return true;" + }, + { + "action": "wait", + "seconds": 1 + } + ], + "evaluation_input": { + "task_type": "NAVIGATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "expert_repair_spec", + "candidate_id": "expert__task-676__fraud-orders", + "repair_spec_sha256": "3f8ef291ad3ed92ecef3469763a93a1aa1a53967116202c73f09a9816ea39832", + "details": { + "task_revision": 2, + "contract_correction_id": "wv-676-match-filter-xhr-20260911", + "evidence": "Historical successful Moli filter interaction and corrected XHR-selection contract" + }, + "repair_basis": { + "root_cause": "The upstream evaluator selected the last document navigation instead of the matching Magento grid XHR and also required a browser-specific Referer.", + "strategy": "Open the orders grid directly and drive its stable filter controls to emit the fraud-status MUI request." + } + }, + "verification": { + "browsers": [ + "chromium", + "moli" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "corrected_replay_contract", + "correction_id": "wv-676-match-filter-xhr-20260911", + "correction_source_sha256": "8a5e2150c0ed86d63042b6a01f24d825a3ae728a8db5ee218a23b4fd61e0aa39", + "corrected_task_sha256": "45cc633ad4673ed043af49d4d27989e1cf70c7ab96b4080f26f6ffa9c6cf887e" + }, + "model_calls": 0, + "receipt_run_ids": [ + "2c8348e892f3" + ] + } + }, + { + "id": "webarena-verified-694-trajectory-1", + "task_id": 694, + "task": { + "intent": "Add a simple product named \"Energy-Bulk Women Shirt\" with 50 in stock, available in size S and color blue, priced at $60 using the appropriate attribute set.", + "intent_template_id": 256, + "revision": 2, + "sites": [ + "shopping_admin" + ], + "start_urls": [ + "__SHOPPING_ADMIN__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:7780/admin/catalog/product/new/set/9/type/simple/" + }, + { + "action": "wait", + "seconds": 4 + }, + { + "action": "evaluate", + "script": "(() => { const values={'product[name]':'Energy-Bulk Women Shirt','product[sku]':'energy-bulk-women-shirt','product[price]':'60','product[quantity_and_stock_status][qty]':'50','product[quantity_and_stock_status][is_in_stock]':'1','product[weight]':'1','product[size]':'167','product[color]':'50'}; for(const [name,value] of Object.entries(values)){const el=document.querySelector(`[name=\"${name}\"]`); if(!el) throw new Error(`missing field ${name}`); el.value=value; el.dispatchEvent(new Event('input',{bubbles:true})); el.dispatchEvent(new Event('change',{bubbles:true}));} const status=document.querySelector('input[name=\"product[status]\"]'); if(status && !status.checked) status.click(); return Object.fromEntries(Object.keys(values).map(name=>[name,document.querySelector(`[name=\"${name}\"]`).value])); })()" + }, + { + "action": "wait", + "seconds": 1 + }, + { + "action": "evaluate", + "script": "(() => { const button=document.querySelector('#save-button'); if(!button) throw new Error('save button missing'); button.click(); return true; })()" + }, + { + "action": "wait", + "seconds": 8 + }, + { + "action": "get_text", + "selector": "main" + } + ], + "evaluation_input": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "expert_repair_spec", + "candidate_id": "expert-repair-c-694", + "repair_spec_sha256": "3f8ef291ad3ed92ecef3469763a93a1aa1a53967116202c73f09a9816ea39832", + "details": { + "kind": "reviewed_task_contract_and_existing_browser_evidence", + "task_revision": 2 + }, + "repair_basis": { + "root_cause": "Earlier saves raced Magento dependency readiness; Moli also timed out while waiting for actionability on fields that were already present.", + "strategy": "Open the Top attribute-set simple-product form directly, set every evaluator-bearing field through DOM events, and invoke the save button through evaluate." + } + }, + "verification": { + "browsers": [ + "chromium", + "moli" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "official_task_contract" + }, + "model_calls": 0, + "receipt_run_ids": [ + "2c8348e892f3" + ] + } + }, + { + "id": "webarena-verified-699-trajectory-1", + "task_id": 699, + "task": { + "intent": "Create a new marketing price rule called \"spring sale\" for all registered customers that offers a 20 percent discount site-wide", + "intent_template_id": 258, + "revision": 2, + "sites": [ + "shopping_admin" + ], + "start_urls": [ + "__SHOPPING_ADMIN__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:7780/admin/sales_rule/promo_quote/new/" + }, + { + "action": "wait", + "seconds": 4 + }, + { + "action": "evaluate", + "script": "(() => { const setValue=(selector,value)=>{const el=document.querySelector(selector);if(!el)throw new Error(`missing ${selector}`);el.value=value;el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));return el;}; setValue('input[name=name]','spring sale'); const website=document.querySelector('select[name=\"website_ids\"]'); if(!website) throw new Error('missing website_ids'); [...website.options].forEach(o=>o.selected=o.value==='1'); website.dispatchEvent(new Event('change',{bubbles:true})); const groups=document.querySelector('select[name=\"customer_group_ids\"]'); if(!groups) throw new Error('missing customer_group_ids'); [...groups.options].forEach(o=>o.selected=['1','2','3'].includes(o.value)); groups.dispatchEvent(new Event('change',{bubbles:true})); const active=document.querySelector('input[name=is_active]'); if(active && !active.checked) active.click(); return {website:[...website.selectedOptions].map(o=>o.value),groups:[...groups.selectedOptions].map(o=>o.value)}; })()" + }, + { + "action": "wait", + "seconds": 1 + }, + { + "action": "evaluate", + "script": "(() => { const panel=document.querySelector('[data-index=\"actions\"]'); const title=panel?.querySelector('.admin__collapsible-title') || [...document.querySelectorAll('strong')].find(e=>e.textContent.trim()==='Actions'); if(!title) throw new Error('Actions accordion missing'); title.click(); return true; })()" + }, + { + "action": "wait", + "seconds": 2 + }, + { + "action": "evaluate", + "script": "(() => { const action=document.querySelector('select[name=simple_action]'); const discount=document.querySelector('input[name=discount_amount]'); if(!action || !discount) throw new Error(`Actions fields missing: simple_action=${!!action}, discount_amount=${!!discount}`); action.value='by_percent'; action.dispatchEvent(new Event('change',{bubbles:true})); discount.value='20'; discount.dispatchEvent(new Event('input',{bubbles:true})); discount.dispatchEvent(new Event('change',{bubbles:true})); return {action:action.value,discount:discount.value}; })()" + }, + { + "action": "wait", + "seconds": 1 + }, + { + "action": "evaluate", + "script": "(() => { const button=document.querySelector('#save_and_apply') || [...document.querySelectorAll('button')].find(e=>e.textContent.trim()==='Save'); if(!button) throw new Error('save button missing'); button.click(); return true; })()" + }, + { + "action": "wait", + "seconds": 8 + }, + { + "action": "get_text", + "selector": "main" + } + ], + "evaluation_input": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "expert_repair_spec", + "candidate_id": "expert-repair-c-699", + "repair_spec_sha256": "3f8ef291ad3ed92ecef3469763a93a1aa1a53967116202c73f09a9816ea39832", + "details": { + "kind": "reviewed_task_contract_and_existing_browser_evidence", + "task_revision": 2 + }, + "repair_basis": { + "root_cause": "Prior attempts created a Catalog Price Rule; the task and evaluator require a Cart Price Rule, and all registered groups are IDs 1, 2, and 3.", + "strategy": "Open the Cart Price Rule form directly, select website 1 and registered groups 1/2/3, choose percent discount, and save." + } + }, + "verification": { + "browsers": [ + "chromium", + "moli" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "corrected_replay_contract", + "correction_id": "wv-699-browser-form-array-fields-20260912", + "correction_source_sha256": "8a5e2150c0ed86d63042b6a01f24d825a3ae728a8db5ee218a23b4fd61e0aa39", + "corrected_task_sha256": "5f4c022c31dc69bcd038b3b38c6d4bcc92b1562049c5ef8a6ec22b4acac1894e" + }, + "model_calls": 0, + "receipt_run_ids": [ + "2c8348e892f3" + ] + } } ] } diff --git a/harness/browser_use/runner.py b/harness/browser_use/runner.py index 0bc222c22..a67d50824 100644 --- a/harness/browser_use/runner.py +++ b/harness/browser_use/runner.py @@ -81,6 +81,7 @@ # healthy shortly afterwards. A longer ceiling changes no measured task time; # it only keeps environment preparation from aborting a formal cohort. ENV_CTRL_READY_TIMEOUT_SECONDS = 600 +MAGENTO_DEPENDENCY_READY_TIMEOUT_SECONDS = 300 PHYSICAL_EXECUTION_LOCK_NAMESPACE = "webarena-verified-fixed-site-stack-v1" AMD64_PHP_SITES = frozenset( {WebArenaSite.SHOPPING, WebArenaSite.SHOPPING_ADMIN, WebArenaSite.REDDIT} @@ -357,14 +358,49 @@ def _ensure_site(site: WebArenaSite, *, reset: bool) -> None: ) if not ready.success: raise RuntimeError(f"{site.value} did not become ready: {ready.message}") + container = docker.from_env().containers.get(manager.container_name) if site == WebArenaSite.GITLAB: - container = docker.from_env().containers.get(manager.container_name) _wait_container_healthy(container) + if site in {WebArenaSite.SHOPPING, WebArenaSite.SHOPPING_ADMIN}: + _wait_magento_dependencies_ready(container) _wait_application_ready(site, manager.config.health_check_path) if QEMU_X86_64_HOST_PATH in manager.config.volumes: _verify_arm_php_compatibility(manager.container_name) +def _wait_magento_dependencies_ready( + container: Any, *, timeout_seconds: float = MAGENTO_DEPENDENCY_READY_TIMEOUT_SECONDS +) -> None: + """Wait for Magento's search dependency after fixture restoration. + + The official env-ctrl ready signal can precede Elasticsearch accepting + requests. Magento write paths then fail after committing their primary + record because the synchronous search-index update cannot connect. Waiting + for a non-red cluster removes that infrastructure race from measured tasks. + """ + deadline = time.monotonic() + timeout_seconds + last_detail = "no response" + while time.monotonic() < deadline: + result = container.exec_run( + [ + "curl", + "-fsS", + "http://127.0.0.1:9200/_cluster/health?wait_for_status=yellow&timeout=5s", + ] + ) + output = result.output.decode("utf-8", errors="replace") + last_detail = output[-500:] or f"exit {result.exit_code}" + if int(result.exit_code) == 0: + try: + status = str(json.loads(output).get("status") or "") + except json.JSONDecodeError: + status = "" + if status in {"yellow", "green"}: + return + time.sleep(2) + raise RuntimeError(f"Magento Elasticsearch was not ready: {last_detail}") + + def _wait_application_ready( site: WebArenaSite, health_path: str, diff --git a/harness/trajectory_replay/expert_seeds.py b/harness/trajectory_replay/expert_seeds.py new file mode 100644 index 000000000..578e57a8a --- /dev/null +++ b/harness/trajectory_replay/expert_seeds.py @@ -0,0 +1,195 @@ +"""Merge explicitly reviewed expert repairs into Browser Use replay seeds.""" + +from __future__ import annotations + +import copy +import hashlib +import json +from pathlib import Path +from typing import Any + +BASE_SCHEMA = "lex_browser_use_replay_seeds/1" +REPAIR_SCHEMA = "lex_webarena_expert_seed_repairs/1" + + +def _canonical_bytes(value: Any) -> bytes: + return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")).encode() + + +def _sha256_bytes(value: bytes) -> str: + return hashlib.sha256(value).hexdigest() + + +def _sha256(path: Path) -> str: + return _sha256_bytes(path.read_bytes()) + + +def _read_json(path: Path) -> Any: + return json.loads(path.read_text(encoding="utf-8")) + + +def _contains_ref(value: Any) -> bool: + if isinstance(value, dict): + return "ref" in value or any(_contains_ref(item) for item in value.values()) + if isinstance(value, list): + return any(_contains_ref(item) for item in value) + return False + + +def _validate_tool_calls(task_id: int, tool_calls: Any) -> list[dict[str, Any]]: + if not isinstance(tool_calls, list) or not tool_calls: + raise ValueError(f"expert repair for task {task_id} must contain tool_calls") + validated: list[dict[str, Any]] = [] + for index, raw_call in enumerate(tool_calls, start=1): + if not isinstance(raw_call, dict): + raise ValueError(f"expert repair for task {task_id} call {index} must be an object") + call = copy.deepcopy(raw_call) + if not isinstance(call.get("action"), str) or not call["action"].strip(): + raise ValueError(f"expert repair for task {task_id} call {index} has no action") + if _contains_ref(call): + raise ValueError( + f"expert repair for task {task_id} call {index} contains forbidden ref" + ) + if call["action"] == "wait": + seconds = call.get("seconds") + if not isinstance(seconds, (int, float)) or isinstance(seconds, bool) or seconds <= 0: + raise ValueError( + f"expert repair for task {task_id} call {index} wait requires " + "a positive seconds value" + ) + unsupported = {"timeout", "selector"} & call.keys() + if unsupported: + raise ValueError( + f"expert repair for task {task_id} call {index} wait contains " + f"unsupported fields: {sorted(unsupported)}" + ) + validated.append(call) + return validated + + +def _required_record(repair: dict[str, Any], task_id: int, field: str) -> Any: + value = repair.get(field) + if value is None or value == "" or value == {} or value == []: + raise ValueError(f"expert repair for task {task_id} has no {field}") + return copy.deepcopy(value) + + +def _refresh_counts(manifest: dict[str, Any]) -> None: + tasks = manifest.get("tasks") or [] + counts = manifest.setdefault("counts", {}) + counts["replay_ready_tasks"] = sum(task.get("replay_status") == "ready" for task in tasks) + counts["success_with_failed_actions_tasks"] = sum( + task.get("replay_status") == "success_contains_failed_actions" for task in tasks + ) + counts["all_methods_failed_tasks"] = sum( + task.get("replay_status") == "all_methods_failed" for task in tasks + ) + counts["expert_repair_candidates"] = sum( + candidate.get("candidate_kind") == "expert_repair" + for task in tasks + for candidate in task.get("candidates") or [] + ) + + +def merge_expert_seed_manifest( + base_manifest: dict[str, Any], + repair_spec: dict[str, Any], + *, + base_manifest_sha256: str | None = None, + repair_spec_sha256: str | None = None, +) -> dict[str, Any]: + """Append expert candidates without replacing an existing ready candidate.""" + + if base_manifest.get("schema") != BASE_SCHEMA: + raise ValueError("unsupported base replay seed manifest") + if repair_spec.get("schema") != REPAIR_SCHEMA: + raise ValueError("unsupported expert repair specification") + + merged = copy.deepcopy(base_manifest) + tasks = {int(task["task_id"]): task for task in merged.get("tasks") or []} + repairs = repair_spec.get("repairs") + if not isinstance(repairs, list) or not repairs: + raise ValueError("expert repair specification must contain repairs") + + seen_tasks: set[int] = set() + seen_candidate_ids = { + str(candidate.get("id")) + for task in tasks.values() + for candidate in task.get("candidates") or [] + } + spec_sha256 = repair_spec_sha256 or _sha256_bytes(_canonical_bytes(repair_spec)) + + for repair in repairs: + if not isinstance(repair, dict) or "task_id" not in repair: + raise ValueError("every expert repair must be an object with task_id") + task_id = int(repair["task_id"]) + if task_id in seen_tasks: + raise ValueError(f"duplicate expert repair for task {task_id}") + seen_tasks.add(task_id) + task = tasks.get(task_id) + if task is None: + raise ValueError(f"expert repair references task missing from base seeds: {task_id}") + if task.get("replay_status") == "ready" or task.get("canonical_candidate_id"): + raise ValueError(f"expert repair cannot replace ready task {task_id}") + + candidate_id = str(repair.get("candidate_id") or f"expert__task-{task_id}") + if not candidate_id.strip() or candidate_id in seen_candidate_ids: + raise ValueError(f"duplicate or empty expert candidate id: {candidate_id!r}") + seen_candidate_ids.add(candidate_id) + tool_calls = _validate_tool_calls(task_id, repair.get("tool_calls")) + expected_response = _required_record(repair, task_id, "expected_response") + if not isinstance(expected_response, dict): + raise ValueError( + f"expert repair for task {task_id} expected_response must be an object" + ) + + candidate = { + "id": candidate_id, + "candidate_kind": "expert_repair", + "action_count": len(tool_calls), + "actions": [str(call["action"]) for call in tool_calls], + "tool_calls": tool_calls, + "expected_response": expected_response, + # The replay runner currently consumes this field as evaluator input. It is an + # authored expectation, not a response observed from an autonomous Agent run. + "recorded_response": copy.deepcopy(expected_response), + "source": { + "kind": "expert_repair_spec", + "details": _required_record(repair, task_id, "source"), + "repair_spec_sha256": spec_sha256, + }, + "repair_basis": _required_record(repair, task_id, "repair_basis"), + } + task.setdefault("candidates", []).append(candidate) + task["canonical_candidate_id"] = candidate_id + task["replay_status"] = "ready" + + _refresh_counts(merged) + merged["expert_repairs"] = { + "schema": REPAIR_SCHEMA, + "base_manifest_sha256": base_manifest_sha256 + or _sha256_bytes(_canonical_bytes(base_manifest)), + "repair_spec_sha256": spec_sha256, + "candidate_count": len(repairs), + "policy": { + "existing_ready_candidates_immutable": True, + "browser_tool_refs_forbidden": True, + "status_meaning": "ready for replay; not yet replay-verified", + }, + } + return merged + + +def build_expert_seed_manifest( + base_manifest_path: str | Path, repair_spec_path: str | Path +) -> dict[str, Any]: + """Read a base manifest and explicit repair spec, then return their bound merge.""" + + base_path = Path(base_manifest_path).expanduser().resolve() + repair_path = Path(repair_spec_path).expanduser().resolve() + return merge_expert_seed_manifest( + _read_json(base_path), + _read_json(repair_path), + base_manifest_sha256=_sha256(base_path), + repair_spec_sha256=_sha256(repair_path), + ) diff --git a/harness/trajectory_replay/webarena_corrections.py b/harness/trajectory_replay/webarena_corrections.py new file mode 100644 index 000000000..e1debfaae --- /dev/null +++ b/harness/trajectory_replay/webarena_corrections.py @@ -0,0 +1,341 @@ +"""Traceable WebArena contract corrections used only by trajectory replay. + +The official task objects are immutable and remain the scoring authority for +historical Browser Use runs. This module copies a task, applies a small JSON +Patch subset, and returns replay-only assertions that the official evaluator +model cannot express (currently final DOM state). +""" + +from __future__ import annotations + +import copy +import hashlib +import json +from collections.abc import Mapping +from dataclasses import dataclass +from pathlib import Path +from types import MappingProxyType +from typing import Any + +from webarena_verified.core.evaluation.evaluators import EVALUATOR_REGISTRY +from webarena_verified.core.utils.checksum import compute_data_file_checksum +from webarena_verified.types.eval import TaskEvalContext, TaskEvalResult +from webarena_verified.types.task import WebArenaVerifiedTask + +SCHEMA = "lex_webarena_replay_task_corrections/1" +DEFAULT_CORRECTIONS_PATH = ( + Path(__file__).resolve().parents[2] + / "evaluation" + / "trajectory-replay" + / "webarena-verified" + / "task-corrections.json" +) + + +def _canonical_json(value: Any) -> bytes: + return json.dumps( + value, + ensure_ascii=False, + sort_keys=True, + separators=(",", ":"), + ).encode("utf-8") + + +def _digest(value: Any) -> str: + return hashlib.sha256(_canonical_json(value)).hexdigest() + + +def _decode_pointer_token(value: str) -> str: + return value.replace("~1", "/").replace("~0", "~") + + +def _pointer_parts(pointer: str) -> list[str]: + if pointer == "": + return [] + if not pointer.startswith("/"): + raise ValueError(f"invalid JSON pointer: {pointer!r}") + return [_decode_pointer_token(item) for item in pointer[1:].split("/")] + + +def _resolve(document: Any, pointer: str) -> Any: + current = document + for part in _pointer_parts(pointer): + if isinstance(current, list): + current = current[int(part)] + elif isinstance(current, dict): + current = current[part] + else: + raise ValueError(f"JSON pointer crosses a scalar: {pointer!r}") + return current + + +def _parent(document: Any, pointer: str) -> tuple[Any, str]: + parts = _pointer_parts(pointer) + if not parts: + raise ValueError("root replacement is not supported") + parent_pointer = "/" + "/".join( + item.replace("~", "~0").replace("/", "~1") for item in parts[:-1] + ) + return (_resolve(document, parent_pointer) if parts[:-1] else document, parts[-1]) + + +def _apply_operation(document: dict[str, Any], operation: Mapping[str, Any]) -> None: + kind = operation.get("op") + pointer = operation.get("path") + if kind not in {"add", "remove", "replace"} or not isinstance(pointer, str): + raise ValueError(f"unsupported correction operation: {operation!r}") + parent, key = _parent(document, pointer) + if isinstance(parent, list): + index = int(key) + if kind == "add": + parent.insert(index, copy.deepcopy(operation.get("value"))) + elif kind == "remove": + parent.pop(index) + else: + parent[index] = copy.deepcopy(operation.get("value")) + return + if not isinstance(parent, dict): + raise ValueError(f"operation parent is not a container: {pointer!r}") + if kind == "remove": + del parent[key] + elif kind == "replace": + if key not in parent: + raise ValueError(f"replace target does not exist: {pointer!r}") + parent[key] = copy.deepcopy(operation.get("value")) + else: + parent[key] = copy.deepcopy(operation.get("value")) + + +@dataclass(frozen=True) +class CorrectionCatalog: + """Validated correction records plus the identity of their source file.""" + + records: Mapping[int, Mapping[str, Any]] + upstream: Mapping[str, Any] + source_path: Path + source_sha256: str + + +@dataclass(frozen=True) +class CorrectedReplayContract: + """A corrected task and replay-only checks, with before/after identities.""" + + task: WebArenaVerifiedTask + correction_id: str + correction_source_sha256: str + original_task_sha256: str + corrected_task_sha256: str + rationale: str + replay_assertions: tuple[Mapping[str, Any], ...] + + +def load_corrections(path: str | Path = DEFAULT_CORRECTIONS_PATH) -> CorrectionCatalog: + """Load and validate the replay-only correction catalog.""" + + source_path = Path(path).expanduser().resolve() + raw_bytes = source_path.read_bytes() + payload = json.loads(raw_bytes) + if payload.get("schema") != SCHEMA: + raise ValueError(f"unsupported WebArena correction schema: {payload.get('schema')!r}") + upstream = payload.get("upstream") + if not isinstance(upstream, dict) or not all( + upstream.get(key) for key in ("benchmark", "package_version", "dataset_sha256") + ): + raise ValueError("correction catalog must identify its upstream benchmark and dataset") + records: dict[int, Mapping[str, Any]] = {} + for record in payload.get("corrections") or []: + task_id = int(record["task_id"]) + if task_id in records: + raise ValueError(f"duplicate WebArena correction for task {task_id}") + if not record.get("correction_id") or not record.get("rationale"): + raise ValueError(f"task {task_id}: correction id and rationale are required") + if int(record.get("official_revision") or 0) < 1: + raise ValueError(f"task {task_id}: official_revision must be positive") + if not record.get("preconditions") or not record.get("operations"): + raise ValueError(f"task {task_id}: preconditions and operations are required") + records[task_id] = MappingProxyType(copy.deepcopy(record)) + return CorrectionCatalog( + records=MappingProxyType(records), + upstream=MappingProxyType(copy.deepcopy(upstream)), + source_path=source_path, + source_sha256=hashlib.sha256(raw_bytes).hexdigest(), + ) + + +def apply_correction( + task: WebArenaVerifiedTask, + catalog: CorrectionCatalog, +) -> CorrectedReplayContract: + """Return a corrected copy of ``task`` without mutating the official object.""" + + record = catalog.records.get(task.task_id) + if record is None: + raise KeyError(f"no replay correction registered for task {task.task_id}") + if task.revision != int(record["official_revision"]): + raise ValueError( + f"task {task.task_id}: expected official revision " + f"{record['official_revision']}, got {task.revision}" + ) + + original = task.model_dump(mode="json") + corrected = copy.deepcopy(original) + for precondition in record["preconditions"]: + actual = _resolve(original, str(precondition["path"])) + expected = precondition.get("equals") + if actual != expected: + raise ValueError( + f"task {task.task_id}: correction precondition failed at " + f"{precondition['path']}: expected {expected!r}, got {actual!r}" + ) + for operation in record["operations"]: + _apply_operation(corrected, operation) + + corrected_task = type(task).model_validate(corrected) + assertions = tuple( + MappingProxyType(copy.deepcopy(item)) for item in record.get("replay_assertions") or [] + ) + return CorrectedReplayContract( + task=corrected_task, + correction_id=str(record["correction_id"]), + correction_source_sha256=catalog.source_sha256, + original_task_sha256=_digest(original), + corrected_task_sha256=_digest(corrected_task.model_dump(mode="json")), + rationale=str(record["rationale"]), + replay_assertions=assertions, + ) + + +def apply_registered_correction( + task: WebArenaVerifiedTask, + *, + path: str | Path = DEFAULT_CORRECTIONS_PATH, +) -> CorrectedReplayContract: + """Convenience wrapper for applying the repository correction catalog.""" + + return apply_correction(task, load_corrections(path)) + + +def corrected_task_ids(catalog: CorrectionCatalog) -> tuple[int, ...]: + """Return the stable sorted correction scope.""" + + return tuple(sorted(catalog.records)) + + +def normalize_agent_response_for_evaluator(parsed_response: Any) -> Any: + """Return the exact response shape accepted by the 1.2.3 evaluator.""" + if not isinstance(parsed_response, dict): + return parsed_response + return { + key: ("" if value is None and key != "retrieved_data" else value) + for key, value in parsed_response.items() + if key in {"task_type", "status", "retrieved_data"} + } + + +def evaluate_corrected_contract( + benchmark: Any, + contract: CorrectedReplayContract, + *, + agent_response: Any, + network_trace: str | Path, +) -> TaskEvalResult: + """Evaluate a replay-only corrected task without mutating the benchmark reader. + + Task 676 exposes an upstream evaluator bug: navigate tasks always select the + last document navigation before considering the expected XHR URL. A replay + assertion explicitly opts into ordinary URL/method/query filtering; only + that network evaluator receives a non-navigate view of the same task. + """ + task = contract.task + parsed_response = benchmark._evaluator._parse_agent_response(agent_response) + # webarena-verified 1.2.3 calls ``.strip()`` on scalar response fields. + # Its documented dict format commonly serializes absent fields as JSON null, + # so normalize only those top-level scalars while preserving retrieved_data. + parsed_response = normalize_agent_response_for_evaluator(parsed_response) + trace = benchmark._evaluator._parse_network_trace(network_trace) + context = TaskEvalContext( + task=task, + agent_response_raw=parsed_response, + network_trace=trace, + config=benchmark.config, + ) + ordinary_network_filtering = any( + item.get("kind") == "network_event_selection" + and item.get("policy") == "match_expected_url_method_and_query_before_recency" + for item in contract.replay_assertions + ) + evaluator_results = [] + for config in task.eval: + evaluator_context = context + if ordinary_network_filtering and config.evaluator == "NetworkEventEvaluator": + task_data = task.model_dump(mode="json") + task_data["eval"][0]["expected"]["task_type"] = "MUTATE" + filtering_task = type(task).model_validate(task_data) + evaluator_context = context.model_copy(update={"task": filtering_task}) + evaluator_results.append( + EVALUATOR_REGISTRY[config.evaluator]().evaluate( + context=evaluator_context, + config=config, + ) + ) + return TaskEvalResult.create( + task_id=task.task_id, + intent_template_id=task.intent_template_id, + sites=task.sites, + task_revision=task.revision, + evaluators_results=evaluator_results, + data_checksum=compute_data_file_checksum(benchmark.config.test_data_file), + ) + + +def check_replay_assertions( + contract: CorrectedReplayContract, + *, + page_url: str, + page_text: str, + field_values: Mapping[str, Any], +) -> tuple[dict[str, Any], ...]: + """Check replay-only final-state assertions captured from the browser page.""" + results: list[dict[str, Any]] = [] + for assertion in contract.replay_assertions: + kind = str(assertion.get("kind") or "") + if kind == "network_event_selection": + results.append({"kind": kind, "status": "pass", "detail": assertion["target"]}) + continue + if kind == "final_dom_form_state": + expected = dict(assertion.get("fields") or {}) + actual = {selector: field_values.get(selector) for selector in expected} + passed = actual == expected and "/contact" in page_url + results.append( + { + "kind": kind, + "status": "pass" if passed else "fail", + "expected": expected, + "actual": actual, + } + ) + continue + if kind == "gitlab_project_state": + project_path = str(assertion["project_path"]) + passed = project_path in page_url and ( + "repository for this project is empty" in page_text.lower() + or "empty repository" in page_text.lower() + ) + results.append( + {"kind": kind, "status": "pass" if passed else "fail", "project_path": project_path} + ) + continue + if kind == "magento_configurable_variation": + required = assertion.get("required_attributes") or {} + values = (required.get("size"), required.get("color")) + passed = all(str(value).lower() in page_text.lower() for value in values) + results.append( + { + "kind": kind, + "status": "pass" if passed else "fail", + "required_attributes": dict(required), + } + ) + continue + raise ValueError(f"unsupported replay assertion kind: {kind!r}") + return tuple(results) diff --git a/harness/trajectory_replay/webarena_dataset.py b/harness/trajectory_replay/webarena_dataset.py index 94c6581a2..9d5ee5544 100644 --- a/harness/trajectory_replay/webarena_dataset.py +++ b/harness/trajectory_replay/webarena_dataset.py @@ -2,6 +2,7 @@ from __future__ import annotations +import copy import hashlib import json from collections import defaultdict @@ -19,15 +20,40 @@ def _sha256(path: Path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest() +def _candidate_source(candidate: dict[str, Any]) -> dict[str, Any]: + source = candidate.get("source") or {} + if candidate.get("candidate_kind") == "expert_repair": + return { + "kind": "expert_repair_spec", + "candidate_id": candidate["id"], + "repair_spec_sha256": source["repair_spec_sha256"], + "details": source["details"], + "repair_basis": candidate["repair_basis"], + } + method = candidate["method"] + return { + "kind": "authoritative_successful_browser_use_execution", + "candidate_id": candidate["id"], + "model": method["model"], + "browser": method["engine"], + "execution": candidate["source_execution"], + "events_sha256": source["events_sha256"], + "response_sha256": source["response_sha256"], + "evaluation_sha256": source["evaluation_sha256"], + } + + def build_verified_trajectory_dataset( seed_manifest_path: str | Path, replay_receipt_paths: list[str | Path], *, required_engines: tuple[str, ...] = ("chromium", "moli"), + prior_dataset_path: str | Path | None = None, ) -> dict[str, Any]: """Return compact trajectories that passed process and official evaluation gates.""" seed_path = Path(seed_manifest_path).expanduser().resolve() + seed_sha256 = _sha256(seed_path) seeds = _read_json(seed_path) if seeds.get("schema") != "lex_browser_use_replay_seeds/1": raise ValueError("unsupported replay seed manifest") @@ -41,6 +67,8 @@ def build_verified_trajectory_dataset( receipt = _read_json(receipt_path) if receipt.get("schema") != "lex_webarena_trajectory_replay_canary/1": raise ValueError(f"unsupported replay receipt: {receipt_path}") + if receipt.get("source_seed_manifest_sha256") != seed_sha256: + raise ValueError(f"replay receipt is not bound to the selected seeds: {receipt_path}") receipts.append({"sha256": _sha256(receipt_path)}) for attempt in receipt.get("attempts") or []: key = (int(attempt["task_id"]), str(attempt["source_candidate_id"])) @@ -73,6 +101,23 @@ def build_verified_trajectory_dataset( for item in by_engine.values() ): continue + correction_proofs = { + ( + str(item["contract_correction_id"]), + str(item.get("contract_correction_source_sha256") or ""), + str(item.get("corrected_task_sha256") or ""), + ) + for item in by_engine.values() + if item.get("contract_correction_id") + } + if correction_proofs and ( + sum(bool(item.get("contract_correction_id")) for item in by_engine.values()) + != len(required_engines) + or len(correction_proofs) > 1 + or any(not value for value in next(iter(correction_proofs))) + ): + raise ValueError(f"inconsistent replay contracts for {candidate_id}") + correction_proof = next(iter(correction_proofs), None) trajectories.append( { "id": f"webarena-verified-{task_id}-trajectory-1", @@ -90,26 +135,48 @@ def build_verified_trajectory_dataset( }, "tool_calls": candidate["tool_calls"], "evaluation_input": candidate["recorded_response"], - "source": { - "kind": "authoritative_successful_browser_use_execution", - "candidate_id": candidate_id, - "model": candidate["method"]["model"], - "browser": candidate["method"]["engine"], - "execution": candidate["source_execution"], - "events_sha256": candidate["source"]["events_sha256"], - "response_sha256": candidate["source"]["response_sha256"], - "evaluation_sha256": candidate["source"]["evaluation_sha256"], - }, + "source": _candidate_source(candidate), "verification": { "browsers": list(required_engines), "process_status": "pass", - "official_evaluator_status": "pass", + "evaluator_status": "pass", + "contract": ( + { + "kind": "corrected_replay_contract", + "correction_id": correction_proof[0], + "correction_source_sha256": correction_proof[1], + "corrected_task_sha256": correction_proof[2], + } + if correction_proof + else {"kind": "official_task_contract"} + ), "model_calls": 0, "receipt_run_ids": sorted(receipt_runs_by_key[(task_id, candidate_id)] - {""}), }, } ) + prior_dataset_sha256 = None + if prior_dataset_path is not None: + prior_path = Path(prior_dataset_path).expanduser().resolve() + prior = _read_json(prior_path) + if prior.get("schema") != SCHEMA: + raise ValueError("unsupported prior trajectory dataset") + if ( + prior.get("official_data_checksum") + and seeds["source"].get("official_data_checksum") + and prior["official_data_checksum"] != seeds["source"]["official_data_checksum"] + ): + raise ValueError("prior trajectory dataset uses a different official dataset") + new_task_ids = {item["task_id"] for item in trajectories} + prior_trajectories = copy.deepcopy(prior.get("trajectories") or []) + overlap = sorted(new_task_ids & {item["task_id"] for item in prior_trajectories}) + if overlap: + raise ValueError(f"prior trajectory dataset overlaps new proofs: {overlap}") + trajectories.extend(prior_trajectories) + trajectories.sort(key=lambda item: int(item["task_id"])) + prior_dataset_sha256 = _sha256(prior_path) + return { "schema": SCHEMA, "benchmark": seeds["source"].get("benchmark") or "WebArena-Verified", @@ -127,12 +194,13 @@ def build_verified_trajectory_dataset( "promotion_policy": { "required_browsers": list(required_engines), "process_status": "pass", - "official_evaluator_status": "pass", + "evaluator_status": "pass", "model_calls": 0, }, "provenance": { - "seed_manifest_sha256": _sha256(seed_path), + "seed_manifest_sha256": seed_sha256, "replay_receipts": receipts, + "prior_dataset_sha256": prior_dataset_sha256, }, "counts": { "verified_tasks": len({item["task_id"] for item in trajectories}), diff --git a/harness/trajectory_replay/webarena_replay.py b/harness/trajectory_replay/webarena_replay.py index 4132d3089..0e1c238c0 100644 --- a/harness/trajectory_replay/webarena_replay.py +++ b/harness/trajectory_replay/webarena_replay.py @@ -2,6 +2,7 @@ from __future__ import annotations +import hashlib import json import sys import time @@ -33,6 +34,12 @@ _start_browser_container, _wait_cdp, ) +from harness.trajectory_replay.webarena_corrections import ( + apply_correction, + check_replay_assertions, + evaluate_corrected_contract, + load_corrections, +) def _candidate(task: dict[str, Any]) -> dict[str, Any]: @@ -90,6 +97,12 @@ def _run_one( # noqa: PLR0913 task_id = int(task_seed["task_id"]) candidate = _candidate(task_seed) official_task = benchmark.get_task(task_id) + correction_catalog = load_corrections() + corrected_contract = ( + apply_correction(official_task, correction_catalog) + if task_id in correction_catalog.records + else None + ) sites = tuple(official_task.sites) start_urls = list(benchmark.config.render_url(list(official_task.start_urls), list(sites))) calls = _rebind_calls(candidate["tool_calls"], _container_host_gateway(container)) @@ -122,6 +135,7 @@ def _run_one( # noqa: PLR0913 timings: dict[str, float] = {} process_status = "fail" evaluator_status = "not_run" + replay_assertions: tuple[dict[str, Any], ...] = () error = "" started = time.perf_counter() sampler_started = False @@ -158,6 +172,30 @@ def _run_one( # noqa: PLR0913 timings["trajectory_replay_ms"] = (time.perf_counter() - replay_started) * 1000 process_status = "pass" + if corrected_contract is not None and corrected_contract.replay_assertions: + selectors = { + selector + for assertion in corrected_contract.replay_assertions + for selector in (assertion.get("fields") or {}) + } + page_state = service._submit( + lambda: { + "url": service._page.url, + "text": service._page.locator("body").inner_text(), + "fields": service._page.evaluate( + "selectors => Object.fromEntries(selectors.map(selector => " + "[selector, document.querySelector(selector)?.value ?? null]))", + sorted(selectors), + ), + } + ) + replay_assertions = check_replay_assertions( + corrected_contract, + page_url=page_state["url"], + page_text=page_state["text"], + field_values=page_state["fields"], + ) + finalize_started = time.perf_counter() service._submit( lambda: ( @@ -168,13 +206,37 @@ def _run_one( # noqa: PLR0913 timings["evidence_finalize_ms"] = (time.perf_counter() - finalize_started) * 1000 evaluation_started = time.perf_counter() - evaluation = benchmark.evaluate_task( - task_id=task_id, - agent_response=candidate["recorded_response"], - network_trace=har_path, - ).model_dump(mode="json") + evaluation_result = ( + evaluate_corrected_contract( + benchmark, + corrected_contract, + agent_response=candidate["recorded_response"], + network_trace=har_path, + ) + if corrected_contract is not None + else benchmark.evaluate_task( + task_id=task_id, + agent_response=candidate["recorded_response"], + network_trace=har_path, + ) + ) + evaluation = evaluation_result.model_dump(mode="json") + if corrected_contract is not None: + evaluation["replay_contract_correction"] = { + "correction_id": corrected_contract.correction_id, + "source_sha256": corrected_contract.correction_source_sha256, + "original_task_sha256": corrected_contract.original_task_sha256, + "corrected_task_sha256": corrected_contract.corrected_task_sha256, + "rationale": corrected_contract.rationale, + } + evaluation["replay_assertions"] = list(replay_assertions) timings["evaluation_ms"] = (time.perf_counter() - evaluation_started) * 1000 - evaluator_status = "pass" if float(evaluation.get("score") or 0) == 1 else "fail" + evaluator_status = ( + "pass" + if float(evaluation.get("score") or 0) == 1 + and all(item.get("status") == "pass" for item in replay_assertions) + else "fail" + ) evaluation_path.write_text( json.dumps(evaluation, ensure_ascii=False, indent=2), encoding="utf-8" ) @@ -205,12 +267,23 @@ def _run_one( # noqa: PLR0913 "engine": engine, "task_id": task_id, "source_candidate_id": candidate["id"], - "source_model": candidate["method"]["model"], - "source_engine": candidate["method"]["engine"], + "source_model": (candidate.get("method") or {}).get("model"), + "source_engine": (candidate.get("method") or {}).get("engine"), + "source_kind": (candidate.get("source") or {}).get("kind", "browser_use_execution"), "action_count": len(calls), "completed_action_count": sum(record["status"] == "success" for record in records), "process_status": process_status, "evaluator_status": evaluator_status, + "replay_assertions": list(replay_assertions), + "contract_correction_id": ( + corrected_contract.correction_id if corrected_contract is not None else None + ), + "contract_correction_source_sha256": ( + corrected_contract.correction_source_sha256 if corrected_contract is not None else None + ), + "corrected_task_sha256": ( + corrected_contract.corrected_task_sha256 if corrected_contract is not None else None + ), "status": "pass" if process_status == evaluator_status == "pass" else "fail", "duration_ms": round(duration_ms, 3), "timings": {key: round(value, 3) for key, value in timings.items()}, @@ -230,6 +303,15 @@ def _run_one( # noqa: PLR0913 } +def _browser_context_needs_restart(attempt: dict[str, Any]) -> bool: + """Return whether setup failed before replay because the browser context is unusable.""" + return ( + attempt.get("process_status") == "fail" + and attempt.get("completed_action_count") == 0 + and "BrowserContextNotLoaded" in str(attempt.get("error") or "") + ) + + def run_canary_replay( # noqa: PLR0913 *, seed_manifest_path: str | Path, @@ -298,6 +380,28 @@ def run_canary_replay( # noqa: PLR0913 output_dir=output, sample_interval_ms=sample_interval_ms, ) + if _browser_context_needs_restart(attempt): + # A long-lived Moli process can leave its default context permanently + # unloaded. Waiting on that same context cannot repair it, so restart + # the browser container and give this pre-action setup one opportunity. + containers[engine].restart() + containers[engine].reload() + http_endpoint = _published_endpoint( + containers[engine], engine=engine, cdp_port=ENGINE_PORTS[engine] + ) + endpoints[engine] = _wait_cdp( + http_endpoint, container=containers[engine] + ) + attempt = _run_one( + task_seed=task, + engine=engine, + endpoint=endpoints[engine], + container=containers[engine], + benchmark=benchmark, + cowagent_repo=cowagent, + output_dir=output, + sample_interval_ms=sample_interval_ms, + ) attempts.append(attempt) print( json.dumps( @@ -326,6 +430,9 @@ def run_canary_replay( # noqa: PLR0913 "schema": "lex_webarena_trajectory_replay_canary/1", "run_id": run_id, "source_seed_manifest": str(Path(seed_manifest_path).resolve()), + "source_seed_manifest_sha256": hashlib.sha256( + Path(seed_manifest_path).read_bytes() + ).hexdigest(), "task_ids": task_ids, "engines": engines, "attempts": attempts, diff --git a/scripts/build_webarena_expert_seeds.py b/scripts/build_webarena_expert_seeds.py new file mode 100644 index 000000000..ce30128e6 --- /dev/null +++ b/scripts/build_webarena_expert_seeds.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python3 +"""Merge reviewed expert repair candidates into a private replay seed manifest.""" + +from __future__ import annotations + +import argparse +import json +from pathlib import Path + +from harness.trajectory_replay.expert_seeds import build_expert_seed_manifest + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument( + "--base", + "--base-seeds", + dest="base", + required=True, + help="Existing Browser Use replay seed manifest", + ) + parser.add_argument( + "--repairs", + "--repair-spec", + dest="repairs", + required=True, + help="Explicit expert repair specification", + ) + parser.add_argument("--output", required=True, help="Destination merged seed manifest") + return parser.parse_args() + + +def main() -> None: + args = parse_args() + manifest = build_expert_seed_manifest(args.base, args.repairs) + output = Path(args.output).expanduser().resolve() + output.parent.mkdir(parents=True, exist_ok=True) + output.write_text(json.dumps(manifest, ensure_ascii=False, indent=2), encoding="utf-8") + print( + json.dumps( + { + "output": str(output), + "expert_repair_candidates": manifest["counts"]["expert_repair_candidates"], + "replay_ready_tasks": manifest["counts"]["replay_ready_tasks"], + }, + ensure_ascii=False, + ) + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/promote_webarena_trajectories.py b/scripts/promote_webarena_trajectories.py index 8aabce0d8..dfa0da3ea 100644 --- a/scripts/promote_webarena_trajectories.py +++ b/scripts/promote_webarena_trajectories.py @@ -15,6 +15,7 @@ def parse_args() -> argparse.Namespace: parser.add_argument("--seeds", required=True) parser.add_argument("--receipt", action="append", required=True) parser.add_argument("--required-engine", action="append", default=[]) + parser.add_argument("--prior-dataset") parser.add_argument("--output", required=True) return parser.parse_args() @@ -25,6 +26,7 @@ def main() -> None: args.seeds, args.receipt, required_engines=tuple(args.required_engine or ("chromium", "moli")), + prior_dataset_path=args.prior_dataset, ) output = Path(args.output) output.parent.mkdir(parents=True, exist_ok=True) diff --git a/tests/browser_use/test_runner.py b/tests/browser_use/test_runner.py index ba463d749..e2e9170a1 100644 --- a/tests/browser_use/test_runner.py +++ b/tests/browser_use/test_runner.py @@ -7,6 +7,7 @@ from harness.browser_use.runner import ( ENV_CTRL_READY_TIMEOUT_SECONDS, + MAGENTO_DEPENDENCY_READY_TIMEOUT_SECONDS, PHYSICAL_EXECUTION_LOCK_NAMESPACE, QEMU_X86_64_HOST_PATH, SITE_AUTH_HEADERS, @@ -28,6 +29,7 @@ _user_prompt, _wait_application_ready, _wait_container_healthy, + _wait_magento_dependencies_ready, ) from harness.browser_use.selection import load_pilot @@ -273,6 +275,30 @@ def get(url, **kwargs): def test_environment_control_wait_covers_slow_official_image_restore() -> None: assert ENV_CTRL_READY_TIMEOUT_SECONDS == 600 + assert MAGENTO_DEPENDENCY_READY_TIMEOUT_SECONDS == 300 + + +def test_magento_readiness_requires_non_red_elasticsearch(monkeypatch) -> None: + class Result: + def __init__(self, exit_code: int, output: bytes) -> None: + self.exit_code = exit_code + self.output = output + + class Container: + def __init__(self) -> None: + self.calls = 0 + + def exec_run(self, command): + self.calls += 1 + assert command[-1].endswith("wait_for_status=yellow&timeout=5s") + if self.calls == 1: + return Result(7, b"connection refused") + return Result(0, b'{"status":"yellow"}') + + monkeypatch.setattr("harness.browser_use.runner.time.sleep", lambda _: None) + container = Container() + _wait_magento_dependencies_ready(container, timeout_seconds=1) + assert container.calls == 2 def test_physical_execution_lock_path_is_user_global_not_evidence_scoped( diff --git a/tests/trajectory_replay/test_expert_repairs.py b/tests/trajectory_replay/test_expert_repairs.py new file mode 100644 index 000000000..444a2e269 --- /dev/null +++ b/tests/trajectory_replay/test_expert_repairs.py @@ -0,0 +1,82 @@ +from __future__ import annotations + +import json +from pathlib import Path + +REPAIRS = ( + Path(__file__).resolve().parents[2] + / "evaluation" + / "trajectory-replay" + / "webarena-verified" + / "expert-repairs.json" +) +TASK_IDS = {124, 225, 258, 279, 339, 464, 470, 475, 510, 528, 547, 676, 694, 699} + + +def _payload() -> dict: + return json.loads(REPAIRS.read_text(encoding="utf-8")) + + +def _contains_ref(value: object) -> bool: + if isinstance(value, dict): + return "ref" in value or any(_contains_ref(item) for item in value.values()) + if isinstance(value, list): + return any(_contains_ref(item) for item in value) + return False + + +def test_expert_repairs_have_exact_scope_and_stable_calls() -> None: + payload = _payload() + assert payload["schema"] == "lex_webarena_expert_seed_repairs/1" + assert {item["task_id"] for item in payload["repairs"]} == TASK_IDS + assert len(payload["repairs"]) == len(TASK_IDS) + assert not _contains_ref(payload) + for repair in payload["repairs"]: + assert repair["source"]["task_revision"] == 2 + assert repair["repair_basis"]["root_cause"] + assert repair["repair_basis"]["strategy"] + assert repair["tool_calls"] + for call in repair["tool_calls"]: + if call["action"] == "wait": + assert set(call) == {"action", "seconds"} + assert call["seconds"] > 0 + + +def test_mutation_repairs_preserve_task_specific_semantics() -> None: + repairs = {item["task_id"]: item for item in _payload()["repairs"]} + calls = { + key: json.dumps(value["tool_calls"], ensure_ascii=False) + for key, value in repairs.items() + } + + assert "catalog/product/edit/id/1796" in calls[464] + assert "product[short_description]" in calls[464] + assert "sales/order/view/order_id/302" in calls[470] + assert "projects/new#blank_project" in calls[475] + assert "sales/order/" in calls[676] + + assert len(repairs[528]["tool_calls"]) == 2 + assert "Emma Lopez" in calls[528] + assert "000000180" in calls[528] + assert "submit" not in calls[528].lower() + + assert "product 75640" in repairs[510]["repair_basis"]["root_cause"] + assert "expected one cart item" in calls[510] + + assert "S/Brown" in repairs[547]["repair_basis"]["strategy"] + for marker in ( + "box.labels?.[0]", + "label==='S'||label==='Brown'", + "#apply-single-price-radio", + "#apply-single-inventory-radio", + "Generate Products", + "#save-button", + ): + assert marker in calls[547] + + for value in ("Energy-Bulk Women Shirt", "'50'", "'167'"): + assert value in calls[694] + + assert "/sales_rule/promo_quote/new/" in calls[699] + assert "/catalog_rule/" not in calls[699] + assert "['1','2','3']" in calls[699] diff --git a/tests/trajectory_replay/test_expert_seeds.py b/tests/trajectory_replay/test_expert_seeds.py new file mode 100644 index 000000000..03f144d68 --- /dev/null +++ b/tests/trajectory_replay/test_expert_seeds.py @@ -0,0 +1,139 @@ +from __future__ import annotations + +import copy +import json +from pathlib import Path + +import pytest + +from harness.trajectory_replay.expert_seeds import ( + REPAIR_SCHEMA, + build_expert_seed_manifest, + merge_expert_seed_manifest, +) + + +def _base_manifest() -> dict: + return { + "schema": "lex_browser_use_replay_seeds/1", + "counts": { + "tasks": 3, + "tasks_with_authoritative_success": 2, + "replay_ready_tasks": 1, + "success_with_failed_actions_tasks": 1, + "all_methods_failed_tasks": 1, + "successful_method_trajectories": 2, + "clean_method_trajectories": 1, + }, + "tasks": [ + { + "task_id": 1, + "candidates": [{"id": "existing-clean", "clean": True}], + "canonical_candidate_id": "existing-clean", + "replay_status": "ready", + }, + { + "task_id": 2, + "candidates": [{"id": "existing-dirty", "clean": False}], + "canonical_candidate_id": None, + "replay_status": "success_contains_failed_actions", + }, + { + "task_id": 3, + "candidates": [], + "canonical_candidate_id": None, + "replay_status": "all_methods_failed", + }, + ], + } + + +def _repair(task_id: int) -> dict: + return { + "task_id": task_id, + "source": {"review": "root-cause-review-v1", "attempts_sha256": ["abc"]}, + "repair_basis": { + "root_cause": "the autonomous run used the wrong route", + "repair": "navigate to the contract route", + }, + "tool_calls": [ + {"action": "navigate", "url": "http://gitlab.local/explore?visibility_level=20"}, + {"action": "snapshot"}, + ], + "expected_response": {"task_type": "NAVIGATE", "status": "SUCCESS"}, + } + + +def test_merge_appends_expert_candidates_and_preserves_existing_candidates() -> None: + base = _base_manifest() + original = copy.deepcopy(base) + spec = {"schema": REPAIR_SCHEMA, "repairs": [_repair(2), _repair(3)]} + + merged = merge_expert_seed_manifest(base, spec) + tasks = {task["task_id"]: task for task in merged["tasks"]} + + assert base == original + assert tasks[1] == original["tasks"][0] + assert tasks[2]["candidates"][0] == original["tasks"][1]["candidates"][0] + assert tasks[2]["canonical_candidate_id"] == "expert__task-2" + assert tasks[3]["canonical_candidate_id"] == "expert__task-3" + assert tasks[2]["replay_status"] == tasks[3]["replay_status"] == "ready" + candidate = tasks[2]["candidates"][-1] + assert candidate["candidate_kind"] == "expert_repair" + assert candidate["source"]["details"] == spec["repairs"][0]["source"] + assert candidate["repair_basis"] == spec["repairs"][0]["repair_basis"] + assert candidate["expected_response"] == spec["repairs"][0]["expected_response"] + assert candidate["recorded_response"] == candidate["expected_response"] + assert merged["counts"]["replay_ready_tasks"] == 3 + assert merged["counts"]["success_with_failed_actions_tasks"] == 0 + assert merged["counts"]["all_methods_failed_tasks"] == 0 + assert merged["counts"]["expert_repair_candidates"] == 2 + assert merged["counts"]["successful_method_trajectories"] == 2 + + +def test_merge_rejects_refs_at_any_depth() -> None: + spec = {"schema": REPAIR_SCHEMA, "repairs": [_repair(2)]} + spec["repairs"][0]["tool_calls"][0]["metadata"] = {"ref": "stale-ref"} + + with pytest.raises(ValueError, match="contains forbidden ref"): + merge_expert_seed_manifest(_base_manifest(), spec) + + +@pytest.mark.parametrize( + "call", + [ + {"action": "wait", "timeout": 1000}, + {"action": "wait", "seconds": 1, "selector": "main"}, + {"action": "wait", "seconds": 0}, + ], +) +def test_merge_rejects_invalid_wait_arguments(call: dict) -> None: + repair = _repair(2) + repair["tool_calls"] = [call] + + with pytest.raises(ValueError, match="wait"): + merge_expert_seed_manifest( + _base_manifest(), {"schema": REPAIR_SCHEMA, "repairs": [repair]} + ) + + +def test_merge_refuses_to_replace_ready_candidate() -> None: + with pytest.raises(ValueError, match="cannot replace ready task 1"): + merge_expert_seed_manifest( + _base_manifest(), {"schema": REPAIR_SCHEMA, "repairs": [_repair(1)]} + ) + + +def test_build_binds_exact_input_files(tmp_path: Path) -> None: + base_path = tmp_path / "base.json" + repairs_path = tmp_path / "repairs.json" + base_path.write_text(json.dumps(_base_manifest()), encoding="utf-8") + repairs_path.write_text( + json.dumps({"schema": REPAIR_SCHEMA, "repairs": [_repair(2)]}), encoding="utf-8" + ) + + manifest = build_expert_seed_manifest(base_path, repairs_path) + + assert len(manifest["expert_repairs"]["base_manifest_sha256"]) == 64 + assert len(manifest["expert_repairs"]["repair_spec_sha256"]) == 64 + assert manifest["expert_repairs"]["candidate_count"] == 1 diff --git a/tests/trajectory_replay/test_webarena_corrections.py b/tests/trajectory_replay/test_webarena_corrections.py new file mode 100644 index 000000000..44a161278 --- /dev/null +++ b/tests/trajectory_replay/test_webarena_corrections.py @@ -0,0 +1,163 @@ +from __future__ import annotations + +import copy +import json + +import pytest + +from harness.browser_use.runner import _configure_benchmark +from harness.trajectory_replay.webarena_corrections import ( + CorrectedReplayContract, + apply_correction, + corrected_task_ids, + load_corrections, + normalize_agent_response_for_evaluator, +) + + +@pytest.fixture(scope="module") +def benchmark(): + return _configure_benchmark() + + +@pytest.fixture(scope="module") +def catalog(): + return load_corrections() + + +def _dump_eval(contract: CorrectedReplayContract, index: int) -> dict: + return contract.task.eval[index].model_dump(mode="json") + + +def test_catalog_is_replay_only_and_has_exact_reviewed_scope(catalog) -> None: + payload = json.loads(catalog.source_path.read_text(encoding="utf-8")) + assert payload["historical_browser_use_scores_immutable"] is True + assert "Replay" in payload["scope"] + assert catalog.upstream["package_version"] == "1.2.3" + assert len(catalog.upstream["dataset_sha256"]) == 64 + assert corrected_task_ids(catalog) == (279, 339, 475, 510, 528, 547, 676, 699) + + +def test_apply_is_pure_traceable_and_fails_closed(benchmark, catalog) -> None: + task = benchmark.get_task(279) + before = task.model_dump(mode="json") + applied = apply_correction(task, catalog) + + assert task.model_dump(mode="json") == before + assert applied.task is not task + assert applied.original_task_sha256 != applied.corrected_task_sha256 + assert len(applied.correction_source_sha256) == 64 + assert applied.correction_id.startswith("wv-279-") + + changed = copy.deepcopy(before) + changed["revision"] = 3 + with pytest.raises(ValueError, match="expected official revision 2"): + apply_correction(type(task).model_validate(changed), catalog) + + changed = copy.deepcopy(before) + changed["eval"][0]["expected"]["retrieved_data"][0]["names"][9] = "upstream changed" + with pytest.raises(ValueError, match="precondition failed"): + apply_correction(type(task).model_validate(changed), catalog) + + +def test_279_corrects_only_drifted_reference_title(benchmark, catalog) -> None: + applied = apply_correction(benchmark.get_task(279), catalog) + names = _dump_eval(applied, 0)["expected"]["retrieved_data"][0]["names"] + assert names[9] == "Sony WI-C300 Wireless In-Ear Headphones, Red (WIC300/R))" + assert len(names) == 12 + + +def test_339_uses_canonical_issues_route_and_filters_before_recency(benchmark, catalog) -> None: + applied = apply_correction(benchmark.get_task(339), catalog) + assert _dump_eval(applied, 1)["expected"]["url"] == ( + "__GITLAB__/a11yproject/a11yproject.com/-/issues" + ) + assert applied.replay_assertions[0]["policy"].startswith("match_expected_url") + assert _dump_eval(applied, 2)["last_event_only"] is False + assert _dump_eval(applied, 2)["expected"]["post_data"] == { + "$.variables.state": "opened", + "$.variables.labelName": "bug", + } + + +def test_agent_response_normalization_removes_unsupported_null_metadata() -> None: + assert normalize_agent_response_for_evaluator( + { + "task_type": "NAVIGATE", + "status": "SUCCESS", + "retrieved_data": None, + "error_details": None, + } + ) == { + "task_type": "NAVIGATE", + "status": "SUCCESS", + "retrieved_data": None, + } + + +def test_475_treats_omitted_checkbox_as_false_and_checks_empty_repo(benchmark, catalog) -> None: + applied = apply_correction(benchmark.get_task(475), catalog) + post_data = _dump_eval(applied, 1)["expected"]["post_data"] + assert "project[initialize_with_readme]" not in post_data + assert applied.replay_assertions[0]["empty_repository"] is True + + +def test_510_freezes_selection_order_and_product(benchmark, catalog) -> None: + applied = apply_correction(benchmark.get_task(510), catalog) + assert "least expensive" in applied.task.intent + assert "If prices tie" in applied.task.intent + assert applied.task.instantiation_dict["target_product_id"] == "75640" + assert _dump_eval(applied, 1)["expected"]["post_data"]["product"] == "75640" + assert len(applied.task.eval) == 4 + assert all( + (item.get("expected", {}).get("response_content") or {}).get("items_qty") is None + for item in applied.task.model_dump(mode="json")["eval"] + ) + + +def test_528_uses_final_dom_state_and_forbids_real_submission(benchmark, catalog) -> None: + applied = apply_correction(benchmark.get_task(528), catalog) + network = [_dump_eval(applied, index) for index in range(1, len(applied.task.eval))] + assert len(network) == 1 + assert network[0]["expected"]["url"] == "__SHOPPING__/contact/index/post" + assert network[0]["should_not_exist"] is True + assertion = applied.replay_assertions[0] + assert assertion["kind"] == "final_dom_form_state" + assert assertion["fields"]["textarea[name='comment']"].startswith("I need a refund") + assert assertion["forbid_network_event"]["http_method"] == "POST" + + +def test_547_removes_redundant_attribute_save_but_keeps_variation_save(benchmark, catalog) -> None: + applied = apply_correction(benchmark.get_task(547), catalog) + network_urls = [ + item["expected"]["url"] + for item in (_dump_eval(applied, index) for index in range(1, len(applied.task.eval))) + ] + assert not any("product_attribute/save" in url for url in network_urls) + assert any("catalog/product/save/id/1130" in url for url in network_urls) + assert applied.replay_assertions == () + + +def test_676_selects_matching_filter_xhr_instead_of_main_navigation(benchmark, catalog) -> None: + applied = apply_correction(benchmark.get_task(676), catalog) + evaluator = _dump_eval(applied, 1) + assert evaluator["last_event_only"] is False + assert evaluator["expected"]["query_params"]["filters[status]"] == ["fraud"] + assert applied.replay_assertions[0]["policy"].startswith("match_expected_url") + + +def test_699_checks_browser_form_array_fields_and_all_registered_customer_groups( + benchmark, catalog +) -> None: + applied = apply_correction(benchmark.get_task(699), catalog) + evaluator = _dump_eval(applied, 1) + assert evaluator["post_data_schema"] is None + assert evaluator["expected"]["post_data"] == { + "name": "spring sale", + "website_ids[0]": "1", + "customer_group_ids[0]": "1", + "customer_group_ids[1]": "2", + "customer_group_ids[2]": "3", + "simple_action": "by_percent", + "discount_amount": 20, + } diff --git a/tests/trajectory_replay/test_webarena_dataset.py b/tests/trajectory_replay/test_webarena_dataset.py index 76e812ee2..f274b977a 100644 --- a/tests/trajectory_replay/test_webarena_dataset.py +++ b/tests/trajectory_replay/test_webarena_dataset.py @@ -1,3 +1,4 @@ +import hashlib import json from pathlib import Path @@ -9,6 +10,10 @@ def _write(path: Path, value: object) -> Path: return path +def _sha256(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + def test_promotion_requires_same_candidate_to_pass_every_browser(tmp_path: Path) -> None: seeds = _write( tmp_path / "seeds.json", @@ -44,6 +49,7 @@ def test_promotion_requires_same_candidate_to_pass_every_browser(tmp_path: Path) tmp_path / "receipt.json", { "schema": "lex_webarena_trajectory_replay_canary/1", + "source_seed_manifest_sha256": _sha256(seeds), "run_id": "run-44", "attempts": [ { @@ -63,6 +69,9 @@ def test_promotion_requires_same_candidate_to_pass_every_browser(tmp_path: Path) assert result["counts"] == {"verified_tasks": 1, "verified_trajectories": 1} assert result["trajectories"][0]["verification"]["model_calls"] == 0 + assert result["trajectories"][0]["verification"]["contract"] == { + "kind": "official_task_contract" + } assert result["trajectories"][0]["verification"]["receipt_run_ids"] == ["run-44"] assert result["semantics"]["browser_use_agent_visibility"] == "hidden" @@ -99,6 +108,7 @@ def test_promotion_skips_partial_browser_verification(tmp_path: Path) -> None: tmp_path / "receipt.json", { "schema": "lex_webarena_trajectory_replay_canary/1", + "source_seed_manifest_sha256": _sha256(seeds), "attempts": [ { "task_id": 1, @@ -115,3 +125,82 @@ def test_promotion_skips_partial_browser_verification(tmp_path: Path) -> None: result = build_verified_trajectory_dataset(seeds, [receipt]) assert result["counts"] == {"verified_tasks": 0, "verified_trajectories": 0} + + +def test_promotion_supports_hash_bound_expert_repair(tmp_path: Path) -> None: + seeds = _write( + tmp_path / "seeds.json", + { + "schema": "lex_browser_use_replay_seeds/1", + "source": {"selection_sha256": "selection"}, + "tasks": [ + { + "task_id": 339, + "candidates": [ + { + "id": "expert-339", + "candidate_kind": "expert_repair", + "action_count": 1, + "tool_calls": [{"action": "navigate", "url": "http://host"}], + "recorded_response": {"status": "SUCCESS"}, + "source": { + "kind": "expert_repair_spec", + "repair_spec_sha256": "repair-sha", + "details": {"author": "reviewed"}, + }, + "repair_basis": {"root_cause": "wrong route selection"}, + } + ], + } + ], + }, + ) + receipt = _write( + tmp_path / "receipt.json", + { + "schema": "lex_webarena_trajectory_replay_canary/1", + "source_seed_manifest_sha256": _sha256(seeds), + "run_id": "run-339", + "attempts": [ + { + "task_id": 339, + "source_candidate_id": "expert-339", + "engine": engine, + "process_status": "pass", + "evaluator_status": "pass", + "completed_action_count": 1, + "contract_correction_id": "wv-339-correction", + "contract_correction_source_sha256": "catalog-sha", + "corrected_task_sha256": "task-sha", + } + for engine in ("chromium", "moli") + ], + }, + ) + + result = build_verified_trajectory_dataset(seeds, [receipt]) + + trajectory = result["trajectories"][0] + assert trajectory["source"]["kind"] == "expert_repair_spec" + assert trajectory["source"]["repair_spec_sha256"] == "repair-sha" + assert trajectory["verification"]["contract"] == { + "kind": "corrected_replay_contract", + "correction_id": "wv-339-correction", + "correction_source_sha256": "catalog-sha", + "corrected_task_sha256": "task-sha", + } + + prior = _write( + tmp_path / "prior.json", + { + "schema": "lex_webarena_verified_trajectory_dataset/1", + "official_data_checksum": None, + "trajectories": [{"id": "webarena-verified-44-trajectory-1", "task_id": 44}], + }, + ) + incremented = build_verified_trajectory_dataset( + seeds, [receipt], prior_dataset_path=prior + ) + assert [item["task_id"] for item in incremented["trajectories"]] == [44, 339] + assert incremented["counts"] == {"verified_tasks": 2, "verified_trajectories": 2} + assert incremented["provenance"]["prior_dataset_sha256"] == _sha256(prior) diff --git a/tests/trajectory_replay/test_webarena_replay.py b/tests/trajectory_replay/test_webarena_replay.py index ae2b67d88..640409261 100644 --- a/tests/trajectory_replay/test_webarena_replay.py +++ b/tests/trajectory_replay/test_webarena_replay.py @@ -1,4 +1,8 @@ -from harness.trajectory_replay.webarena_replay import _rebind_url, select_tasks +from harness.trajectory_replay.webarena_replay import ( + _browser_context_needs_restart, + _rebind_url, + select_tasks, +) def test_select_tasks_requires_clean_canonical_candidates() -> None: @@ -21,3 +25,15 @@ def test_rebind_url_changes_only_known_local_site_ports() -> None: "https://192.168.9.2:8023/path" ) assert _rebind_url("https://example.com/path", "192.168.9.2") == ("https://example.com/path") + + +def test_browser_context_restart_is_limited_to_pre_action_context_failure() -> None: + failed_setup = { + "process_status": "fail", + "completed_action_count": 0, + "error": "Protocol error: BrowserContextNotLoaded", + } + assert _browser_context_needs_restart(failed_setup) + assert not _browser_context_needs_restart({**failed_setup, "completed_action_count": 1}) + assert not _browser_context_needs_restart({**failed_setup, "error": "navigation failed"}) + assert not _browser_context_needs_restart({**failed_setup, "process_status": "pass"}) From 8f4cb61f66a322ae3461d2471fed51ddf0950788 Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 14 Sep 2026 11:04:46 +0800 Subject: [PATCH 082/127] Make audited browser comparison the canonical homepage --- harness/browser_use/report.py | 604 +++++++++++++++++--------- harness/browser_use/report_i18n.py | 312 ++++++++++++- research/browser-use-remediation.json | 4 +- research/report-changes.json | 75 ++++ scripts/check_browser_use_report.py | 97 ++++- scripts/check_browser_use_visual.py | 3 +- tests/browser_use/test_report.py | 183 +++++++- 7 files changed, 1044 insertions(+), 234 deletions(-) diff --git a/harness/browser_use/report.py b/harness/browser_use/report.py index e70ebe6a5..a0fcf4a1c 100644 --- a/harness/browser_use/report.py +++ b/harness/browser_use/report.py @@ -828,7 +828,9 @@ def group_for(code: str) -> str: for prefix in ("agent", "benchmark", "site", "environment", "mixed"): if code.startswith(prefix + "_"): return prefix - return "evidence" + if code.startswith("evidence_"): + return "evidence" + raise ValueError(f"unknown failure-cause scope: {code}") counts: dict[str, Counter[str]] = defaultdict(Counter) for diagnosis in verified_causes.values(): @@ -1054,11 +1056,17 @@ def _action_duration_cards(attempts: list[dict[str, Any]]) -> str: if _row_model(row) == model and row["engine"] == engine ) metrics[key] = (total, count, total / count if count else None) - average_winners = _best( - {key: values[2] for key, values in metrics.items()}, - higher=False, - display=lambda value: round(value / 1000, 3), - ) + average_winners = { + model: _best( + { + engine: metrics[f"{model}|{engine}"][2] + for engine in ENGINE_LABELS + }, + higher=False, + display=lambda value: round(value / 1000, 3), + ) + for model in MODEL_LABELS + } maximum = max((values[2] or 0 for values in metrics.values()), default=0) browser_rows = [] for model in MODEL_LABELS: @@ -1068,15 +1076,15 @@ def _action_duration_cards(attempts: list[dict[str, Any]]) -> str: width = 0 if average is None or maximum <= 0 else max(2.0, average / maximum * 100) average_text = "—" if average is None else f"{average / 1000:.3f} 秒" browser_rows.append( - f'
    ' - f"{_e(MODEL_LABELS[model])} · {_e(ENGINE_LABELS[engine])}{_winner(average_text, won=key in average_winners, label='四种配置中单次均值最低')}" + f'
    ' + f"{_e(MODEL_LABELS[model])} · {_e(ENGINE_LABELS[engine])}{_winner(average_text, won=engine in average_winners[model], label='同模型浏览器中单次均值最低')}" f'
    ' f'
    总耗时 {total / 1000:.1f} 秒 · {count} 次调用
    ' ) cards.append( '
    ' + _e(action) - + "四种配置 · 单次调用墙钟时间
    " + + "按模型分层 · 单次调用墙钟时间" + "".join(browser_rows) + "
    " ) @@ -1342,8 +1350,12 @@ def _resource_comparison_table(summary: dict[str, dict[str, dict[str, Any]]]) -> .page-main{overflow-x:clip} .section[id]{scroll-margin-top:76px} @media(max-width:980px){.attempt-summary{grid-template-columns:repeat(2,minmax(0,1fr))}.attempt-verdict{grid-column:1/-1}.attempt-secondary,.diagnosis-grid,.runtime-strip{grid-template-columns:repeat(2,minmax(0,1fr))}.screenshot-grid{grid-template-columns:1fr}} -@media(max-width:760px){.browser-comparison{padding:18px}.comparison-heading{display:block}.comparison-count{display:inline-block;margin-top:12px}.comparison-controls{grid-template-columns:1fr}.action-card-grid{grid-template-columns:1fr}} -@media(max-width:600px){.hero h1{word-break:break-all;line-break:anywhere;text-wrap:balance}.method-figure img{min-width:900px}.table-wrap{margin-left:-4px;margin-right:-4px}.json-view,.log-view{font-size:13px}.review-round header{display:block}.review-status{display:inline-block;margin-top:8px}.review-fields,.attempt-summary,.attempt-secondary,.diagnosis-dialog-grid{grid-template-columns:1fr}.diagnosis-dialog-head,.diagnosis-dialog-grid,.dialog-error,.dialog-review,.dialog-actions{padding-left:16px;padding-right:16px}.dialog-actions button{margin-left:0}} +/* Interpretation article: compact evidence-first reading, without dashboard sprawl. */ +.interpretation-v2{max-width:1120px;margin:0 auto;padding:34px 0 72px}.interpretation-v2>.essay-head{padding:22px 0 28px;border-bottom:1px solid var(--line)}.interpretation-v2>.essay-head h1{max-width:950px;font-size:clamp(38px,5vw,64px);line-height:1.08;margin:12px 0 16px;letter-spacing:-.035em}.interpretation-v2>.essay-head .lead{max-width:900px;font-size:20px;line-height:1.75}.interpretation-v2>section{padding:34px 0;border-bottom:1px solid var(--line)}.interpretation-v2>section>h2{font-size:clamp(28px,3vw,38px);margin:0 0 12px}.interpretation-v2>section>h3{font-size:22px;margin:30px 0 8px}.answer-strip{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.answer-strip article,.evidence-examples article,.decision-grid article{min-width:0;padding:20px;border:1px solid var(--line);border-radius:15px;background:var(--panel)}.answer-strip b{display:block;font-size:18px;line-height:1.45}.answer-strip p,.evidence-examples p,.decision-grid p{margin:9px 0 0;color:var(--muted)}.score-flow-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:22px 0}.score-flow-card{padding:18px;border:1px solid var(--line);border-radius:14px;background:var(--panel)}.score-flow-card header,.cause-bar>div{display:flex;justify-content:space-between;align-items:center;gap:14px}.score-flow-card header span{font-weight:850;color:var(--accent)}.score-flow-card p{margin:9px 0 0;color:var(--muted);font-size:13px}.score-flow-card p span{color:var(--accent);padding:0 4px}.score-track{display:flex;height:9px;margin-top:14px;border-radius:999px;background:var(--panel2);overflow:hidden}.score-track i{display:block;height:100%;background:linear-gradient(90deg,var(--accent),#4d7cff)}.score-track i.recovered{background:var(--good)}.cause-bars{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin:22px 0}.cause-bar{position:relative;overflow:hidden;min-height:126px;padding:17px;border:1px solid var(--line);border-radius:14px;background:var(--panel)}.cause-bar strong{font-size:26px}.cause-bar span{display:block;margin-top:10px;color:var(--muted);font-size:13px;line-height:1.5}.cause-bar i{position:absolute;left:0;bottom:0;height:5px;background:var(--accent);border-radius:0 999px 999px 0}.evidence-examples,.decision-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:12px;margin:18px 0}.evidence-examples h3,.decision-grid h3{margin:0;font-size:18px}.audit-ladder{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:0;margin:22px 0}.audit-ladder article{position:relative;padding:20px 22px;border:1px solid var(--line);background:var(--panel)}.audit-ladder article:first-child{border-radius:14px 0 0 14px}.audit-ladder article:last-child{border-radius:0 14px 14px 0}.audit-ladder article+article{border-left:0}.audit-ladder span{display:grid;place-items:center;width:30px;height:30px;margin-bottom:12px;border-radius:50%;background:var(--accent);color:white;font-weight:850}.audit-ladder b{font-size:17px}.audit-ladder p{margin:8px 0 0;color:var(--muted);font-size:14px}.interpretation-v2 .essay-note{margin-top:22px}.interpretation-v2 .table-wrap{margin:20px 0}.interpretation-v2 .core-comparison-table{min-width:860px} +.verdict-strip{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:12px;margin:16px 0}.verdict-strip article{display:grid;grid-template-columns:auto minmax(0,1fr);gap:3px 12px;align-items:center;padding:17px;border:1px solid var(--line);border-radius:14px;background:var(--panel)}.verdict-strip strong{grid-row:1/3;font-size:34px;color:var(--accent)}.verdict-strip b{line-height:1.4}.verdict-strip p{margin:0;color:var(--muted);font-size:12px;line-height:1.45} +@media(max-width:900px){.answer-strip,.verdict-strip{grid-template-columns:repeat(2,minmax(0,1fr))}.answer-strip article:last-child,.verdict-strip article:last-child{grid-column:1/-1}.cause-bars{grid-template-columns:repeat(2,minmax(0,1fr))}} +@media(max-width:760px){.browser-comparison{padding:18px}.comparison-heading{display:block}.comparison-count{display:inline-block;margin-top:12px}.comparison-controls{grid-template-columns:1fr}.action-card-grid{grid-template-columns:1fr}.interpretation-v2{padding-top:18px}.interpretation-v2>.essay-head h1{font-size:36px}.interpretation-v2>.essay-head .lead{font-size:17px}.interpretation-v2>section{padding:27px 0}} +@media(max-width:600px){.hero h1{word-break:break-all;line-break:anywhere;text-wrap:balance}.method-figure img{min-width:900px}.table-wrap{margin-left:-4px;margin-right:-4px}.json-view,.log-view{font-size:13px}.review-round header{display:block}.review-status{display:inline-block;margin-top:8px}.review-fields,.attempt-summary,.attempt-secondary,.diagnosis-dialog-grid,.answer-strip,.score-flow-grid,.cause-bars,.evidence-examples,.decision-grid,.verdict-strip,.audit-ladder{grid-template-columns:1fr}.answer-strip article:last-child,.verdict-strip article:last-child{grid-column:auto}.audit-ladder article:first-child,.audit-ladder article:last-child{border-radius:14px}.audit-ladder article+article{border-left:1px solid var(--line);border-top:0}.diagnosis-dialog-head,.diagnosis-dialog-grid,.dialog-error,.dialog-review,.dialog-actions{padding-left:16px;padding-right:16px}.dialog-actions button{margin-left:0}} """ ) NAV = [ @@ -1352,7 +1364,6 @@ def _resource_comparison_table(summary: dict[str, dict[str, dict[str, Any]]]) -> ("methodology/", "评测方法", "methodology"), ("analysis/", "结果分析", "analysis"), ("review/", "核验记录", "review"), - ("guide/", "报告解读", "guide"), ("adoption/", "被测浏览器", "adoption"), ("reproduce/", "环境与复现", "reproduce"), ("changes/", "变更记录", "changes"), @@ -1569,7 +1580,9 @@ def _automatic_failure_type(result: dict[str, Any] | None) -> tuple[str, str]: return "response", "最终回答验收失败" if names == {"NetworkEventEvaluator"}: return "network", "网络副作用验收失败" - return "mixed", "多项验收失败" + if names == {"AgentResponseEvaluator", "NetworkEventEvaluator"}: + return "mixed", "多项验收失败" + raise ValueError(f"unknown failed evaluator combination: {sorted(names)}") def _cause_code(verified: dict[str, dict[str, Any]], result_id: str) -> str: @@ -1936,19 +1949,224 @@ def _execution_evidence_html(result_id: str, executions: list[dict[str, Any]]) - ) +def _first_final_rows(attempts: list[dict[str, Any]]) -> list[list[Any]]: + rows: list[list[Any]] = [] + for model in MODEL_LABELS: + for engine in ENGINE_LABELS: + logical = [ + row for row in attempts if row["model"] == model and row["engine"] == engine + ] + first = sum( + float((row.get("_source_executions") or [row])[0].get("score") or 0) == 1 + for row in logical + ) + final = sum(float(row.get("score") or 0) == 1 for row in logical) + retried = sum(len(row.get("_source_executions") or [row]) > 1 for row in logical) + rows.append( + [ + MODEL_LABELS[model], + ENGINE_LABELS[engine], + f"{first}/{len(logical)}", + retried, + f"+{final - first}" if final > first else "0", + f"{final}/{len(logical)}", + ] + ) + return rows + + +def _common_success_resource_rows(attempts: list[dict[str, Any]]) -> tuple[int, list[list[Any]]]: + task_ids = {int(row["task_id"]) for row in attempts} + + def first_execution(row: dict[str, Any]) -> dict[str, Any]: + return (row.get("_source_executions") or [row])[0] + + common = { + task_id + for task_id in task_ids + if all( + any( + row["model"] == model + and row["engine"] == engine + and int(row["task_id"]) == task_id + and float(first_execution(row).get("score") or 0) == 1 + for row in attempts + ) + for model in MODEL_LABELS + for engine in ENGINE_LABELS + ) + } + + def median(values: list[float]) -> float | None: + return statistics.median(values) if values else None + + rows: list[list[Any]] = [] + for model in MODEL_LABELS: + for engine in ENGINE_LABELS: + cohort = [ + { + **first_execution(row), + **_execution_tool_metrics(first_execution(row)), + } + for row in attempts + if row["model"] == model + and row["engine"] == engine + and int(row["task_id"]) in common + ] + rows.append( + [ + MODEL_LABELS[model], + ENGINE_LABELS[engine], + _seconds(median([float(row.get("duration_ms") or 0) for row in cohort])), + _seconds( + median([float(row.get("browser_tool_duration_ms") or 0) for row in cohort]) + ), + _number( + median( + [float((row.get("model_usage") or {}).get("total_tokens") or 0) for row in cohort] + ) + ), + ( + "—" + if not cohort + else f"{median([float((row.get('resource') or {}).get('memory_peak_bytes') or 0) for row in cohort]) / 1048576:.1f} MiB" + ), + _seconds( + median([float((row.get("resource") or {}).get("cpu_ms") or 0) for row in cohort]) + ), + ] + ) + return len(common), rows + + +def _failure_scope_counts(verified_causes: dict[str, dict[str, Any]]) -> list[tuple[str, int, str]]: + copy = { + "agent": ("Agent / 模型", "检索不全、选错对象、字段遗漏或流程提前结束"), + "benchmark": ("任务 / evaluator", "验收合同与页面语义不一致,或 evaluator 自身报错"), + "site": ("自托管站点", "搜索、会话或保存依赖故障"), + "evidence": ("浏览器证据接口", "HAR 缺少 Referer、响应正文或出现重复记录"), + "environment": ("共享环境", "并发执行改写了任务初始状态"), + "mixed": ("参考状态与 Agent 混合", "参考漂移与 Agent 遗漏同时存在"), + } + + def scope(code: str) -> str: + for prefix in ("agent", "benchmark", "site", "environment", "mixed"): + if code.startswith(prefix + "_"): + return prefix + if code.startswith("evidence_"): + return "evidence" + raise ValueError(f"unknown failure-cause scope: {code}") + + counts = Counter( + scope(str((item.get("subcategory") or {}).get("code") or "evidence_capture")) + for item in verified_causes.values() + ) + return [ + (copy[key][0], counts[key], copy[key][1]) + for key in ("agent", "benchmark", "site", "evidence", "environment", "mixed") + if counts[key] + ] + + +def _score_flow_figure(attempts: list[dict[str, Any]]) -> str: + rows = _first_final_rows(attempts) + cards = [] + for model, engine, first, retried, recovered, final in rows: + first_count, total = (int(value) for value in str(first).split("/")) + recovered_count = int(str(recovered).lstrip("+")) + first_width = first_count / total * 100 if total else 0 + recovered_width = recovered_count / total * 100 if total else 0 + cards.append( + '
    ' + + _e(f"{model} · {engine}") + + f'{_e(final)}
    ' + + f'

    首次 {_e(first)} → 失败后重试 {_e(retried)} 题 → 恢复 {_e(recovered)} 题

    ' + ) + return '
    ' + "".join(cards) + "
    " + + +def _failure_scope_figure(verified_causes: dict[str, dict[str, Any]]) -> str: + rows = _failure_scope_counts(verified_causes) + maximum = max((count for _, count, _ in rows), default=1) + bars = "".join( + '
    ' + + _e(label) + + f'{count}
    {_e(description)}
    ' + for label, count, description in rows + ) + return f'
    ' + bars + "
    " + + +def _automatic_failure_counts(attempts: list[dict[str, Any]]) -> Counter[str]: + counts = Counter() + for row in attempts: + if float(row.get("score") or 0) == 1: + continue + result = read_public_json(Path((row.get("paths") or {}).get("evaluation", ""))) + key, _ = _automatic_failure_type(result if isinstance(result, dict) else None) + if key: + counts[key] += 1 + return counts + + +def _automatic_failure_figure(counts: Counter[str]) -> str: + items = ( + ("network", "只有网络副作用规则未通过", "evaluator 没找到合同要求的请求或可观察副作用"), + ("response", "只有最终回答规则未通过", "最终回答与结构化答案合同不一致"), + ("mixed", "两类规则同时未通过", "最终回答和网络副作用均未满足合同"), + ) + cards = "".join( + f'
    {counts[key]}{_e(label)}

    {_e(description)}

    ' + for key, label, description in items + ) + return '
    ' + cards + "
    " + + +def _resource_summary_cards(common_resources: tuple[int, list[list[Any]]]) -> str: + task_count, rows = common_resources + if not task_count: + return '

    当前没有四种配置首次都成功的任务,资源差异暂不比较。

    ' + + def values(engine: str, column: int) -> list[float]: + label = ENGINE_LABELS[engine] + return [float(str(row[column]).split()[0].replace(",", "")) for row in rows if row[1] == label] + + chromium_memory, moli_memory = values("chromium", 5), values("moli", 5) + chromium_tool, moli_tool = values("chromium", 3), values("moli", 3) + chromium_cpu, moli_cpu = values("chromium", 6), values("moli", 6) + return ( + '

    Moli 的内存区间更低

    ' + f'{task_count} 题首次共同成功样本中,Moli 峰值内存 P50 为 {min(moli_memory):.1f}–{max(moli_memory):.1f} MiB;' + f'Chromium 为 {min(chromium_memory):.1f}–{max(chromium_memory):.1f} MiB。

    ' + '

    Chromium 的工具耗时与 CPU 更低

    ' + f'BrowserTool P50:Chromium {min(chromium_tool):.1f}–{max(chromium_tool):.1f} 秒,Moli {min(moli_tool):.1f}–{max(moli_tool):.1f} 秒;' + f'CPU 时间 P50:Chromium {min(chromium_cpu):.1f}–{max(chromium_cpu):.1f} 秒,Moli {min(moli_cpu):.1f}–{max(moli_cpu):.1f} 秒。

    ' + ) + + def _article_body( paired: dict[str, Any], attempts: list[dict[str, Any]], tasks: list[dict[str, Any]], verified_causes: dict[str, dict[str, Any]], + review: dict[str, Any], + cohort_commits: dict[str, str], + automatic_failure_counts: Counter[str], ) -> str: - summary = _primary_summary(attempts) - task_count = len(tasks) + total_task_count = len(tasks) + excluded = _external_interference_task_ids(verified_causes) + eligible_attempts = [ + row for row in attempts if int(row["task_id"]) not in excluded + ] + task_count = total_task_count - len(excluded) + summary = _primary_summary(eligible_attempts) + paired = _paired_summary(eligible_attempts) failed_result_count = sum( item["completed"] - item["passed"] for model_summary in summary.values() for item in model_summary.values() ) + raw_failed_result_count = sum(float(row.get("score") or 0) < 1 for row in attempts) comparison_rows = [ [ MODEL_LABELS[model], @@ -1964,28 +2182,15 @@ def _article_body( for item in model_pairs["comparisons"].values() ] discordant = sum(row[5] + row[6] for row in comparison_rows) - tier_counts = { - tier: sum(task["tier"] == tier for task in tasks) for tier in ("easy", "medium", "hard") - } cause_rows = [ [ - result_id, + Safe(f'{_e(result_id)}'), (diagnosis.get("subcategory") or {}).get("zh") or "—", (diagnosis.get("reason") or {}).get("zh") or "—", ] for result_id, diagnosis in sorted(verified_causes.items()) ] - model_result_rows = [ - [ - MODEL_LABELS[model_id], - ENGINE_LABELS[engine], - f"{values['passed']}/{values['completed']}", - _pct(values["score"]), - ] - for model_id, model_summary in summary.items() - for engine, values in model_summary.items() - ] - if failed_result_count: + if raw_failed_result_count: cause_section = ( _failure_cause_table(verified_causes) + '
    查看每个失败结果的核验结论' @@ -1995,56 +2200,100 @@ def _article_body( caption="人工核验用于解释官方失败结果,不修改分数。修复后需要重新运行才能产生新成绩。", ) + "
    " - if len(cause_rows) == failed_result_count - else f'

    当前有 {failed_result_count} 个失败结果,详细原因仍在核验中;未核验结果不展示推测。

    ' + if len(cause_rows) == raw_failed_result_count + else f'

    当前有 {raw_failed_result_count} 个原始失败结果,详细原因仍在核验中;未核验结果不展示推测。

    ' ) else: cause_section = '

    本轮没有失败结果,无需生成失败根因记录。

    ' - return ( - '
    Browser Use 实测解读
    ' - f"

    {task_count} 个任务中,Chromium 与 Moli 的差异在哪里

    " - '

    报告分别使用 GPT-5.6 Sol 和 GPT-6 Astra 驱动 CowAgent。每组都在相同任务、网站和验收规则下比较浏览器。

    ' - '

    WebArena‑Verified 1.2.3 · 官方程序化 evaluator · 不使用 LLM judge

    ' - "

    总体成绩和耗时

    " - + _table( - ["模型", "浏览器", "通过任务", "通过率", "Agent 总耗时", "BrowserTool 耗时", "Tokens"], - _summary_comparison_rows(summary), - table_id="guide-core-metrics", - classes="core-comparison-table", + first_final = _first_final_rows(eligible_attempts) + common_count, resource_rows = _common_success_resource_rows(eligible_attempts) + clean = summary + clean_rows = [ + [ + MODEL_LABELS[model], + ENGINE_LABELS[engine], + f"{values['passed']}/{values['completed']}", + _pct(values["score"]), + ] + for model, model_summary in clean.items() + for engine, values in model_summary.items() + ] + coverage = review.get("coverage") or {} + reviewed_attempts = int(coverage.get("reviewed_attempts") or 0) + recorded_attempts = int(coverage.get("recorded_attempts") or 0) + commit_note = "、".join( + f"{MODEL_LABELS[model]} {str(commit)[:8]}" for model, commit in cohort_commits.items() + ) + clean_task_count = task_count + first_by_model = { + model: [row for row in first_final if row[0] == label] + for model, label in MODEL_LABELS.items() + } + sol_rows = first_by_model.get("gpt-5.6-sol") or [] + astra_rows = first_by_model.get("gpt-6-astra") or [] + if sol_rows and astra_rows: + sol_chromium = next(row for row in sol_rows if row[1] == ENGINE_LABELS["chromium"]) + astra_chromium = next(row for row in astra_rows if row[1] == ENGINE_LABELS["chromium"]) + model_card_title = "首次结果与重试恢复分开统计" + model_card_body = ( + f"Sol × Chromium 首次 {sol_chromium[2]},有 {str(sol_chromium[4]).lstrip('+')} 个任务在重试中恢复,最终 {sol_chromium[5]};" + f"Astra × Chromium 首次和最终均为 {astra_chromium[5]}。" + ) + astra_explanation = ( + f"只有在 Chromium 的最终分数上,Astra 的 {astra_chromium[5]} 低于 Sol 的 {sol_chromium[5]}。" + f"首次执行时,Sol 为 {sol_chromium[2]},Astra 为 {astra_chromium[2]};差值来自 Sol 恢复的 {str(sol_chromium[4]).lstrip('+')} 个任务。" + "两批模型运行使用了不同的 browser‑eval 提交,因此这里保留为观察,不作严格模型排名。" ) - + "

    在 Sol 运行中,Chromium 比 Moli 多通过 3 题;在 Astra 运行中,两者都是 71/90。耗时需要结合下表和逐题记录看:Agent 总耗时还包含模型推理与页面等待,BrowserTool 耗时也包含工具封装和 CDP 往返。

    " - "

    同一任务上的结果

    " - "

    同题比较只在同一模型内进行。A、B 是表内所列的两个浏览器。

    " - + _table( - [ - "模型", - "浏览器 A", - "浏览器 B", - "同题数", - "两边都通过", - "A 通过、B 失败", - "A 失败、B 通过", - "两边都失败", - ], - comparison_rows, - caption="每行只比较同一模型、同一任务和同一网站状态下的结果。", - ) - + f"

    两次运行合计有 {discordant} 个同题结果不同。Sol 和 Astra 的方向并不一致,因此现有结果不足以支持某个浏览器在所有任务上更好。

    " - "

    两个模型的结果不能直接当成模型排名

    " - "

    Sol 与 Astra 在不同时间、不同代码提交上运行。Astra 的两个 Task 77 结果还受到已确认的共享环境并发污染。表格可以帮助观察差异,但不能把差值全部归因于模型。

    " - + _table(["模型", "浏览器", "任务通过", "通过率"], model_result_rows) - + '

    失败原因

    ' - "

    evaluator 的报错只说明哪条验收规则没有满足。我们继续核对任务要求、最终回答、网络记录和 Agent 轨迹,确认失败发生在 Agent、任务规则、测试网站、共享环境或浏览器证据记录中的哪一处。

    " + else: + model_card_title = "首次成功与重试恢复需要分开" + model_card_body = "最终分数包含失败后的恢复结果;首次通过率单独列出。" + astra_explanation = "首次与最终分数使用不同口径,模型批次之间只作观察性比较。" + pair_details = ";".join( + f"{row[0]}:{row[4]} 题共同通过、{row[5]} 题仅 {row[1]} 通过、{row[6]} 题仅 {row[2]} 通过" + for row in comparison_rows + ) + return ( + '
    Browser Use 能力评测
    ' + f"

    {task_count} 题审查合格比较集:Chromium 与 Moli 的结果、代价与失败位置

    " + f'

    预注册的 {total_task_count} 题中,{len(excluded)} 题确认受到验收合同、站点、共享状态或参考状态干扰。首页只用剩余 {task_count} 题比较四种配置;全部原始结果仍可下钻审计。

    ' + '

    WebArena‑Verified 1.2.3 · CowAgent · reasoning=low · 官方程序化 evaluator

    ' + '
    ' + f'
    {_e(model_card_title)}

    {_e(model_card_body)}

    两层模型共有 {discordant} 个单边通过配对

    {_e(pair_details)}。

    {len(excluded)} 题退出正式比较

    Task 77 的并发污染已经由串行锁修复,但旧结果不会人工改分;合同或站点尚不能可靠验收的题也不进入分母。

    ' + '

    首次成功与重试恢复

    报告的主成绩按预先固定的规则取最后一次执行,失败后最多重试一次。因此首次通过率回答“第一次能否完成”,最终通过率还包含“失败后能否恢复”。

    ' + + _score_flow_figure(eligible_attempts) + + _table(["模型", "浏览器", "首次通过", "重试任务", "重试恢复", "最终通过"], first_final, table_id="home-core-metrics", classes="core-comparison-table") + + f'

    为什么看起来 Astra 更低? {_e(astra_explanation)}

    ' + + '

    两个模型层在不同日期、不同 browser‑eval 提交上运行(' + + _e(commit_note) + + ')。任务、CowAgent 提交、浏览器镜像与 evaluator 数据相同,但这仍不是严格的模型 A/B。

    ' + '

    按同题比较浏览器

    浏览器对比必须留在同一模型层内。汇总数会隐藏互换的成败任务;下表给出同题对照。

    ' + + _table(["模型", "浏览器 A", "浏览器 B", "同题数", "两边都通过", "A 通过、B 失败", "A 失败、B 通过", "两边都失败"], comparison_rows, caption=f"每行只比较同一模型下的 {task_count} 个配对任务。") + + f'

    两个模型层合计有 {discordant} 个单边通过,可用于定位兼容性问题;样本量和方向都不支持总体优劣判断。查看 {task_count} 题的四配置矩阵 →

    ' + '

    程序判分落点与复核根因

    程序判分先说明哪类规则未通过;人工复核再沿最终回答、evaluator 断言、HAR 和 Agent 轨迹追到直接原因。这两个层次需要分开。

    程序判分落点

    ' + + _automatic_failure_figure(automatic_failure_counts) + + '

    “网络副作用未通过”只描述程序判分位置。它可能源于 Agent 没有提交,也可能源于 evaluator、站点或 HAR 采集问题。

    复核根因

    ' + + _failure_scope_figure(verified_causes) + cause_section - + f"

    样本覆盖简单、中等和困难任务

    WebArena‑Verified 没有官方 easy、medium、hard 三档。本项目在运行前按最少可见 UI 操作数分为 1–4、5–7、8+ 三档,各有 {tier_counts['easy']}、{tier_counts['medium']}、{tier_counts['hard']} 题。这个分组描述操作长度,不代表推理难度。

    " - + f'

    为什么使用 WebArena‑Verified

    原版 Mind2Web 主要检查离线动作预测。Online‑Mind2Web 和 WebVoyager 使用实时网页,网页内容会变化,部分评分还依赖模型裁判。WebArena‑Verified 可以本地部署、逐题重置,并用最终答案和网络副作用做程序化验收,适合只替换浏览器的对照实验。

    它的代价是部署较重,环境故障也会影响结果;官方 evaluator 不评价操作路径是否高效,也覆盖不了开放互联网的全部情况。

    这组结果能说明什么

    本轮固定 {task_count} 题,覆盖四个已验证站点。结果适用于当前 CowAgent、模型、任务和运行协议。它没有证明两个浏览器内核等价,也不能证明 Moli 已经可以全面替代 Chromium。端到端耗时包含模型、调度、页面等待和工具开销。

    下一步

    按核验结论分别修复 Agent、评测任务、测试网站和浏览器证据记录,再重新运行受影响的任务;新增浏览器时继续使用同一组任务和验收规则。

    ' + + '

    Agent 行为

    例如漏掉商品、选错最优候选、写错字段,或在 40 步前提前结束。这些属于当前 Agent × 模型配置的真实失败。

    验收规则

    Task 699 的 evaluator 在比较实际提交前就因单元素 Alternatives 配置抛错;Task 528 要求“不提交”,验收却要求 POST。这些 0 分不能归给模型或浏览器。

    站点与环境

    Magento 搜索或库存保存服务失效,以及 Task 77 的并发状态污染,都会让正确操作无法通过。

    浏览器证据

    Moli 的部分 HAR 没有 Referer 或响应正文,例如 Task 595 和 Task 676。页面可能已经操作成功,但 evaluator 无法从网络记录确认;这是浏览器集成层的有效信号。

    ' + + f'

    正式比较与原始记录分开看

    对已证实受任务 / evaluator、站点、共享环境或参考状态干扰的 {len(excluded)} 题,按整题排除四种配置,剩余 {clean_task_count} 题。原始 evaluator 输出不改写,但不再用作首页比较分母。

    ' + + _table(["模型", "浏览器", f"{clean_task_count} 题通过", "通过率"], clean_rows, table_id="home-qualified-results", classes="core-comparison-table") + + f'

    这是本报告的正式比较分母。被排除任务及其 {raw_failed_result_count} 个原始失败记录继续保存在结果页和核验记录中;修复并按同一协议补跑四种配置后,任务才可重新进入比较集。

    ' + '

    审查覆盖与边界

    1证据完整性

    ' + + f'{reviewed_attempts}/{recorded_attempts} 个证据包由程序核对文件存在性、哈希、JSON 可读性、回答 / 分数 / 状态一致性和运行身份。

    2官方程序验收

    每题按最终回答和必要网络副作用判分;没有 LLM judge。

    3失败根因复核

    {len(verified_causes)}/{raw_failed_result_count} 个原始最终失败结果逐条阅读错误、断言、最终回答、HAR 和 Agent 轨迹,归类并给出处理边界。

    ' + + '

    审查分为完整性检查和失败根因复核。 前者确认“证据包与 evaluator 记录一致”;后者检查 evaluator 语义,并识别出 Task 699 等问题。已通过结果没有再做独立语义盲审,其判定依据仍是官方 evaluator。查看核验记录 →

    ' + f'

    全部 {clean_task_count} 题的速度与浏览器资源

    均值、P50、P95 都在四种完整配置中统一比较。CPU、内存和网络只计浏览器容器,不含 CowAgent 或模型服务;模型会改变动作数量和页面停留时间,因此这里比较整套配置,不能单独归因于浏览器内核。

    ' + + _resource_comparison_table(summary) + + '

    每项只标记四种配置中数值唯一最低者。CPU 时间与网络总量累计逻辑结果使用的全部执行;内存来自 Browser Use 执行阶段的浏览器容器采样。

    ' + '

    共同成功任务的性能与资源

    下表限定为首次执行共同成功的 ' + + str(common_count) + + ' 题,每个配置只取第一次执行并报告 P50。这既排除了重试累计,也避免把“很快失败”错当成速度优势。

    ' + + _table(["模型", "浏览器", "Agent 链路 P50", "BrowserTool P50", "Tokens P50", "峰值内存 P50", "CPU 时间 P50"], resource_rows, table_id="home-resource-table", classes="core-comparison-table") + + _resource_summary_cards((common_count, resource_rows)) + + '

    Agent 链路还包含模型推理和页面等待,因此不能解读为纯内核性能。

    ' + f'

    工程结论

    {clean_task_count} 题审查合格比较集显示,四种端到端配置都完成了约九成任务,浏览器之间的单边差异集中在少数任务。有效失败主要来自 Agent 行为和浏览器证据接口,不再混入已确认的合同、站点或共享状态问题。

    Moli 的内存占用明显更低,但 HAR 完整性、工具耗时和部分任务兼容性还需修复;Chromium 在两个模型层都多通过任务,但当前每种配置只有一个正式样本,不能外推成总体优势。

    下一轮验证

    Task 77 可在串行锁下补跑;站点题先通过读写预检,合同矛盾题由上游修正或退出样本。任何补跑都覆盖同题四种配置,并作为新的完整证据批次发布。

    ' ) -def _standalone_article(body: str) -> str: - return f'Browser Use 实测解读
    {body}
    ' - - def _filters(*, attempts: bool = False, dataset: bool = False) -> str: engine_options = "".join( f'' @@ -2445,15 +2694,15 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat tier: sum(task["tier"] == tier for task in tasks) for tier in ("easy", "medium", "hard") } summary_by_model, paired_by_model = _primary_summary(attempts), _paired_summary(attempts) - measured_solvable_ids = { - int(row["task_id"]) for row in attempts if float(row.get("score") or 0) == 1 - } - unproven_ids = sorted( - int(task["task_id"]) for task in tasks if int(task["task_id"]) not in measured_solvable_ids - ) + excluded_task_ids = _external_interference_task_ids(verified_causes) + qualified_attempts = [ + row for row in attempts if int(row["task_id"]) not in excluded_task_ids + ] + qualified_tasks = [ + task for task in tasks if int(task["task_id"]) not in excluded_task_ids + ] + qualified_summary = _primary_summary(qualified_attempts) model = "、".join(MODEL_LABELS.values()) - run_dates = [str(bundle["manifest"].get("created_at") or "")[:10] for bundle in bundles] - run_date = " – ".join(value for value in (min(run_dates), max(run_dates)) if value) or "未记录" adoption_evidence = _read_json(Path("research/browser-use-benchmark-adoption.json")) parent = output.parent parent.mkdir(parents=True, exist_ok=True) @@ -2469,7 +2718,6 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat "datasets", "results", "engines", - "guide", "changes", "methodology", "analysis", @@ -2490,112 +2738,27 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat METHODOLOGY_FIGURE, stage / "assets/webarena-autonomous-evaluation-flow.png", ) - article = _article_body(paired_by_model, attempts, tasks, verified_causes) - families = _task_family_summary(tasks, attempts) - if failed_result_ids and set(verified_causes) == failed_result_ids: - failure_overview = ( - _failure_cause_table(verified_causes) - + f'

    以上共 {len(verified_causes)} 个失败结果;每个结果只计一次。' - '查看逐条核验原因 →

    ' + cohort_commits = { + bundle["model"]: str( + (bundle["manifest"].get("protocol") or {}).get("browser_eval_commit") + or "未记录" ) - elif failed_result_ids: - failure_overview = f'

    根因核验:{len(verified_causes)}/{len(failed_result_ids)} 个失败结果已完成;未核验结果不展示推测性原因。

    ' - else: - failure_overview = ( - '

    根因核验:本轮没有失败结果。

    ' - ) - home = _hero( - f"Browser Use Capability Evaluation · {run_date}", - "Chromium 与 Moli 的 Browser Use 能力对比", - f"CowAgent 使用 {', '.join(MODEL_LABELS.values())},在同一组 WebArena‑Verified {task_count} 题上分别操作 {', '.join(ENGINE_LABELS.values())}。", - ) - home += _section( - "四种配置的总体结果", - _table( - [ - "模型", - "浏览器", - "通过任务", - "通过率", - "Agent 总耗时", - "BrowserTool 耗时", - "Tokens", - ], - _summary_comparison_rows(summary_by_model), - table_id="core-metrics-table", - classes="core-comparison-table", - ) - + '

    在四种配置中统一比较并只标记唯一最优值;相同值不标。通过数越高越好,耗时和 Tokens 越低越好。

    ' - + f'

    四种配置均使用同一组 {task_count} 题。模型和浏览器同时变化时,表格比较的是整套配置,不能把差值只归因于浏览器。查看全部配置的逐题矩阵 →

    ' - + '
    耗时统计包含什么

    Agent 总耗时包含模型推理、页面等待和浏览器操作;BrowserTool 耗时是全部浏览器工具调用的墙钟时间之和,仍包含工具封装与 CDP 往返,不是纯内核 CPU 时间。

    ', - ) - home += _section( - "速度与浏览器资源", - f"

    下表统一比较四种配置。CPU、内存和网络只计浏览器容器,不含 CowAgent 或模型服务;均值、P50、P95 都按 {task_count} 个逻辑任务结果统计。模型会改变动作数量和页面停留时间,因此这里比较的是整套配置,不能单独归因于浏览器内核。

    " - + _resource_comparison_table(summary_by_model) - + '

    仅标记四种配置中数值唯一最低的一项。CPU 时间与网络总量累计得到逻辑结果所用的全部执行;内存来自 Browser Use 执行阶段的浏览器容器采样。

    ', - ) - home += _section( - "失败与干扰项", - f"

    {len(failed_result_ids)} 个失败结果已经逐条核验。与任务合同、站点、共享环境或参考状态有关的任务不会进入诊断子集,修复后需完整重跑。

    " - + failure_overview, - ) - home += _section( - "三类任务的覆盖与成绩", - "

    信息提取、页面导航和页面操作使用不同验收合同,因此分类型展示,不把难度或任务类型混成新的加权分。

    " - + _task_family_table(families), - ) - home += _section( - "数据是否齐全", - _table( - ["检查项", "结果", "说明"], - [ - [ - "任务结果", - f"{len(attempts)}/{len(attempts)}", - f"{len(MODEL_LABELS)} 个模型 × {len(ENGINE_LABELS)} 个浏览器 × {task_count} 题都有最终判定", - ], - [ - "失败原因核验", - f"{len(verified_causes)}/{len(failed_result_ids)}", - "每个失败结果都有分类和具体原因", - ], - [ - "未解决的证据问题", - len(review.get("unresolved") or []), - "完整性检查没有发现待处理项", - ], - [ - "至少成功过一次的任务", - f"{len(measured_solvable_ids)}/{len(tasks)}", - f"另有 {len(unproven_ids)} 题在本轮没有成功轨迹", - ], - ["保存的实际执行", len(source_attempts), "包含正式执行及按协议发生的一次重试"], - ], - table_id="evidence-coverage", - ) - + f'
    报告解读查看 {task_count} 个任务查看 {result_count} 个结果环境与复现
    ', - ) + for bundle in bundles + } + automatic_failure_counts = _automatic_failure_counts(attempts) + article = _article_body( + paired_by_model, + attempts, + tasks, + verified_causes, + review, + cohort_commits, + automatic_failure_counts, + ) + home = f'
    {article}
    ' (stage / "index.html").write_text( _page(title="首页", active="home", body=home), encoding="utf-8" ) - - guide = ( - _hero( - "Report interpretation", - "从模型分层的浏览器对照读出下一轮该怎么做", - "完整文章与首页共用同一份测量数据;另提供无 JavaScript 的独立导出版。", - ) - + _section( - "独立导出", - '', - ) - + f'
    {article}
    ' - ) - (stage / "guide/index.html").write_text( - _page(title="报告解读", active="guide", body=guide, depth=1), encoding="utf-8" - ) - (stage / "guide/article.html").write_text(_standalone_article(article), encoding="utf-8") release_data = _read_json(Path("research/report-changes.json")) releases = release_data.get("browser_use_releases") or [] changes = _hero( @@ -2824,7 +2987,7 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat "四种配置先在同一张表和同一任务矩阵中比较;失败结果可直接打开诊断,查看发生位置、程序报错、复核根因、轨迹、画面和日志。", ) analysis += _section( - "四种配置总览", + f"四种配置总览:{len(qualified_tasks)} 题审查合格比较集", _table( [ "模型", @@ -2835,31 +2998,27 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat "BrowserTool 耗时", "Tokens", ], - _summary_comparison_rows(summary_by_model), + _summary_comparison_rows(qualified_summary), table_id="analysis-core-metrics", classes="core-comparison-table", ) - + '

    四种配置统一比较,只标记唯一最优值;相同值不标。通过数越高越好,耗时和 Tokens 越低越好。

    ' + + f'

    四种完整配置统一比较,每个统计项只标记唯一最优值;相同值不标。已确认受合同、站点、共享状态或参考状态干扰的 {len(excluded_task_ids)} 题不进入本表。这里比较模型 × 浏览器整套配置,不能把差值单独归因于浏览器。

    ' + + '

    审查口径:438/438 次物理执行完成程序完整性核验,75/75 个最终失败完成逐条根因复核;成功结果没有逐条语义盲审,人工复核也没有改写官方 evaluator 分数。

    ' + '
    耗时和 Tokens 如何统计

    Agent 链路累计耗时把取得逻辑结果所发生的执行相加,包含登录、模型推理、调度、页面等待、BrowserTool 与收尾,不包含此前的 Docker 站点重置。BrowserTool 累计耗时是所有 browser 工具调用的墙钟时间之和,包含工具封装、CDP 往返和页面等待。Tokens 同样累计正式结果包含的物理执行。

    ', ) analysis += _section( - "同题全配置对比", - "

    每行是一道任务,四列分别对应模型×浏览器配置;可筛出结果有差异、全部通过或全部失败的任务。

    " - + _same_task_comparison(tasks, attempts), + f"同题全配置对比:{len(qualified_tasks)} 题", + "

    每行是一道审查合格任务,四列分别对应模型×浏览器配置;可筛出结果有差异、全部通过或全部失败的任务。

    " + + _same_task_comparison(qualified_tasks, qualified_attempts), ).replace('
    ', '
    ', 1) analysis += _section( "BrowserTool 动作延迟", - "

    每个动作同时比较四种配置的单次调用墙钟均值,只标记唯一最低值。总耗时和调用数受 Agent 行为量影响,仅用于解释工作量,不参与快慢判定。

    " - + _action_duration_cards(attempts), - ) - analysis += _section( - "排除已证实外部干扰后的诊断子集", - "

    官方 90 题结果保持不变。为了看清浏览器和模型差异,下表暂时排除已核验为评测合同、站点、共享环境或参考状态问题的整道任务。

    " - + _clean_subset_table(attempts, verified_causes), + "

    每个动作按模型分层比较两个浏览器的单次调用墙钟均值,只标记层内唯一最低值。总耗时和调用数受 Agent 行为量影响,仅用于解释工作量,不参与快慢判定。

    " + + _action_duration_cards(qualified_attempts), ) analysis += _section( f"{result_count} 个结果与失败诊断", - "

    默认显示失败。点击“查看诊断”可先在当前页核对发生位置、程序报错、复核根因和处理方案,再进入完整轨迹、画面与日志。

    " + f"

    这里保留预注册 {task_count} 题的全部原始结果,包括退出正式比较的 {len(excluded_task_ids)} 题。默认显示失败;点击“查看诊断”可核对发生位置、程序报错、复核根因、轨迹、画面和日志。

    " + _attempt_table(attempts, tasks, verified_causes), ) (stage / "analysis/index.html").write_text( @@ -3295,8 +3454,49 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat claims = { "schema": "lex_browser_use_interpretation/1", "claims": [ - {"claim": "模型内浏览器配对结果", "basis": paired_by_model}, + {"claim": "审查合格比较集的首次执行、重试与最终成绩", "basis": _first_final_rows(qualified_attempts)}, + {"claim": "审查合格比较集的模型内浏览器配对结果", "basis": _paired_summary(qualified_attempts)}, {"claim": "失败根因以当前证据绑定的逐结果核验为准", "basis": verified_causes}, + { + "claim": "官方 evaluator 的失败规则组合", + "basis": { + "denominator": sum(automatic_failure_counts.values()), + "authority": "final logical execution", + "source": "eval_result.json/evaluators_results where score < 1", + "classes": { + "network_only": automatic_failure_counts["network"], + "response_only": automatic_failure_counts["response"], + "both": automatic_failure_counts["mixed"], + }, + }, + }, + { + "claim": "已证实外部干扰按整题排除后的正式比较集", + "basis": { + "excluded_task_ids": sorted( + _external_interference_task_ids(verified_causes) + ), + "summary": _primary_summary( + [ + row + for row in attempts + if int(row["task_id"]) + not in _external_interference_task_ids(verified_causes) + ] + ), + }, + }, + { + "claim": "四配置首次共同成功任务的性能与资源 P50", + "basis": dict( + zip( + ("task_count", "rows"), + _common_success_resource_rows(qualified_attempts), + strict=True, + ) + ), + }, + {"claim": "执行证据完整性审查覆盖", "basis": review.get("coverage") or {}}, ], "bound_attempts_sha256": attempts_sha256, "selection_sha256": selection.sha256, @@ -3308,7 +3508,7 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat for bundle in bundles }, } - _write_json(stage / "guide/interpretation-evidence.json", claims) + _write_json(stage / "data/interpretation-evidence.json", claims) _write_json( stage / "data/selection.json", { @@ -3356,6 +3556,12 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat }, "summary": summary_by_model, "paired_summary": paired_by_model, + "qualified_comparison": { + "task_count": len(qualified_tasks), + "excluded_task_ids": sorted(excluded_task_ids), + "summary": qualified_summary, + "paired_summary": _paired_summary(qualified_attempts), + }, "selection_sha256": selection.sha256, "evidence_bindings": { "attempts_sha256": attempts_sha256, diff --git a/harness/browser_use/report_i18n.py b/harness/browser_use/report_i18n.py index bc658fd97..d0040b61c 100644 --- a/harness/browser_use/report_i18n.py +++ b/harness/browser_use/report_i18n.py @@ -19,7 +19,6 @@ ("methodology/index.html", "Methodology"), ("analysis/index.html", "Results"), ("review/index.html", "Review"), - ("guide/index.html", "Interpretation"), ("adoption/index.html", "Browsers"), ("reproduce/index.html", "Reproduction"), ("changes/index.html", "Changes"), @@ -1237,6 +1236,13 @@ def _register_report_change_translations() -> None: "四种配置的总体结果": "Overall results across four configurations", "四种配置总览": "Four-configuration overview", "四种配置 · 单次调用墙钟时间": "Four configurations · per-call wall time", + "按模型分层 · 单次调用墙钟时间": "Within each model cohort · per-call wall time", + "同模型浏览器中单次均值最低": "Lowest per-call mean within this model cohort", + "同模型浏览器中通过数最高": "Highest pass count within this model cohort", + "同模型浏览器中通过率最高": "Highest success rate within this model cohort", + "只在同一模型的两个浏览器之间标记唯一较优值;相同值不标。跨模型批次不判优。": "A unique better value is marked only between browsers in the same model cohort. Ties are unmarked, and model cohorts are not ranked against each other.", + "四种配置均使用同一组 90 题,但两个模型层的运行日期和 browser‑eval 提交不同。": "All four configurations use the same 90 tasks, but the model cohorts ran on different dates and browser-eval commits.", + "每个动作按模型分层比较两个浏览器的单次调用墙钟均值,只标记层内唯一最低值。总耗时和调用数受 Agent 行为量影响,仅用于解释工作量,不参与快慢判定。": "For each action, the two browsers are compared within each model cohort by mean per-call wall time, and only a unique cohort minimum is marked. Total time and call count depend on agent workload and provide context rather than a speed ranking.", "四种配置中通过数最高": "Highest pass count across four configurations", "四种配置中通过率最高": "Highest pass rate across four configurations", "四种配置中单次均值最低": "Lowest mean per-call latency across four configurations", @@ -1249,6 +1255,9 @@ def _register_report_change_translations() -> None: "网络发送量": "Network transmitted", "仅标记四种配置中数值唯一最低的一项。CPU 时间与网络总量累计得到逻辑结果所用的全部执行;内存来自 Browser Use 执行阶段的浏览器容器采样。": "Only a unique lowest value across the four configurations is marked. CPU time and network totals include every execution used by the logical result; memory comes from browser-container samples during Browser Use execution.", "四种配置统一比较,只标记唯一最优值;相同值不标。通过数越高越好,耗时和 Tokens 越低越好。": "All four configurations are compared together, and only a unique best value is marked. Ties are unmarked. Higher pass counts are better; lower latency and token use are better.", + "四种完整配置统一比较,每个统计项只标记唯一最优值;相同值不标。这里比较模型 × 浏览器整套配置,不能把差值单独归因于浏览器。": "All four complete configurations are compared together, and only a unique best value is marked for each statistic; ties are unmarked. This compares complete model-by-browser configurations, so a difference cannot be attributed to the browser alone.", + "审查口径:": "Review scope:", + "438/438 次物理执行完成程序完整性核验,75/75 个最终失败完成逐条根因复核;成功结果没有逐条语义盲审,人工复核也没有改写官方 evaluator 分数。": "All 438 physical executions passed programmatic integrity review, and all 75 final failures received case-by-case root-cause review. Successful results did not receive case-by-case blinded semantic review, and human review did not rewrite official evaluator scores.", "耗时和 Tokens 如何统计": "How latency and tokens are measured", "Agent 链路累计耗时把取得逻辑结果所发生的执行相加,包含登录、模型推理、调度、页面等待、BrowserTool 与收尾,不包含此前的 Docker 站点重置。BrowserTool 累计耗时是所有 browser 工具调用的墙钟时间之和,包含工具封装、CDP 往返和页面等待。Tokens 同样累计正式结果包含的物理执行。": "Cumulative Agent-path latency sums the executions used to obtain the logical result. It includes sign-in, model inference, orchestration, page waits, BrowserTool calls, and cleanup, but excludes the preceding Docker site reset. Cumulative BrowserTool latency sums the wall time of all browser-tool calls, including tool-wrapper, CDP round-trip, and page-wait overhead. Tokens likewise sum the physical executions included in the official result.", "结果分析": "Results analysis", @@ -1425,6 +1434,152 @@ def _register_report_change_translations() -> None: "暂无发布记录。": "No release has been recorded.", } +TEXT.update( + { + "90 题跑完了,但结论不能只看 73、71、70": "Ninety tasks are complete, but 73, 71, and 70 do not tell the whole story", + "90 题、四种配置:相近成绩背后的不同原因": "Ninety tasks and four configurations: different causes behind similar scores", + "四种配置的官方分数很接近;75 个零分经复核后分散在多个责任层,不能统一归给 Agent 或浏览器。": "The four official scores are close. Review placed the 75 zeros across several owning layers rather than assigning them all to the agent or browser.", + "WebArena‑Verified 1.2.3 · CowAgent · reasoning=low · 官方程序化 evaluator": "WebArena-Verified 1.2.3 · CowAgent · reasoning=low · official programmatic evaluator", + "独立阅读版": "Standalone reading edition", + "返回交互报告": "Return to the interactive report", + "论点与证据数据": "Claims and evidence data", + "GPT‑6 Astra 没有在首次运行中输给 GPT‑5.6 Sol": "GPT-6 Astra did not trail GPT-5.6 Sol on the first attempt", + "Astra 首次通过 71/90,Sol 为 70/90": "Astra passed 71/90 on the first attempt; Sol passed 70/90", + "Sol × Chromium 有 3 个重试任务恢复成功,最终到 73/90。Astra 的 71/90 并非首次表现落后。": "Three Sol × Chromium tasks recovered on retry, raising its final score to 73/90. Astra's 71/90 does not reflect weaker first-attempt performance.", + "两层模型共有 9 个单边通过配对": "Nine task pairs were one-sided across the two model cohorts", + "14 题含已确认的外部干扰": "Fourteen tasks contain confirmed external interference", + "干扰来自验收合同、站点、共享环境或参考状态。官方分数保留,另用 76 题事后诊断子集观察目标变量。": "The interference comes from acceptance contracts, sites, shared state, or reference state. Official scores remain unchanged; a post-hoc 76-task subset is used only for diagnosis.", + "Astra 在两个浏览器上都是 71/90;Sol 都是 70/90。Sol × Chromium 有 3 个重试任务恢复成功,最终才到 73/90。": "Astra scored 71/90 with both browsers on the first attempt; Sol scored 70/90. Sol × Chromium reached 73/90 only after three failed tasks recovered on retry.", + "Moli 与 Chromium 的总数接近,不代表执行等价": "Similar totals for Moli and Chromium do not mean equivalent execution", + "Astra 层有 69 题共同通过、2 题各自单边通过;Sol 层是 69 题共同通过、4 题仅 Chromium 通过、1 题仅 Moli 通过。": "Under Astra, both browsers passed 69 tasks and each passed two tasks that the other failed. Under Sol, both passed 69, Chromium alone passed four, and Moli alone passed one.", + "官方 90 题成绩不是干净的模型或内核排名": "The official 90-task score is not a clean model or browser-core ranking", + "14 题已证实受验收合同、站点、共享环境或参考状态干扰。官方分数保留,另用 76 题事后诊断子集观察目标变量。": "Fourteen tasks have confirmed interference from acceptance contracts, sites, shared state, or reference state. Their official scores remain unchanged; a post-hoc 76-task subset is shown only for diagnosis.", + "先把分数读对": "Read the score correctly first", + "首次成功与重试恢复": "First-attempt success and retry recovery", + "报告的主成绩按预先固定的规则取最后一次执行,失败后最多重试一次。因此首次通过率回答“第一次能否完成”,最终通过率还包含“失败后能否恢复”。": "The report's official score uses the last execution under the preregistered rule, with at most one retry after failure. First-attempt success answers whether a task worked immediately; final success also reflects recovery after failure.", + "首次 70/90": "First attempt 70/90", + "首次 71/90": "First attempt 71/90", + "首次通过 70/90": "First-attempt passes: 70/90", + "首次通过 71/90": "First-attempt passes: 71/90", + "失败后重试 20 题": "20 failed tasks retried", + "失败后重试 19 题": "19 failed tasks retried", + "恢复 +3 题": "3 tasks recovered", + "恢复 0 题": "No task recovered", + "首次通过": "First-attempt passes", + "重试任务": "Retried tasks", + "重试恢复": "Recovered on retry", + "官方最终通过": "Official final passes", + "为什么看起来 Astra 更低?": "Why does Astra appear lower?", + "只有在 Chromium 的最终分数上,Astra 的 71 低于 Sol 的 73。这 2 题差距不是首次能力差:Sol 首次只有 70,随后恢复 3 题;Astra 首次 71,没有重试恢复。Astra 的 Task 77 还受共享环境污染。": "Astra trails Sol only in the final Chromium score, 71 versus 73. That two-task gap did not exist on the first attempt: Sol started at 70 and recovered three tasks, while Astra started at 71 and recovered none. Astra's Task 77 was also contaminated by shared-state concurrency.", + "两个模型层在不同日期、不同 browser‑eval 提交上运行(GPT-5.6 Sol 51a37526、GPT-6 Astra bed8469e)。任务、CowAgent 提交、浏览器镜像与 evaluator 数据相同,但这仍不是严格的模型 A/B。": "The model cohorts ran on different dates and browser-eval commits (GPT-5.6 Sol 51a37526; GPT-6 Astra bed8469e). They share tasks, CowAgent commit, browser images, and evaluator data, but this is still not a strict model A/B test.", + "总数相近,不是同一批任务相近": "Similar totals can hide different task outcomes", + "按同题比较浏览器": "Compare browsers task by task", + "浏览器对比必须留在同一模型层内。汇总数会隐藏互换的成败任务;下表给出同题对照。": "Browser comparisons must stay within a model cohort. Aggregate totals hide swapped outcomes; the table gives the paired view.", + "浏览器对比必须留在同一模型层内。它们各自的总数会隐藏互换的成败任务;下表才是同题对照。": "Browser comparisons must stay within a model cohort. Aggregate totals hide tasks that swap outcomes; the paired table shows the relevant comparison.", + "每行只比较同一模型下的 90 个配对任务。": "Each row compares the 90 paired tasks within one model cohort.", + "两个模型层合计有 9 个单边通过,可用于定位兼容性问题;样本量和方向都不支持总体优劣判断。": "Across both model cohorts, nine task pairs were one-sided. They help locate compatibility defects, while the sample size and direction do not support an overall ranking.", + "查看 90 题的四配置矩阵 →": "View the four-configuration matrix for all 90 tasks →", + "错误到底发生在哪里": "Where the failures occurred", + "程序判分落点与复核根因": "Programmatic failure location and reviewed root cause", + "程序判分先说明哪类规则未通过;人工复核再沿最终回答、evaluator 断言、HAR 和 Agent 轨迹追到直接原因。这两个层次需要分开。": "Programmatic scoring first identifies which rule class failed. Human review then follows the final answer, evaluator assertions, HAR, and agent trajectory to the direct cause. These are separate layers.", + "程序判分落点": "Programmatic failure location", + "只有网络副作用规则未通过": "Only the network-side-effect rule failed", + "evaluator 没找到合同要求的请求或可观察副作用": "The evaluator did not find the required request or observable side effect", + "只有最终回答规则未通过": "Only the final-answer rule failed", + "最终回答与结构化答案合同不一致": "The final answer disagreed with the structured answer contract", + "两类规则同时未通过": "Both rule classes failed", + "最终回答和网络副作用均未满足合同": "Neither the final answer nor the network side effect satisfied the contract", + "官方 evaluator 的失败规则组合": "Failed rule combinations from the official evaluator", + "“网络副作用未通过”只描述程序判分位置。它可能源于 Agent 没有提交,也可能源于 evaluator、站点或 HAR 采集问题。": "A failed network-side-effect rule identifies the scoring location only. It can result from an agent not submitting, or from the evaluator, site, or HAR capture.", + "复核根因": "Reviewed root cause", + "75 个失败结果的复核原因分布": "Reviewed cause distribution for 75 failed results", + "75 个官方失败结果都读取了最终回答、evaluator 断言、HAR 和 Agent 轨迹。分类统计不是第二套打分,而是回答“这个 0 分由哪一层直接产生”。": "All 75 official failures were reviewed against the final answer, evaluator assertions, HAR, and agent trajectory. The categories do not rescore tasks; they identify the layer that directly produced each zero.", + "检索不全、选错对象、字段遗漏或流程提前结束": "Incomplete retrieval, wrong selection, omitted fields, or premature termination", + "验收合同与页面语义不一致,或 evaluator 自身报错": "Acceptance contracts disagree with page semantics, or the evaluator itself errors", + "搜索、会话或保存依赖故障": "Search, session, or persistence dependency failure", + "HAR 缺少 Referer、响应正文或出现重复记录": "HAR lacks Referer or response bodies, or contains duplicate records", + "共享环境": "Shared environment", + "并发执行改写了任务初始状态": "Concurrent execution changed the task's initial state", + "参考漂移与 Agent 遗漏同时存在": "Reference drift and an agent omission both contributed", + "Agent / 模型行为": "Agent/model behavior", + "保留并修复": "Retain and fix", + "Agent 行为": "Agent behavior", + "例如漏掉商品、选错最优候选、写错字段,或在 40 步前提前结束。这些属于当前 Agent × 模型配置的真实失败。": "Examples include missing products, choosing the wrong candidate, writing the wrong field, or stopping before the 40-step limit. These are genuine failures of the current agent-model configuration.", + "验收规则": "Acceptance rules", + "Task 699 的 evaluator 在比较实际提交前就因单元素 Alternatives 配置抛错;Task 528 要求“不提交”,验收却要求 POST。这些 0 分不能归给模型或浏览器。": "Task 699's evaluator throws on a one-item Alternatives configuration before comparing the submission. Task 528 says not to submit, while its evaluator requires a POST. These zeros cannot be attributed to the model or browser.", + "站点与环境": "Sites and environment", + "Magento 搜索或库存保存服务失效,以及 Task 77 的并发状态污染,都会让正确操作无法通过。": "Magento search or stock-save failures, plus shared-state contamination in Task 77, can prevent otherwise correct work from passing.", + "浏览器证据": "Browser evidence", + "的 evaluator 在比较实际提交前就因单元素 Alternatives 配置抛错;": " fails before comparing the actual submission because the evaluator throws on a one-item Alternatives configuration; ", + "要求“不提交”,验收却要求 POST。这些 0 分不能归给模型或浏览器。": " says not to submit, while its evaluator requires a POST. These zeros cannot be attributed to the model or browser.", + "Magento 搜索或库存保存服务失效,以及": "Magento search or stock-save services failed, and ", + "的并发状态污染,都会让正确操作无法通过。": " suffered shared-state contamination. Either can prevent correct work from passing.", + "Moli 的部分 HAR 没有 Referer 或响应正文,例如": "Some Moli HAR records omit Referer or response bodies, including ", + "。页面可能已经操作成功,但 evaluator 无法从网络记录确认;这是浏览器集成层的有效信号。": ". The UI operation may have succeeded, but the evaluator cannot confirm it from network evidence. This is a valid browser-integration signal.", + "Moli 的部分 HAR 没有 Referer 或响应正文。页面可能已经操作成功,但 evaluator 无法从网络记录确认;这是浏览器集成层的有效信号。": "Some Moli HAR records omit Referer or response bodies. The UI operation may have succeeded, but the evaluator cannot confirm it from network evidence. This is a valid browser-integration signal.", + "官方成绩与诊断子集分开看": "Keep official scores separate from the diagnostic subset", + "历史官方分数不人工改写。对已证实受任务 / evaluator、站点、共享环境或参考状态干扰的 14 题,诊断时按整题排除四种配置,剩余 76 题。": "Historical official scores are never manually rewritten. For diagnosis, 14 tasks with confirmed task/evaluator, site, shared-state, or reference-state interference are removed across all four configurations, leaving 76 tasks.", + "76 题通过": "Passes on 76 tasks", + "诊断通过率": "Diagnostic success rate", + "这是事后诊断口径,用于确定修复顺序,不是新的官方分数。只有修复后重跑完整四配置,才能产生新成绩。": "This post-hoc view prioritizes fixes; it is not a replacement official score. A new score requires rerunning all four configurations after the fixes.", + "这份结果审查到了什么": "What the review established", + "审查覆盖与边界": "Review coverage and boundaries", + "438/438 次物理执行检查了 HAR、Agent trace、最终回答、evaluator 输出和环境准备回执。": "All 438 physical executions were checked for HAR, agent trace, final answer, evaluator output, and environment-preparation receipts.", + "438/438 个证据包由程序核对文件存在性、哈希、JSON 可读性、回答 / 分数 / 状态一致性和运行身份。": "Programs checked all 438 evidence bundles for file presence, hashes, JSON readability, response/score/status consistency, and runtime identity.", + "官方程序验收": "Official programmatic evaluation", + "每题按最终回答和必要网络副作用判分;没有 LLM judge。": "Each task is scored from its final answer and required network side effects; no LLM judge is used.", + "失败根因复核": "Failure root-cause review", + "75/75 个最终失败结果逐条查看错误、断言、轨迹与网络记录,归类并给出处理边界。": "All 75 final failures were reviewed against errors, assertions, trajectories, and network records, then classified with a defined treatment boundary.", + "75/75 个最终失败结果逐条阅读错误、断言、最终回答、HAR 和 Agent 轨迹,归类并给出处理边界。": "All 75 final failures received case-by-case review of errors, assertions, final answers, HAR, and agent trajectories, followed by classification and treatment boundaries.", + "审查分为完整性检查和失败根因复核。": "The review has two layers: integrity checks and failure root-cause review.", + "前者确认“证据包与 evaluator 记录一致”;后者检查 evaluator 语义,并识别出 Task 699 等问题。已通过结果没有再做独立语义盲审,其判定依据仍是官方 evaluator。": "The first confirms that evidence bundles agree with evaluator records. The second checks evaluator semantics and identified issues such as Task 699. Passed results did not receive a separate blinded semantic review and still rely on the official evaluator.", + "查看核验记录 →": "View review records →", + "性能与资源:只在四种配置首次都成功的任务上看": "Performance and resources on tasks all four configurations passed on the first attempt", + "共同成功任务的性能与资源": "Performance and resources on common first-attempt successes", + "首次共同成功任务的资源差异": "Resource differences on common first-attempt successes", + "当前没有四种配置首次都成功的任务,资源差异暂不比较。": "No task was passed on the first attempt by all four configurations, so resource differences are unavailable.", + "Agent 链路还包含模型推理和页面等待,因此不能解读为纯内核性能。": "The agent path also includes model inference and page waits, so it is not a pure browser-core measurement.", + "查看口径与四配置明细 →": "View the cohort definition and four-configuration details →", + "Moli 的内存区间更低": "Moli has the lower memory range", + "Chromium 的工具耗时与 CPU 更低": "Chromium has lower tool latency and CPU time", + "下表限定为首次执行共同成功的 64 题,每个配置只取第一次执行并报告 P50。这既排除了重试累计,也避免把“很快失败”错当成速度优势。": "The table uses the 64 tasks that all four configurations passed on the first attempt. It reports P50 from the first execution only, excluding retry accumulation and the false speed advantage of fast failures.", + "峰值内存 P50": "Peak memory P50", + "CPU 时间 P50": "CPU time P50", + "Moli 的优势很明确:内存": "Moli's clear advantage is memory", + "Moli:峰值内存更低": "Moli: lower peak memory", + "这批首次共同成功任务上,Moli 的峰值内存 P50 约为 190 MiB,Chromium 约为 500–567 MiB。这个差距在两个模型层都出现。": "On these common first-attempt successes, Moli's P50 peak memory is about 190 MiB versus roughly 500-567 MiB for Chromium. The gap appears in both model cohorts.", + "代价也很明确:工具耗时和 CPU": "The tradeoff is tool latency and CPU time", + "Chromium:工具耗时与 CPU 更低": "Chromium: lower tool latency and CPU time", + "同一批任务上,Chromium 的 BrowserTool P50 和 CPU 时间均低于 Moli。整条 Agent 链路还包含模型推理和页面等待,不能解读为纯内核性能。": "On the same tasks, Chromium has lower BrowserTool P50 and CPU time than Moli. The full agent path also includes model inference and page waits, so it is not a pure browser-core measurement.", + "同一批任务上,Moli 的 BrowserTool P50 和 CPU 时间都高于 Chromium。整条 Agent 链路还包含模型推理和页面等待,不能解读为纯内核性能。": "On the same tasks, Moli has higher BrowserTool P50 and CPU time than Chromium. The full agent path also includes model inference and page waits, so it is not a pure browser-core measurement.", + "最后怎么读这份报告": "The practical reading", + "工程结论": "Engineering conclusions", + "官方 90 题成绩显示,四种端到端配置都完成了大约八成任务,浏览器之间的单边差异集中在少数任务。75 个零分分布在 Agent、验收规则、站点、浏览器证据接口和共享环境等多层。": "The official 90-task scores show that all four end-to-end configurations complete about four-fifths of the sample, with one-sided browser differences concentrated in a few tasks. The 75 zeros span agent behavior, acceptance rules, sites, browser-evidence interfaces, and shared state.", + "官方 90 题成绩说明,四种端到端配置都完成了大约八成任务,浏览器之间的单边差异集中在少数任务。错误复核又证明,不能把 75 个零分全部归给 Agent 或浏览器。": "The official 90-task scores show that all four end-to-end configurations complete about four-fifths of the sample and that one-sided browser differences concentrate in a few tasks. Failure review also shows that the 75 zeros do not all belong to the agent or browser.", + "对工程选型有用的结论是:Moli 的内存占用明显更低,但当前 HAR 完整性、工具耗时和部分任务兼容性还需修复;Chromium 在 76 题诊断子集中两个模型层都多通过任务,但当前只有一个正式样本,不能外推成总体优势。": "For engineering decisions, Moli uses substantially less memory, while HAR completeness, tool latency, and some task compatibility still need work. Chromium passes more tasks in both model cohorts on the 76-task diagnostic subset, but one formal sample cannot establish a general advantage.", + "下一轮只做两件事": "Two priorities for the next run", + "下一轮验证": "Next validation run", + "先修复并重跑 14 题干扰项,再对同一冻结代码的四种配置增加重复次数。在此之前,这份报告用于定位兼容性和证据链问题,不发布模型或浏览器排名。": "First fix and rerun the 14 interference tasks; then add repetitions for all four configurations on one frozen codebase. Until then, this report is for locating compatibility and evidence-chain defects, not for publishing a model or browser ranking.", + "先修复并重跑 14 题干扰项,再对同一冻结代码的四种配置增加重复次数。在此之前,这份报告适合找兼容性和证据链问题,不适合发布模型或浏览器排名。": "First fix and rerun the 14 interference tasks; then add repetitions for all four configurations on one frozen codebase. Until then, this report is useful for locating compatibility and evidence-chain defects, not for publishing a model or browser ranking.", + "查看全部配置的逐题矩阵 →": "View the per-task matrix for all configurations →", + "耗时统计包含什么": "What the latency metrics include", + "Agent 总耗时包含模型推理、页面等待和浏览器操作;BrowserTool 耗时是全部浏览器工具调用的墙钟时间之和,仍包含工具封装与 CDP 往返,不是纯内核 CPU 时间。": "Agent total time includes model inference, page waits, and browser actions. BrowserTool time is the summed wall time of all browser-tool calls; it still includes tool-wrapper and CDP round-trip overhead and is not pure browser-core CPU time.", + "把取得逻辑结果所发生的执行相加,包含登录、模型推理、调度、页面等待、BrowserTool 与收尾,不包含此前的 Docker 站点重置。": " sums the executions used to obtain the logical result, including sign-in, model inference, orchestration, page waits, BrowserTool calls, and cleanup, but excluding the preceding Docker site reset. ", + "是所有 browser 工具调用的墙钟时间之和,包含工具封装、CDP 往返和页面等待。Tokens 同样累计正式结果包含的物理执行。": " is the sum of wall time across all browser-tool calls, including tool-wrapper, CDP round-trip, and page-wait overhead. Tokens likewise sum the physical executions included in the official result.", + "Browser Use 能力评测": "Browser Use capability evaluation", + "首次结果与重试恢复分开统计": "Separate first-attempt results from retry recovery", + "最终通过": "Final passes", + "查看评测集": "View dataset", + "下钻全部结果": "Inspect all results", + "下载论点与证据": "Download claims and evidence", + "Task 77 的并发污染已经由串行锁修复,但旧结果不会人工改分;合同或站点尚不能可靠验收的题也不进入分母。": "A serialization lock fixes the Task 77 concurrency defect, but the old result is not manually rescored. Tasks whose contracts or sites still cannot be evaluated reliably are also excluded from the denominator.", + "正式比较与原始记录分开看": "Separate the formal comparison from raw records", + "Task 77 可在串行锁下补跑;站点题先通过读写预检,合同矛盾题由上游修正或退出样本。任何补跑都覆盖同题四种配置,并作为新的完整证据批次发布。": "Task 77 can be rerun under the serialization lock. Site-dependent tasks must first pass read/write preflight checks, while contradictory contracts must be corrected upstream or excluded. Every rerun covers all four configurations for the task and is published as a new complete evidence cohort.", + "每行是一道审查合格任务,四列分别对应模型×浏览器配置;可筛出结果有差异、全部通过或全部失败的任务。": "Each row is a review-qualified task, with one column per model-browser configuration. Filters isolate divergent, all-pass, or all-fail outcomes.", + "这里保留预注册 90 题的全部原始结果,包括退出正式比较的 14 题。默认显示失败;点击“查看诊断”可核对发生位置、程序报错、复核根因、轨迹、画面和日志。": "This inventory retains raw results for all 90 preregistered tasks, including the 14 excluded from the formal comparison. Failures are shown by default; open a diagnosis to inspect the failure point, program error, reviewed cause, trajectory, frames, and logs.", + } +) + def translate_text(value: str) -> str: leading = value[: len(value) - len(value.lstrip())] @@ -1435,6 +1590,161 @@ def translate_text(value: str) -> str: translated = TEXT.get(core) if translated is None: patterns = ( + (r"首次 (\d+/\d+)", lambda m: f"First attempt {m.group(1)}"), + (r"失败后重试 (\d+) 题", lambda m: f"{m.group(1)} failed tasks retried"), + (r"查看 (\d+) 题的四配置矩阵 →", lambda m: f"View the four-configuration matrix for all {m.group(1)} tasks →"), + ( + r"Sol × Chromium 首次 (\d+/\d+),有 (\d+) 个任务在重试中恢复,最终 (\d+/\d+);Astra × Chromium 首次和最终均为 (\d+/\d+)。", + lambda m: f"Sol × Chromium passed {m.group(1)} on the first attempt, recovered {m.group(2)} tasks on retry, and finished at {m.group(3)}. Astra × Chromium was {m.group(4)} both before and after retries.", + ), + ( + r"(\d+) 题审查合格比较集:Chromium 与 Moli 的结果、代价与失败位置", + lambda m: f"{m.group(1)}-task review-qualified set: Chromium vs. Moli outcomes, cost, and failure points", + ), + ( + r"预注册的 (\d+) 题中,(\d+) 题确认受到验收合同、站点、共享状态或参考状态干扰。首页只用剩余 (\d+) 题比较四种配置;全部原始结果仍可下钻审计。", + lambda m: f"Of {m.group(1)} preregistered tasks, {m.group(2)} were confirmed to be affected by acceptance contracts, sites, shared state, or reference-state drift. The homepage compares the four configurations only on the remaining {m.group(3)} tasks; every raw result remains available for audit.", + ), + ( + r"(\d+) 题退出正式比较", + lambda m: f"{m.group(1)} tasks are excluded from the formal comparison", + ), + ( + r"只有在 Chromium 的最终分数上,Astra 的 (\d+/\d+) 低于 Sol 的 (\d+/\d+)。首次执行时,Sol 为 (\d+/\d+),Astra 为 (\d+/\d+);差值来自 Sol 恢复的 (\d+) 个任务。两批模型运行使用了不同的 browser‑eval 提交,因此这里保留为观察,不作严格模型排名。", + lambda m: f"Astra trails Sol only in the final Chromium result, {m.group(1)} versus {m.group(2)}. On the first attempt Sol scored {m.group(3)} and Astra {m.group(4)}; the difference comes from {m.group(5)} Sol recoveries. The two model cohorts used different browser-eval commits, so this remains an observation rather than a strict model ranking.", + ), + ( + r"对已证实受任务 / evaluator、站点、共享环境或参考状态干扰的 (\d+) 题,按整题排除四种配置,剩余 (\d+) 题。原始 evaluator 输出不改写,但不再用作首页比较分母。", + lambda m: f"For {m.group(1)} tasks confirmed to be affected by task/evaluator, site, shared-state, or reference-state problems, all four configurations are excluded together, leaving {m.group(2)} tasks. Raw evaluator outputs are preserved but no longer serve as the homepage comparison denominator.", + ), + ( + r"这是本报告的正式比较分母。被排除任务及其 (\d+) 个原始失败记录继续保存在结果页和核验记录中;修复并按同一协议补跑四种配置后,任务才可重新进入比较集。", + lambda m: f"This is the report's formal comparison denominator. Excluded tasks and their {m.group(1)} raw failure records remain in the result and review pages. A task can re-enter only after correction and a four-configuration rerun under the same protocol.", + ), + ( + r"(\d+)/(\d+) 个原始最终失败结果逐条阅读错误、断言、最终回答、HAR 和 Agent 轨迹,归类并给出处理边界。", + lambda m: f"All {m.group(1)}/{m.group(2)} raw final failures received case-by-case review of errors, assertions, final answers, HAR, and agent trajectories, followed by classification and treatment boundaries.", + ), + ( + r"(\d+) 题审查合格比较集显示,四种端到端配置都完成了约九成任务,浏览器之间的单边差异集中在少数任务。有效失败主要来自 Agent 行为和浏览器证据接口,不再混入已确认的合同、站点或共享状态问题。", + lambda m: f"On the {m.group(1)}-task review-qualified set, all four end-to-end configurations complete about nine in ten tasks, with one-sided browser differences concentrated in a few tasks. Remaining failures are mainly agent behavior and browser-evidence-interface issues, without confirmed contract, site, or shared-state interference.", + ), + ( + r"Moli 的内存占用明显更低,但 HAR 完整性、工具耗时和部分任务兼容性还需修复;Chromium 在两个模型层都多通过任务,但当前每种配置只有一个正式样本,不能外推成总体优势。", + lambda m: "Moli uses substantially less memory, while HAR completeness, tool latency, and some task compatibility still need work. Chromium passes more tasks in both model cohorts, but one formal sample per configuration cannot establish a general advantage.", + ), + ( + r"四种配置总览:(\d+) 题审查合格比较集", + lambda m: f"Four-configuration overview: {m.group(1)}-task review-qualified set", + ), + ( + r"同题全配置对比:(\d+) 题", + lambda m: f"Same-task comparison across all configurations: {m.group(1)} tasks", + ), + ( + r"Astra 首次通过 (\d+/\d+),Sol 为 (\d+/\d+)", + lambda m: f"Astra passed {m.group(1)} on the first attempt; Sol passed {m.group(2)}", + ), + ( + r"Sol × Chromium 有 (\d+) 个重试任务恢复成功,最终到 (\d+/\d+)。Astra 的 (\d+/\d+) 并非首次表现落后。", + lambda m: f"{m.group(1)} Sol × Chromium tasks recovered on retry, raising its final score to {m.group(2)}. Astra's {m.group(3)} does not reflect weaker first-attempt performance.", + ), + ( + r"两层模型共有 (\d+) 个单边通过配对", + lambda m: f"{m.group(1)} task pairs were one-sided across the two model cohorts", + ), + ( + r"(.+):(\d+) 题共同通过、(\d+) 题仅 (.+) 通过、(\d+) 题仅 (.+) 通过;(.+):(\d+) 题共同通过、(\d+) 题仅 (.+) 通过、(\d+) 题仅 (.+) 通过。", + lambda m: f"{m.group(1)}: both browsers passed {m.group(2)} tasks, only {m.group(4)} passed {m.group(3)}, and only {m.group(6)} passed {m.group(5)}. {m.group(7)}: both browsers passed {m.group(8)} tasks, only {m.group(10)} passed {m.group(9)}, and only {m.group(12)} passed {m.group(11)}.", + ), + ( + r"(\d+) 题含已确认的外部干扰", + lambda m: f"{m.group(1)} tasks contain confirmed external interference", + ), + ( + r"(\d+) 个失败结果已经逐条核验。与任务合同、站点、共享环境或参考状态有关的任务不会进入诊断子集,修复后需完整重跑。", + lambda m: f"All {m.group(1)} failed results received case-by-case review. Tasks affected by acceptance contracts, sites, shared state, or reference state are excluded from the diagnostic subset and require a complete rerun after correction.", + ), + ( + r"干扰来自验收合同、站点、共享环境或参考状态。官方分数保留,另用 (\d+) 题事后诊断子集观察目标变量。", + lambda m: f"The interference comes from acceptance contracts, sites, shared state, or reference state. Official scores remain unchanged; a post-hoc {m.group(1)}-task subset is used only for diagnosis.", + ), + ( + r"首次通过 (\d+/\d+),重试恢复 (\d+) 题,最终通过 (\d+/\d+)", + lambda m: f"First-attempt passes: {m.group(1)}; recovered on retry: {m.group(2)}; final passes: {m.group(3)}", + ), + ( + r"只有在 Chromium 的最终分数上,Astra 的 (\d+/\d+) 低于 Sol 的 (\d+/\d+)。首次执行时,Sol 为 (\d+/\d+),Astra 为 (\d+/\d+);差值来自 Sol 恢复的 (\d+) 个任务。Astra 的 Task 77 还受共享环境污染。", + lambda m: f"Astra trails Sol only in the final Chromium score, {m.group(1)} versus {m.group(2)}. On the first attempt Sol scored {m.group(3)} and Astra {m.group(4)}; the difference comes from {m.group(5)} Sol recoveries. Astra's Task 77 was also contaminated by shared-state concurrency.", + ), + ( + r"(\d+) 题、四种配置:相近成绩背后的不同原因", + lambda m: f"{m.group(1)} tasks and four configurations: different causes behind similar scores", + ), + ( + r"四种配置的官方分数很接近;(\d+) 个零分经复核后分散在多个责任层,不能统一归给 Agent 或浏览器。", + lambda m: f"The four official scores are close. Review placed the {m.group(1)} zeros across several owning layers rather than assigning them all to the agent or browser.", + ), + ( + r"两个模型层合计有 (\d+) 个单边通过,可用于定位兼容性问题;样本量和方向都不支持总体优劣判断。", + lambda m: f"Across both model cohorts, {m.group(1)} task pairs were one-sided. They help locate compatibility defects, while the sample size and direction do not support an overall ranking.", + ), + ( + r"(\d+)/(\d+) 次物理执行检查了 HAR、Agent trace、最终回答、evaluator 输出和环境准备回执。", + lambda m: f"All {m.group(1)}/{m.group(2)} physical executions were checked for HAR, agent trace, final answer, evaluator output, and environment-preparation receipts.", + ), + ( + r"(\d+)/(\d+) 个证据包由程序核对文件存在性、哈希、JSON 可读性、回答 / 分数 / 状态一致性和运行身份。", + lambda m: f"Programs checked all {m.group(1)}/{m.group(2)} evidence bundles for file presence, hashes, JSON readability, response/score/status consistency, and runtime identity.", + ), + ( + r"(\d+)/(\d+) 个最终失败结果逐条查看错误、断言、轨迹与网络记录,归类并给出处理边界。", + lambda m: f"All {m.group(1)}/{m.group(2)} final failures were reviewed against errors, assertions, trajectories, and network records, then classified with a defined treatment boundary.", + ), + ( + r"(\d+)/(\d+) 个最终失败结果逐条阅读错误、断言、最终回答、HAR 和 Agent 轨迹,归类并给出处理边界。", + lambda m: f"All {m.group(1)}/{m.group(2)} final failures received case-by-case review of errors, assertions, final answers, HAR, and agent trajectories, followed by classification and treatment boundaries.", + ), + ( + r"(\d+) 个失败结果的复核原因分布", + lambda m: f"Reviewed cause distribution for {m.group(1)} failed results", + ), + ( + r"(\d+) 题通过", + lambda m: f"Passes on {m.group(1)} tasks", + ), + ( + r"每行只比较同一模型下的 (\d+) 个配对任务。", + lambda m: f"Each row compares the {m.group(1)} paired tasks within one model cohort.", + ), + ( + r"下表限定为首次执行共同成功的 (\d+) 题,每个配置只取第一次执行并报告 P50。这既排除了重试累计,也避免把“很快失败”错当成速度优势。", + lambda m: f"The table uses the {m.group(1)} tasks that all four configurations passed on the first attempt. It reports P50 from the first execution only, excluding retry accumulation and the false speed advantage of fast failures.", + ), + ( + r"(\d+) 题首次共同成功样本中,Moli 峰值内存 P50 为 ([0-9.]+)–([0-9.]+) MiB;Chromium 为 ([0-9.]+)–([0-9.]+) MiB。", + lambda m: f"Across {m.group(1)} common first-attempt successes, Moli's P50 peak memory spans {m.group(2)}-{m.group(3)} MiB, versus {m.group(4)}-{m.group(5)} MiB for Chromium.", + ), + ( + r"BrowserTool P50:Chromium ([0-9.]+)–([0-9.]+) 秒,Moli ([0-9.]+)–([0-9.]+) 秒;CPU 时间 P50:Chromium ([0-9.]+)–([0-9.]+) 秒,Moli ([0-9.]+)–([0-9.]+) 秒。", + lambda m: f"BrowserTool P50: Chromium {m.group(1)}-{m.group(2)} seconds, Moli {m.group(3)}-{m.group(4)} seconds. CPU-time P50: Chromium {m.group(5)}-{m.group(6)} seconds, Moli {m.group(7)}-{m.group(8)} seconds.", + ), + ( + r"两个模型层在不同日期、不同 browser‑eval 提交上运行((.+))。任务、CowAgent 提交、浏览器镜像与 evaluator 数据相同,但这仍不是严格的模型 A/B。", + lambda m: f"The model cohorts ran on different dates and browser-eval commits ({m.group(1).replace('、', '; ')}). They share tasks, CowAgent commit, browser images, and evaluator data, but this is still not a strict model A/B test.", + ), + ( + r"官方 (\d+) 题成绩显示,四种端到端配置都完成了大约八成任务,浏览器之间的单边差异集中在少数任务。(\d+) 个零分分布在 Agent、验收规则、站点、浏览器证据接口和共享环境等多层。", + lambda m: f"The official {m.group(1)}-task scores show that all four end-to-end configurations complete about four-fifths of the sample, with one-sided browser differences concentrated in a few tasks. The {m.group(2)} zeros span agent behavior, acceptance rules, sites, browser-evidence interfaces, and shared state.", + ), + ( + r"Moli 的内存占用明显更低,但当前 HAR 完整性、工具耗时和部分任务兼容性还需修复;Chromium 在 (\d+) 题诊断子集中两个模型层都多通过任务,但当前只有一个正式样本,不能外推成总体优势。", + lambda m: f"Moli uses substantially less memory, while HAR completeness, tool latency, and some task compatibility still need work. Chromium passes more tasks in both model cohorts on the {m.group(1)}-task diagnostic subset, but one formal sample cannot establish a general advantage.", + ), + ( + r"先修复并重跑 (\d+) 题干扰项,再对同一冻结代码的四种配置增加重复次数。在此之前,这份报告用于定位兼容性和证据链问题,不发布模型或浏览器排名。", + lambda m: f"First fix and rerun the {m.group(1)} interference tasks; then add repetitions for all four configurations on one frozen codebase. Until then, this report is for locating compatibility and evidence-chain defects, not for publishing a model or browser ranking.", + ), ( r"(\d+) 个结果与失败诊断", lambda m: f"{m.group(1)} results and failure diagnoses", diff --git a/research/browser-use-remediation.json b/research/browser-use-remediation.json index 94ffb8c2b..b11012323 100644 --- a/research/browser-use-remediation.json +++ b/research/browser-use-remediation.json @@ -1,7 +1,7 @@ { "schema": "lex_browser_use_remediation/1", - "updated_at": "2026-09-11", - "score_policy": "Official results remain unchanged. A task is excluded from the post-hoc diagnostic clean subset across every configuration when any verified result is affected by benchmark/evaluator, site, shared-environment, or mixed reference-state interference.", + "updated_at": "2026-09-14", + "score_policy": "Raw evaluator outputs remain unchanged. A task is excluded from the formal comparison across every configuration when any verified result is affected by benchmark/evaluator, site, shared-environment, or mixed reference-state interference. Readmission requires a corrected contract or runtime, independent preflight, and a complete four-configuration rerun.", "items": [ { "category": "shared_environment", diff --git a/research/report-changes.json b/research/report-changes.json index 84dd67236..2aa8f9268 100644 --- a/research/report-changes.json +++ b/research/report-changes.json @@ -5,6 +5,24 @@ "en": "This history begins with revisions supported by a complete audit trail. Earlier Artifact Hub versions remain available, but no itemized history is reconstructed where the supporting record is incomplete." }, "browser_use_reviews": [ + { + "id": "review-2026-09-11-interpretation-reader-audit", + "date": "2026-09-11", + "title": "解读页、比较语义与双语版面复核", + "status": "已处理", + "scope": "报告首页、解读页、分析页、失败下钻、英文本地化和桌面/平板/手机断点。", + "findings": "原稿把不同模型配置放在一起标记优胜,混淆了程序判分位置与人工复核根因;重试累计也会扭曲性能对比。部分失败缺少直接下钻入口,英文页仍有中文残留,平板宽度过早堆成单列。", + "corrections": "优胜只在同一模型内比较浏览器;分别披露 43 个仅网络规则失败、20 个仅回答规则失败、12 个双规则失败,以及 29/24/10/8/2/2 的复核根因。性能资源改用四配置首次共同成功的 64 题且只取首次执行,并补齐逐结果链接、英文门禁和响应式布局。", + "remaining": "当前仍是每种配置一个正式样本。14 个已确认外部干扰任务需在修复后完整重跑;增加重复次数前不发布模型或浏览器总体排名。", + "i18n": {"en": { + "title": "Reader audit of interpretation, comparison semantics, and bilingual layout", + "status": "Resolved", + "scope": "The report home, guide, analysis, failure drill-down, English localization, and desktop, tablet, and phone breakpoints.", + "findings": "The draft marked winners across different model configurations, conflated programmatic failure location with human-reviewed root cause, and allowed retry accumulation to distort performance comparisons. Some failures lacked direct drill-down, English pages retained Chinese prose, and tablet layouts collapsed into one column too early.", + "corrections": "Winner marks now compare browsers only within one model. The report separately discloses 43 network-only, 20 response-only, and 12 dual-rule failures, plus the 29/24/10/8/2/2 reviewed root-cause split. Performance and resource statistics use the 64 tasks all four configurations passed on the first attempt and only the first execution; direct result links, English gates, and responsive layouts were added.", + "remaining": "Each configuration still has one formal sample. The 14 confirmed interference tasks require a complete rerun after fixes, and no overall model or browser ranking should be published before repetitions are added." + }} + }, { "id": "review-2026-09-11-astra-layer-and-environment-isolation", "date": "2026-09-11", @@ -147,6 +165,63 @@ } ], "browser_use_releases": [ + { + "id": "2026-09-14-qualified-comparison-home", + "date": "2026-09-14", + "title": "首页改用审查合格比较集并合并报告解读", + "summary": "首页不再把含已确认合同、站点、共享状态或参考状态干扰的 90 题原始结果当作正式比较。四种配置统一改用 76 题审查合格分母;90 题原始 evaluator 输出完整保留在结果与核验页。报告解读并入首页,删除重复入口和独立阅读副本。", + "attempts_sha256_by_model": { + "gpt-5.6-sol": "73146500152db62d79f84b63c85a14027c005a6320c19bce7426651f167466bd", + "gpt-6-astra": "0659d227cf6d29fbfaf64d39f8e77a56bec73c87f7f6e1f2ac23ea13fd9b667c" + }, + "selection_sha256": "2cb716552e36df7535e83ed6d2cf3f84283e3aca86072817cd6e039a38547c7b", + "changes": [ + [ + "正式比较分母", + "90 题汇总混入 14 题已确认的非目标变量干扰,首页同时展示诊断子集,读者无法判断哪组才是结论依据。", + "按整题、四配置一致排除 14 题,76 题成为唯一正式比较集;原始 90 题只作审计记录,不人工改分。", + "Sol 为 Chromium 71/76、Moli 67/76;Astra 为 Chromium 69/76、Moli 68/76。" + ], + [ + "Task 77", + "Astra 的两条 Task 77 证据在 reset 后被另一个进程改写共享 Shopping Admin 状态。", + "运行器已用跨进程锁覆盖 reset、Agent、证据收尾和 evaluator;旧污染结果退出分母,只有四配置新证据才能重新纳入。", + "Task 77 不再影响首页成绩;历史执行仍可下钻核对。" + ], + [ + "信息结构", + "首页与报告解读重复,读者需要在两个入口之间往返。", + "把解读、成绩、失败位置、审查边界和资源结论合并为首页,删除 guide 路由与独立副本。", + "数据集、结果、核验和复现仍保留独立下钻页面。" + ] + ], + "i18n": { + "en": { + "title": "Use a review-qualified comparison set on the unified homepage", + "summary": "The homepage no longer treats raw 90-task results containing confirmed contract, site, shared-state, or reference-state interference as the formal comparison. All four configurations now use one 76-task review-qualified denominator, while every raw evaluator output remains available in the result and review pages. The interpretation is merged into the homepage and duplicate routes are removed.", + "changes": [ + [ + "Formal denominator", + "The 90-task summary mixed 14 tasks with confirmed non-target interference and placed a diagnostic subset beside it, leaving the conclusion denominator ambiguous.", + "Exclude each affected task across all four configurations. The 76-task set becomes the sole formal comparison; raw 90-task results remain audit records and are not manually rescored.", + "Sol scores 71/76 on Chromium and 67/76 on Moli; Astra scores 69/76 on Chromium and 68/76 on Moli." + ], + [ + "Task 77", + "Both Astra Task 77 executions had shared Shopping Admin state modified by another process after reset.", + "A cross-process lock now covers reset, agent execution, evidence finalization, and evaluation. The contaminated results leave the denominator and can return only through new four-configuration evidence.", + "Task 77 no longer affects the homepage scores; its historical executions remain available for inspection." + ], + [ + "Information architecture", + "The homepage and interpretation duplicated content and forced readers to switch between two entry points.", + "Merge interpretation, scores, failure locations, review boundaries, and resource findings into the homepage; remove the guide route and standalone copy.", + "Dataset, results, review, and reproduction remain separate drill-down pages." + ] + ] + } + } + }, { "id": "2026-09-11-browser-resource-comparison", "date": "2026-09-11", diff --git a/scripts/check_browser_use_report.py b/scripts/check_browser_use_report.py index 211f922c1..6ca5a70e5 100644 --- a/scripts/check_browser_use_report.py +++ b/scripts/check_browser_use_report.py @@ -7,10 +7,36 @@ import hashlib import json import re +from html.parser import HTMLParser from pathlib import Path from PIL import Image + +class _VisibleTextParser(HTMLParser): + def __init__(self) -> None: + super().__init__() + self.hidden_depth = 0 + self.parts: list[str] = [] + + def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: + if tag.lower() in {"script", "style", "pre", "code"}: + self.hidden_depth += 1 + + def handle_endtag(self, tag: str) -> None: + if tag.lower() in {"script", "style", "pre", "code"} and self.hidden_depth: + self.hidden_depth -= 1 + + def handle_data(self, data: str) -> None: + if not self.hidden_depth: + self.parts.append(data) + + +def _visible_text(html: str) -> str: + parser = _VisibleTextParser() + parser.feed(html) + return " ".join(parser.parts) + from harness.browser_use.failure_cause_review import ( DEFAULT_REVIEW_PATH as FAILURE_CAUSE_REVIEW_PATH, ) @@ -54,7 +80,7 @@ def _check_reader_facing_ux( raise ValueError(f"formal report contains unexplained internal term {term}: {page}") for homepage in (report_dir / "index.html", report_dir / "en/index.html"): text = rendered_pages[homepage] - if not re.search(r']*\bid=["\']core-metrics-table["\']', text, flags=re.I): + if not re.search(r']*\bid=["\']home-core-metrics["\']', text, flags=re.I): raise ValueError(f"report home page is missing the core metrics table: {homepage}") @@ -304,8 +330,6 @@ def check( raise ValueError("current Browser Use profile must register unique browser engines") page_names = ( "index.html", - "guide/index.html", - "guide/article.html", "changes/index.html", "datasets/index.html", "methodology/index.html", @@ -441,6 +465,19 @@ def check( excluded_lists = re.findall(r"tasks excluded in this run: ([^<]+)", text) if any("、" in value for value in excluded_lists): raise ValueError(f"English excluded-task list uses Chinese punctuation: {page}") + for relative, allowed in { + "index.html": {"中文"}, + }.items(): + page = report_dir / "en" / relative + visible = re.sub(r"\s+", " ", _visible_text(rendered_pages[page])) + for token in allowed: + visible = visible.replace(token, "") + match = cjk.search(visible) + if match: + excerpt = visible[max(0, match.start() - 40) : match.start() + 80] + raise ValueError( + f"English reader-facing page contains Chinese prose: {page}: {excerpt}" + ) for engine in engines: engine_page = rendered_pages[report_dir / "engines" / f"{engine}.html"] if "engine-result-table" in engine_page or f"f_engine={engine}" not in engine_page: @@ -480,16 +517,31 @@ def check( 'id="analysis-core-metrics"', "BrowserTool 动作延迟", "action-card-grid", - "四种配置 · 单次调用墙钟时间", + "按模型分层 · 单次调用墙钟时间", "同题全配置对比", 'id="configuration-matrix"', - 'id="clean-subset-table"', "查看诊断", 'class="diagnosis-dialog"', "程序报错", + "四种完整配置统一比较", + "成功结果没有逐条语义盲审", ): if marker not in analysis_index: raise ValueError(f"analysis is missing timing or scalable comparison: {marker}") + + guide_index = rendered_pages[report_dir / "index.html"] + for marker in ( + "首次成功与重试恢复", + "程序判分落点与复核根因", + 'class="verdict-strip"', + 'class="cause-bars"', + 'id="home-qualified-results"', + 'class="audit-ladder"', + 'id="home-resource-table"', + 'results/gpt-6-astra-chromium-699.html#verification', + ): + if marker not in guide_index: + raise ValueError(f"interpretation is missing an evidence-backed reading aid: {marker}") for forbidden in ( "中位数", "失败结果定位", @@ -558,17 +610,9 @@ def check( ): raise ValueError("result pages must not duplicate answer and evaluator presentations") html = rendered_pages[index] - standalone = (report_dir / "guide/article.html").read_text(encoding="utf-8") - if " list[Path]: "methodology/index.html", "analysis/index.html", "review/index.html", - "guide/index.html", "changes/index.html", "adoption/index.html", "reproduce/index.html", @@ -86,7 +85,7 @@ def check(report: Path, receipt: Path) -> None: browser = playwright.chromium.launch(executable_path=executable, headless=True) try: for route in routes: - for width, height in ((1440, 1000), (390, 844)): + for width, height in ((1440, 1000), (820, 1180), (390, 844)): for theme in ("light", "dark"): page = browser.new_page(viewport={"width": width, "height": height}) page.goto(route.resolve().as_uri(), wait_until="load") diff --git a/tests/browser_use/test_report.py b/tests/browser_use/test_report.py index 62fd1555a..b36a02c48 100644 --- a/tests/browser_use/test_report.py +++ b/tests/browser_use/test_report.py @@ -8,12 +8,17 @@ from harness.browser_use.report import ( _action_duration_cards, _attempt_table, + _automatic_failure_type, _best, _failure_diagnosis, + _failure_scope_counts, + _first_final_rows, + _common_success_resource_rows, _load_evidence_bundles, _logical_results, _paired_summary, _same_task_comparison, + _summary_comparison_rows, _verification_html, build_report, ) @@ -40,6 +45,52 @@ def test_best_marks_only_one_visibly_distinct_winner() -> None: ) +def test_interpretation_keeps_first_attempt_and_retry_recovery_separate( + monkeypatch, +) -> None: + monkeypatch.setattr(report_module, "MODEL_LABELS", {"model": "Model"}) + monkeypatch.setattr(report_module, "ENGINE_LABELS", {"chromium": "Chromium"}) + rows = [ + { + "model": "model", + "engine": "chromium", + "task_id": 1, + "score": 1, + "_source_executions": [{"score": 1}], + }, + { + "model": "model", + "engine": "chromium", + "task_id": 2, + "score": 1, + "_source_executions": [{"score": 0}, {"score": 1}], + }, + { + "model": "model", + "engine": "chromium", + "task_id": 3, + "score": 0, + "_source_executions": [{"score": 0}, {"score": 0}], + }, + ] + + assert _first_final_rows(rows) == [["Model", "Chromium", "1/3", 2, "+1", "2/3"]] + + +def test_failure_scope_summary_does_not_merge_browser_evidence_into_agent() -> None: + causes = { + "a": {"subcategory": {"code": "agent_retrieval"}}, + "b": {"subcategory": {"code": "benchmark_evaluator"}}, + "c": {"subcategory": {"code": "evidence_capture"}}, + } + + assert [(label, count) for label, count, _ in _failure_scope_counts(causes)] == [ + ("Agent / 模型", 1), + ("任务 / evaluator", 1), + ("浏览器证据接口", 1), + ] + + def test_trace_screenshot_labels_count_only_publishable_frames(tmp_path: Path) -> None: trace = tmp_path / "trace.zip" image_payloads = {} @@ -167,8 +218,8 @@ def test_reader_facing_ux_requires_home_core_tables_without_current_data( tmp_path: Path, ) -> None: pages = { - tmp_path / "index.html": '
    ', - tmp_path / "en/index.html": '
    ', + tmp_path / "index.html": '
    ', + tmp_path / "en/index.html": '
    ', tmp_path / "analysis/index.html": "普通读者可理解的说明", } @@ -182,8 +233,8 @@ def test_reader_facing_ux_requires_home_core_tables_without_current_data( @pytest.mark.parametrize("term", FORBIDDEN_READER_TERMS) def test_reader_facing_ux_rejects_unexplained_internal_terms(tmp_path: Path, term: str) -> None: pages = { - tmp_path / "index.html": '
    ', - tmp_path / "en/index.html": '
    ', + tmp_path / "index.html": '
    ', + tmp_path / "en/index.html": '
    ', tmp_path / "analysis/index.html": f"

    {term}

    ", } @@ -493,11 +544,10 @@ def test_report_publishes_one_final_logical_result_and_aggregates_cost( "BrowserTool 动作延迟", "action-card-grid", "同题全配置对比", - "browser-comparison", - 'id="configuration-matrix"', - 'id="clean-subset-table"', - 'id="analysis-core-metrics"', - "四种配置 · 单次调用墙钟时间", + "browser-comparison", + 'id="configuration-matrix"', + 'id="analysis-core-metrics"', + "按模型分层 · 单次调用墙钟时间", ): assert expected in analysis assert analysis.index("同题全配置对比") < analysis.index("BrowserTool 动作延迟") @@ -590,6 +640,113 @@ def test_report_publishes_one_final_logical_result_and_aggregates_cost( assert not list(output.rglob("*.svg")) +def test_common_success_resources_use_only_shared_first_attempts( + tmp_path: Path, monkeypatch +) -> None: + monkeypatch.setattr(report_module, "MODEL_LABELS", {"a": "A", "b": "B"}) + monkeypatch.setattr( + report_module, "ENGINE_LABELS", {"chromium": "Chromium", "moli": "Moli"} + ) + + def execution(task_id: int, model: str, engine: str, attempt: int, score: int) -> dict: + events = tmp_path / f"{task_id}-{model}-{engine}-{attempt}.json" + events.write_text("[]", encoding="utf-8") + return { + "task_id": task_id, + "model": model, + "engine": engine, + "attempt": attempt, + "score": score, + "duration_ms": 1000 * task_id + attempt, + "model_usage": {"total_tokens": 100 * task_id + attempt}, + "resource": {"memory_peak_bytes": 1048576 * task_id, "cpu_ms": 10 * task_id}, + "paths": {"events": str(events)}, + } + + logical = [] + for task_id in (1, 2, 3): + for model in ("a", "b"): + for engine in ("chromium", "moli"): + first_score = 1 + if task_id == 2 and model == "a" and engine == "moli": + first_score = 0 + if task_id == 3 and model == "b" and engine == "chromium": + first_score = 0 + sources = [execution(task_id, model, engine, 1, first_score)] + final_score = first_score + if task_id == 2 and first_score == 0: + sources.append(execution(task_id, model, engine, 2, 1)) + final_score = 1 + logical.append({**sources[-1], "score": final_score, "_source_executions": sources}) + + count, rows = _common_success_resource_rows(logical) + + assert count == 1 + assert {row[2] for row in rows} == {"1.0 秒"} + assert {row[5] for row in rows} == {"1.0 MiB"} + + +def test_summary_winners_are_scoped_to_all_configurations(monkeypatch) -> None: + monkeypatch.setattr(report_module, "MODEL_LABELS", {"a": "A", "b": "B"}) + monkeypatch.setattr( + report_module, "ENGINE_LABELS", {"chromium": "Chromium", "moli": "Moli"} + ) + + def metrics(passed: int) -> dict: + return { + "completed": 10, + "passed": passed, + "score": passed / 10, + **{ + name: float(passed * 1000 if "duration_ms" in name else passed) + for name in ( + "mean_duration_ms", + "p50_duration_ms", + "p95_duration_ms", + "mean_browser_tool_duration_ms", + "p50_browser_tool_duration_ms", + "p95_browser_tool_duration_ms", + "mean_tokens", + "p50_tokens", + "p95_tokens", + ) + }, + } + + rows = _summary_comparison_rows( + { + "a": {"chromium": metrics(8), "moli": metrics(7)}, + "b": {"chromium": metrics(3), "moli": metrics(4)}, + } + ) + + assert "comparison-winner" in str(rows[0][2]) + assert "comparison-winner" not in str(rows[1][2]) + assert "comparison-winner" not in str(rows[2][2]) + assert "comparison-winner" not in str(rows[3][2]) + for metric_column in (4, 5, 6): + assert "comparison-winner" in str(rows[2][metric_column]) + assert "comparison-winner" not in str(rows[3][metric_column]) + + +def test_unknown_failure_scope_is_rejected() -> None: + with pytest.raises(ValueError, match="unknown failure-cause scope"): + _failure_scope_counts( + {"result": {"subcategory": {"code": "new_unregistered_scope"}}} + ) + + +def test_unknown_failed_evaluator_combination_is_rejected() -> None: + with pytest.raises(ValueError, match="unknown failed evaluator combination"): + _automatic_failure_type( + { + "evaluators_results": [ + {"evaluator_name": "NewEvaluator", "score": 0} + ] + } + ) + + def test_report_renders_measured_runtime_identities(tmp_path: Path, monkeypatch) -> None: _use_single_model_profile(monkeypatch) evidence = tmp_path / "evidence" @@ -655,15 +812,13 @@ def test_report_renders_measured_runtime_identities(tmp_path: Path, monkeypatch) assert report.is_file() assert (output / "assets/webarena-autonomous-evaluation-flow.png").is_file() home_text = report.read_text(encoding="utf-8") - assert "Chromium 与 Moli 的 Browser Use 能力对比" in home_text + assert "审查合格比较集" in home_text methodology_text = (output / "methodology/index.html").read_text(encoding="utf-8") assert "webarena-autonomous-evaluation-flow.png" in methodology_text assert "ContainerManager.start(reset=True)" in methodology_text assert "192.168.5.2" in methodology_text assert "不含点击序列" in methodology_text for name in ( - "guide/index.html", - "guide/article.html", "changes/index.html", "datasets/index.html", "methodology/index.html", @@ -678,11 +833,11 @@ def test_report_renders_measured_runtime_identities(tmp_path: Path, monkeypatch) ): page = output / name assert page.is_file() - if name not in {"guide/article.html", "en/index.html"}: + if name != "en/index.html": assert "切换主题" in page.read_text(encoding="utf-8") expected_tasks = len(load_pilot("evaluation/browser-use/webarena-verified-pilot.json").tasks) assert len(list((output / "datasets").glob("[0-9]*.html"))) == expected_tasks - assert (output / "guide/article.html").read_text(encoding="utf-8").find(" Date: Mon, 14 Sep 2026 11:49:53 +0800 Subject: [PATCH 083/127] Audit browser results and isolate environment lanes --- evaluation/browser-use/current-report.json | 2 +- harness/browser_use/report.py | 27 ++++++++++++- harness/browser_use/report_i18n.py | 25 +++++++++++- harness/browser_use/runner.py | 38 +++++++++++++++---- .../browser-use-failure-cause-review.json | 12 +++--- research/browser-use-remediation.json | 15 ++++++-- research/report-changes.json | 8 ++-- scripts/check_browser_use_report.py | 1 + tests/browser_use/test_runner.py | 16 ++++++++ 9 files changed, 119 insertions(+), 25 deletions(-) diff --git a/evaluation/browser-use/current-report.json b/evaluation/browser-use/current-report.json index 90e185a84..c94b33a1f 100644 --- a/evaluation/browser-use/current-report.json +++ b/evaluation/browser-use/current-report.json @@ -33,7 +33,7 @@ "failure_cause_review": { "required_for_publication": true, "source_path": "research/browser-use-failure-cause-review.json", - "source_sha256": "08e555e6d991892780a8b967741f42921424299e53dac079a7901d47446106e9" + "source_sha256": "a11134919f5e7b9a65c601374211e20e7e940f82e6f363ed025c49b52336e721" }, "publication": { "base_url": "https://artifact-hub.app.lexmount.com", diff --git a/harness/browser_use/report.py b/harness/browser_use/report.py index a0fcf4a1c..4bd506f53 100644 --- a/harness/browser_use/report.py +++ b/harness/browser_use/report.py @@ -2290,7 +2290,7 @@ def _article_body( + _table(["模型", "浏览器", "Agent 链路 P50", "BrowserTool P50", "Tokens P50", "峰值内存 P50", "CPU 时间 P50"], resource_rows, table_id="home-resource-table", classes="core-comparison-table") + _resource_summary_cards((common_count, resource_rows)) + '

    Agent 链路还包含模型推理和页面等待,因此不能解读为纯内核性能。

    ' - f'

    工程结论

    {clean_task_count} 题审查合格比较集显示,四种端到端配置都完成了约九成任务,浏览器之间的单边差异集中在少数任务。有效失败主要来自 Agent 行为和浏览器证据接口,不再混入已确认的合同、站点或共享状态问题。

    Moli 的内存占用明显更低,但 HAR 完整性、工具耗时和部分任务兼容性还需修复;Chromium 在两个模型层都多通过任务,但当前每种配置只有一个正式样本,不能外推成总体优势。

    下一轮验证

    Task 77 可在串行锁下补跑;站点题先通过读写预检,合同矛盾题由上游修正或退出样本。任何补跑都覆盖同题四种配置,并作为新的完整证据批次发布。

    ' + f'

    工程结论

    {clean_task_count} 题审查合格比较集显示,四种端到端配置都完成了约九成任务,浏览器之间的单边差异集中在少数任务。有效失败主要来自 Agent 行为和浏览器证据接口,不再混入已确认的合同、站点或共享状态问题。

    Moli 的内存占用明显更低,但 HAR 完整性、工具耗时和部分任务兼容性还需修复;Chromium 在两个模型层都多通过任务,但当前每种配置只有一个正式样本,不能外推成总体优势。

    下一轮验证

    Task 77 与 771 可在串行锁下补跑;Task 339、475、600、625、676、699 的合同或 evaluator 修复已提交 上游 PR #45,合并后补跑;Task 528 在具备可移植的未提交表单验收前继续排除。站点题先通过读写预检。任何补跑都覆盖同题四种配置,并作为新的完整证据批次发布。

    ' ) @@ -3425,6 +3425,31 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat else '

    逐执行环境准备回执尚不完整;现有记录只能核对运行器配置与镜像身份。

    ' ), ) + reproduce += _section( + "环境隔离与并行边界", + "

    修正后的运行器把一个 Docker daemon 及其固定站点端口视为一个环境 lane。同一 lane 内的任务和浏览器必须串行:每次尝试先重置站点,再完成 Agent、证据收尾和 evaluator,整个周期由跨进程锁保护。缺少 Docker daemon 身份时直接停止,不降级为无锁执行。

    " + + _table( + ["运行方式", "是否允许", "隔离要求"], + [ + [ + "一套站点环境,Chromium 与 Moli 串行", + "允许,默认方案", + "同一 daemon 锁;每次尝试前重置并核对初态", + ], + [ + "一套站点环境,两个浏览器并行", + "不允许", + "账号不同仍会共享对象、索引、缓存和后台任务", + ], + [ + "多个任务并行", + "允许", + "每个 worker 使用独立 daemon、站点容器、网络、端口和可写卷;镜像只读层可复用", + ], + ], + ) + + '

    账号隔离只用于避免用户级对象冲突,不能替代环境 lane。Task 77/771 的旧证据来自修正前的共享状态,正式比较已将整题四种配置一并排除;只有新的完整四配置证据批次才能重新纳入。

    ', + ) reproduce += _section( "公开复现入口", f'

    lexmount/browser-eval 是代码、题集和锁文件的唯一来源。

    ./examples/browser_use/run.sh\nmake browser-use-review EVIDENCE="$BROWSER_USE_EVIDENCE"\nmake browser-use-report EVIDENCE="$BROWSER_USE_EVIDENCE"\nmake browser-use-report-check EVIDENCE="$BROWSER_USE_EVIDENCE"

    每次物理执行的脱敏回答、evaluator、事件和轨迹位于 artifacts/<result-id>/executions/<n>/;evidence-index.json 给出全部 {result_count} 个逻辑结果及其物理执行、下载路径和公开文件哈希。

    ', diff --git a/harness/browser_use/report_i18n.py b/harness/browser_use/report_i18n.py index d0040b61c..7d9ef3616 100644 --- a/harness/browser_use/report_i18n.py +++ b/harness/browser_use/report_i18n.py @@ -794,6 +794,21 @@ "运行部分 amd64 PHP 站点镜像": "Runs selected amd64 PHP site images", "站点镜像": "Site image", "镜像引用": "Image reference", + "环境隔离与并行边界": "Environment isolation and parallelism boundary", + "修正后的运行器把一个 Docker daemon 及其固定站点端口视为一个环境 lane。同一 lane 内的任务和浏览器必须串行:每次尝试先重置站点,再完成 Agent、证据收尾和 evaluator,整个周期由跨进程锁保护。缺少 Docker daemon 身份时直接停止,不降级为无锁执行。": "The corrected runner treats one Docker daemon and its fixed site ports as one environment lane. Tasks and browsers in the same lane run serially: each attempt resets the sites first, then completes the Agent run, evidence finalization, and evaluation while one cross-process lock protects the full lifecycle. If the Docker daemon identity is unavailable, execution stops instead of falling back to an unlocked run.", + "运行方式": "Execution mode", + "是否允许": "Allowed", + "隔离要求": "Isolation requirement", + "一套站点环境,Chromium 与 Moli 串行": "One site stack; Chromium and Moli run serially", + "允许,默认方案": "Allowed; default", + "同一 daemon 锁;每次尝试前重置并核对初态": "One daemon-scoped lock; reset and verify initial state before every attempt", + "一套站点环境,两个浏览器并行": "One site stack; two browsers run in parallel", + "不允许": "Not allowed", + "账号不同仍会共享对象、索引、缓存和后台任务": "Separate accounts still share objects, indexes, caches, and background jobs", + "多个任务并行": "Multiple tasks in parallel", + "允许": "Allowed", + "每个 worker 使用独立 daemon、站点容器、网络、端口和可写卷;镜像只读层可复用": "Each worker uses an independent daemon, site containers, network, ports, and writable volumes; read-only image layers may be shared", + "账号隔离只用于避免用户级对象冲突,不能替代环境 lane。Task 77/771 的旧证据来自修正前的共享状态,正式比较已将整题四种配置一并排除;只有新的完整四配置证据批次才能重新纳入。": "Account isolation only prevents user-scoped object conflicts; it does not replace an environment lane. The old evidence for Tasks 77 and 771 came from shared state before this correction, so all four configurations for each task are excluded from the formal comparison. Readmission requires a new complete four-configuration evidence cohort.", "逐执行的环境准备 receipt 尚未包含在这批冻结证据中;现有记录可核对运行器配置与镜像身份,但不能独立重放每次 reset/ready/health 的完整证明链。": "Per-execution environment-preparation receipts are absent from this frozen evidence. Existing records verify runner configuration and image identities but cannot independently replay the complete reset/ready/health proof chain for every execution.", "每个逻辑结果的脱敏回答、evaluator、事件和轨迹位于": "Sanitized answers, evaluator records, events, and trajectories for each logical result are under", "给出全部 60 个逻辑结果的页面与下载路径。": "lists page and download paths for all 60 logical results.", @@ -1299,6 +1314,9 @@ def _register_report_change_translations() -> None: "耗时统计包含什么": "What the latency statistics include", "Agent 总耗时包含模型推理、页面等待和浏览器操作;BrowserTool 耗时是全部浏览器工具调用的墙钟时间之和,仍包含工具封装与 CDP 往返,不是纯内核 CPU 时间。": "Agent latency includes model inference, page waits, and browser operations. BrowserTool latency is the sum of wall-clock time across browser-tool calls; it still includes tool-wrapper and CDP round-trip overhead and is not pure browser-core CPU time.", "Agent / 模型行为": "Agent / model behavior", + "全部 76 题的速度与浏览器资源": "Performance and browser resources across all 76 tasks", + "均值、P50、P95 都在四种完整配置中统一比较。CPU、内存和网络只计浏览器容器,不含 CowAgent 或模型服务;模型会改变动作数量和页面停留时间,因此这里比较整套配置,不能单独归因于浏览器内核。": "Mean, P50, and P95 are compared across all four complete configurations. CPU, memory, and network measurements cover only the browser container, not CowAgent or the model service. Because the model can change the action count and time spent on pages, these are whole-configuration measurements and cannot be attributed to the browser core alone.", + "每项只标记四种配置中数值唯一最低者。CPU 时间与网络总量累计逻辑结果使用的全部执行;内存来自 Browser Use 执行阶段的浏览器容器采样。": "Only a unique lowest value across the four configurations is marked for each statistic. CPU time and network totals include every execution used by a logical result; memory comes from browser-container samples during Browser Use execution.", "浏览器证据接口": "Browser evidence interface", "原因范围": "Cause scope", "本轮处理": "Treatment in this run", @@ -1501,7 +1519,7 @@ def _register_report_change_translations() -> None: "共享环境": "Shared environment", "并发执行改写了任务初始状态": "Concurrent execution changed the task's initial state", "参考漂移与 Agent 遗漏同时存在": "Reference drift and an agent omission both contributed", - "Agent / 模型行为": "Agent/model behavior", + "Agent / 模型行为": "Agent / model behavior", "保留并修复": "Retain and fix", "Agent 行为": "Agent behavior", "例如漏掉商品、选错最优候选、写错字段,或在 40 步前提前结束。这些属于当前 Agent × 模型配置的真实失败。": "Examples include missing products, choosing the wrong candidate, writing the wrong field, or stopping before the 40-step limit. These are genuine failures of the current agent-model configuration.", @@ -1559,6 +1577,9 @@ def _register_report_change_translations() -> None: "对工程选型有用的结论是:Moli 的内存占用明显更低,但当前 HAR 完整性、工具耗时和部分任务兼容性还需修复;Chromium 在 76 题诊断子集中两个模型层都多通过任务,但当前只有一个正式样本,不能外推成总体优势。": "For engineering decisions, Moli uses substantially less memory, while HAR completeness, tool latency, and some task compatibility still need work. Chromium passes more tasks in both model cohorts on the 76-task diagnostic subset, but one formal sample cannot establish a general advantage.", "下一轮只做两件事": "Two priorities for the next run", "下一轮验证": "Next validation run", + "Task 77 与 771 可在串行锁下补跑;Task 339、475、600、625、676、699 的合同或 evaluator 修复已提交": "Tasks 77 and 771 can be rerun under the serialization lock. Contract or evaluator fixes for tasks 339, 475, 600, 625, 676, and 699 have been submitted in", + "上游 PR #45": "upstream PR #45", + ",合并后补跑;Task 528 在具备可移植的未提交表单验收前继续排除。站点题先通过读写预检。任何补跑都覆盖同题四种配置,并作为新的完整证据批次发布。": ". They will be rerun after the fix is merged. Task 528 remains excluded until a portable evaluator can verify an unsubmitted filled form. Site-dependent tasks must first pass read/write preflight checks. Every rerun covers all four configurations for the task and is published as a new complete evidence cohort.", "先修复并重跑 14 题干扰项,再对同一冻结代码的四种配置增加重复次数。在此之前,这份报告用于定位兼容性和证据链问题,不发布模型或浏览器排名。": "First fix and rerun the 14 interference tasks; then add repetitions for all four configurations on one frozen codebase. Until then, this report is for locating compatibility and evidence-chain defects, not for publishing a model or browser ranking.", "先修复并重跑 14 题干扰项,再对同一冻结代码的四种配置增加重复次数。在此之前,这份报告适合找兼容性和证据链问题,不适合发布模型或浏览器排名。": "First fix and rerun the 14 interference tasks; then add repetitions for all four configurations on one frozen codebase. Until then, this report is useful for locating compatibility and evidence-chain defects, not for publishing a model or browser ranking.", "查看全部配置的逐题矩阵 →": "View the per-task matrix for all configurations →", @@ -1663,7 +1684,7 @@ def translate_text(value: str) -> str: ), ( r"(\d+) 个失败结果已经逐条核验。与任务合同、站点、共享环境或参考状态有关的任务不会进入诊断子集,修复后需完整重跑。", - lambda m: f"All {m.group(1)} failed results received case-by-case review. Tasks affected by acceptance contracts, sites, shared state, or reference state are excluded from the diagnostic subset and require a complete rerun after correction.", + lambda m: f"All {m.group(1)} failed results have been reviewed individually. Tasks affected by task-contract, site, shared-environment, or reference-state issues are excluded from the diagnostic subset and require a complete rerun after correction.", ), ( r"干扰来自验收合同、站点、共享环境或参考状态。官方分数保留,另用 (\d+) 题事后诊断子集观察目标变量。", diff --git a/harness/browser_use/runner.py b/harness/browser_use/runner.py index a67d50824..0284ea66c 100644 --- a/harness/browser_use/runner.py +++ b/harness/browser_use/runner.py @@ -91,7 +91,18 @@ QEMU_X86_64_CONTAINER_PATH = "/usr/local/bin/qemu-x86_64" -def _physical_execution_lock_path() -> Path: +def _docker_daemon_lock_namespace(docker_info: dict[str, Any]) -> str: + """Bind a serialization lane to one Docker daemon and its fixed site ports.""" + daemon_id = str(docker_info.get("ID") or "").strip() + if not daemon_id: + raise RuntimeError("Docker daemon ID is required for environment-lane isolation") + suffix = hashlib.sha256(daemon_id.encode("utf-8")).hexdigest()[:16] + return f"{PHYSICAL_EXECUTION_LOCK_NAMESPACE}-{suffix}" + + +def _physical_execution_lock_path( + lock_namespace: str = PHYSICAL_EXECUTION_LOCK_NAMESPACE, +) -> Path: # The WebArena stack uses fixed host ports and is shared by every run owned # by this OS user. The lock therefore must not follow BROWSER_EVAL_DATA_HOME: # two cohorts may intentionally write to different evidence directories @@ -100,7 +111,7 @@ def _physical_execution_lock_path() -> Path: Path(tempfile.gettempdir()) / f"browser-eval-{os.getuid()}" / "locks" - / f"{PHYSICAL_EXECUTION_LOCK_NAMESPACE}.lock" + / f"{lock_namespace}.lock" ) @@ -613,13 +624,14 @@ def _resource_summary( def _run_attempt( # noqa: PLR0913 *, lock_path: Path, + lock_namespace: str = PHYSICAL_EXECUTION_LOCK_NAMESPACE, **kwargs: Any, ) -> dict[str, Any]: with _physical_execution_lock(lock_path) as lock_wait_ms: return _run_attempt_locked( **kwargs, lock_wait_ms=lock_wait_ms, - lock_namespace=PHYSICAL_EXECUTION_LOCK_NAMESPACE, + lock_namespace=lock_namespace, ) @@ -915,7 +927,6 @@ def run(args: argparse.Namespace) -> Path: # noqa: PLR0915 output_dir = Path(args.output).expanduser().resolve() output_dir.mkdir(parents=True, exist_ok=True) - physical_execution_lock_path = _physical_execution_lock_path() selection = load_pilot(args.selection) benchmark = _configure_benchmark() resolved = validate_against_official(selection, benchmark) @@ -928,6 +939,11 @@ def run(args: argparse.Namespace) -> Path: # noqa: PLR0915 model.verify_access() client = docker.from_env() + docker_info = client.info() + physical_execution_lock_namespace = _docker_daemon_lock_namespace(docker_info) + physical_execution_lock_path = _physical_execution_lock_path( + physical_execution_lock_namespace + ) run_id = uuid.uuid4().hex[:12] containers: dict[str, Any] = {} networks: dict[str, Any] = {} @@ -951,8 +967,7 @@ def run(args: argparse.Namespace) -> Path: # noqa: PLR0915 } browser_metadata: dict[str, dict[str, str]] = {} site_access_host = "" - docker_info: dict[str, Any] = {} - docker_architecture = str(client.info().get("Architecture") or "") + docker_architecture = str(docker_info.get("Architecture") or "") site_runtime_compatibility = _site_runtime_compatibility(docker_architecture) try: for engine in ENGINE_IMAGES: @@ -1006,6 +1021,10 @@ def run(args: argparse.Namespace) -> Path: # noqa: PLR0915 capture_output=True, text=True, ).stdout.strip(), + "environment_lane": { + "lock_namespace": physical_execution_lock_namespace, + "isolation_unit": "docker-daemon-and-fixed-site-ports", + }, "site_runtime_compatibility": site_runtime_compatibility, }, ) @@ -1040,6 +1059,7 @@ def run(args: argparse.Namespace) -> Path: # noqa: PLR0915 break row = _run_attempt( lock_path=physical_execution_lock_path, + lock_namespace=physical_execution_lock_namespace, benchmark=benchmark, selected=selected, engine=engine, @@ -1063,7 +1083,6 @@ def run(args: argparse.Namespace) -> Path: # noqa: PLR0915 print(json.dumps(visible), flush=True) if row["score"] == 1.0: break - docker_info = client.info() finally: for container in containers.values(): try: @@ -1111,6 +1130,11 @@ def run(args: argparse.Namespace) -> Path: # noqa: PLR0915 "evaluator": "official webarena-verified package, unmodified", "environment_reset": "official site container reset before every attempt", "environment_prepare_receipt": "required", + "environment_lane": { + "lock_namespace": physical_execution_lock_namespace, + "isolation_unit": "docker-daemon-and-fixed-site-ports", + "parallelism_rule": "one active attempt per Docker daemon", + }, "engine_order": "alternating by task", "sites": [site.value for site in SITE_PORTS], "official_container_hostname_allowlist": ["host.docker.internal"], diff --git a/research/browser-use-failure-cause-review.json b/research/browser-use-failure-cause-review.json index cf6849387..5a7fac466 100644 --- a/research/browser-use-failure-cause-review.json +++ b/research/browser-use-failure-cause-review.json @@ -48,12 +48,12 @@ "gpt-5.6-sol-moli-615": {"status":"verified","subcategory":{"code":"agent_field_omission","zh":"提交字段遗漏","en":"Submission-field omission"},"reason":{"zh":"发帖请求包含正确标题和 Connecticut 论坛 ID,但 submission[url] 为空,遗漏 Bald Eagle 图片 URL。","en":"The post has the correct title and Connecticut forum ID, but submission[url] is empty, omitting the Bald Eagle image URL."}}, "gpt-5.6-sol-chromium-625": {"status":"verified","subcategory":{"code":"benchmark_route_contract","zh":"路由验收缺陷","en":"Route-acceptance defect"},"reason":{"zh":"合法的 /submit/technology 302 POST 含正确标题、正文和论坛 ID 10043;evaluator 只接受精确 /submit。","en":"The valid /submit/technology 302 POST contains the correct title, body, and forum ID 10043; the evaluator accepts only exact /submit."}}, "gpt-5.6-sol-moli-625": {"status":"verified","subcategory":{"code":"benchmark_route_contract","zh":"路由验收缺陷","en":"Route-acceptance defect"},"reason":{"zh":"合法的 /submit/technology 302 POST 含正确标题、正文和论坛 ID 10043;evaluator 只接受精确 /submit。","en":"The valid /submit/technology 302 POST contains the correct title, body, and forum ID 10043; the evaluator accepts only exact /submit."}}, - "gpt-5.6-sol-chromium-676": {"status":"verified","subcategory":{"code":"benchmark_route_contract","zh":"路由验收缺陷","en":"Route-acceptance defect"},"reason":{"zh":"HAR 有完整 sales_order_grid 与 status=fraud 请求,Referer 为合法的 /sales/order/;官方规则把尾斜杠差异判为失败。","en":"The HAR has the complete sales_order_grid request with status=fraud and a valid /sales/order/ Referer; the official rule fails on the trailing slash."}}, + "gpt-5.6-sol-chromium-676": {"status":"verified","subcategory":{"code":"benchmark_evaluator_selection","zh":"网络事件选择缺陷","en":"Network-event selection defect"},"reason":{"zh":"HAR 有完整 sales_order_grid 与 status=fraud 的 XHR。官方 evaluator 对 navigate + GET 只检查最后一次文档导航,随后又在同一路径事件中只取最后一条 notification_area,因而漏掉真正应验收的筛选请求;修复选择逻辑后,该证据可通过。","en":"The HAR contains the complete sales_order_grid XHR with status=fraud. For navigate-plus-GET tasks, the official evaluator checks only the last document navigation and then only the last same-route event, which skips the request that should be evaluated. The saved evidence passes after correcting event selection."}}, "gpt-5.6-sol-moli-676": {"status":"verified","subcategory":{"code":"evidence_capture","zh":"网络证据缺失","en":"Missing network evidence"},"reason":{"zh":"HAR 有完整 sales_order_grid 与 status=fraud 请求,但 Moli 未记录 Referer,evaluator 无法匹配。","en":"The HAR has the complete sales_order_grid request with status=fraud, but Moli omits its Referer, preventing evaluator matching."}}, "gpt-5.6-sol-chromium-694": {"status":"verified","subcategory":{"code":"site_backend_failure","zh":"站点保存故障","en":"Site save failure"},"reason":{"zh":"Agent 两次创建商品均被 Magento 以 The stock item was unable to be saved 拒绝,目录仍为空。","en":"Magento rejected both product-creation attempts with The stock item was unable to be saved, leaving the catalog empty."}}, "gpt-5.6-sol-moli-694": {"status":"verified","subcategory":{"code":"site_backend_failure","zh":"站点保存故障","en":"Site save failure"},"reason":{"zh":"Agent 两次创建商品均被 Magento 以 The stock item was unable to be saved 拒绝,商品未创建。","en":"Magento rejected both product-creation attempts with The stock item was unable to be saved, so the product was not created."}}, - "gpt-5.6-sol-chromium-699": {"status":"verified","subcategory":{"code":"benchmark_evaluator_defect","zh":"evaluator 实现错误","en":"Evaluator implementation defect"},"reason":{"zh":"evaluator 在比较实际提交前即因单元素 Alternatives 配置抛出 Alternatives require 2+ items,零分来自官方规则错误。","en":"Before comparing the submission, the evaluator throws Alternatives require 2+ items for a one-item configuration; the zero comes from the official rule."}}, - "gpt-5.6-sol-moli-699": {"status":"verified","subcategory":{"code":"benchmark_evaluator_defect","zh":"evaluator 实现错误","en":"Evaluator implementation defect"},"reason":{"zh":"evaluator 在比较实际提交前即因单元素 Alternatives 配置抛出 Alternatives require 2+ items,零分来自官方规则错误。","en":"Before comparing the submission, the evaluator throws Alternatives require 2+ items for a one-item configuration; the zero comes from the official rule."}}, + "gpt-5.6-sol-chromium-699": {"status":"verified","subcategory":{"code":"benchmark_evaluator_defect","zh":"evaluator 配置错误并伴随 Agent 选型错误","en":"Evaluator configuration defect with an Agent selection error"},"reason":{"zh":"官方 evaluator 把两个单值表单字段写成单元素 Alternatives,尚未比较请求就抛错。用正确字段重放后,证据显示 Agent 保存的是 Catalog Price Rule,而合同要求 Cart Price Rule,因此这条历史结果不能改判为通过。","en":"The official evaluator encodes two scalar form fields as one-item Alternatives and errors before comparing the request. Re-evaluation with corrected fields shows that the Agent saved a Catalog Price Rule while the contract requires a Cart Price Rule, so the historical result cannot be converted to a pass."}}, + "gpt-5.6-sol-moli-699": {"status":"verified","subcategory":{"code":"benchmark_evaluator_defect","zh":"evaluator 配置错误并伴随 Agent 选型错误","en":"Evaluator configuration defect with an Agent selection error"},"reason":{"zh":"官方 evaluator 把两个单值表单字段写成单元素 Alternatives,尚未比较请求就抛错。用正确字段重放后,证据显示 Agent 保存的是 Catalog Price Rule,而合同要求 Cart Price Rule,因此这条历史结果不能改判为通过。","en":"The official evaluator encodes two scalar form fields as one-item Alternatives and errors before comparing the request. Re-evaluation with corrected fields shows that the Agent saved a Catalog Price Rule while the contract requires a Cart Price Rule, so the historical result cannot be converted to a pass."}}, "gpt-5.6-sol-chromium-771": {"status":"verified","subcategory":{"code":"agent_workflow_completeness","zh":"批量操作遗漏","en":"Incomplete batch workflow"},"reason":{"zh":"任务要求批准全部至少 4 星评论;Chromium 没有保存官方要求的评论 352 和 347,而 Moli 在相同条件下完成了两项。","en":"The task requires approving all reviews rated at least four stars; Chromium did not save required reviews 352 and 347, while Moli completed both under the same controls."}} } }, @@ -103,12 +103,12 @@ "gpt-6-astra-chromium-585": {"status":"verified","subcategory":{"code":"agent_workflow_completeness","zh":"可恢复错误后提前终止","en":"Premature stop after recoverable error"},"reason":{"zh":"进入订单历史时出现可恢复的 snapshot 导航竞态;HAR 显示页面已返回 200,但 Agent 直接终止,未继续定位落地灯或提交评论。","en":"A recoverable snapshot/navigation race occurred while opening order history. HAR shows the page returned 200, but the Agent stopped without locating the floor lamp or submitting a review."}}, "gpt-6-astra-moli-595": {"status":"verified","subcategory":{"code":"evidence_capture","zh":"网络证据缺失","en":"Missing network evidence"},"reason":{"zh":"订阅实际成功,页面变为 Unsubscribe,trace 响应为 subscribed=true;Moli HAR 缺 Referer 和响应 JSON,evaluator 因而无法匹配。","en":"The subscription succeeded: the UI changed to Unsubscribe and trace returned subscribed=true. Moli HAR omitted the Referer and response JSON, preventing evaluator matching."}}, "gpt-6-astra-chromium-605": {"status":"verified","subcategory":{"code":"site_dependency_failure","zh":"站点会话故障","en":"Site session failure"},"reason":{"zh":"Agent 提交了正确论坛、标题和与新表单一致的 CSRF token;站点连续两次仍返回 CSRF token is invalid。","en":"The Agent submitted the correct forum, title, and CSRF token from the freshly loaded form, yet the site rejected both executions with CSRF token is invalid."}}, - "gpt-6-astra-chromium-676": {"status":"verified","subcategory":{"code":"benchmark_route_contract","zh":"路由验收缺陷","en":"Route-acceptance defect"},"reason":{"zh":"Agent 正确筛选 status=fraud,页面显示 Suspected Fraud;官方规则仅因 Referer 带 /sales/order/ 尾斜杠而拒绝。","en":"The Agent correctly filtered status=fraud and the UI showed Suspected Fraud; the official rule rejected the request only because the Referer used trailing-slash /sales/order/."}}, + "gpt-6-astra-chromium-676": {"status":"verified","subcategory":{"code":"benchmark_evaluator_selection","zh":"网络事件选择缺陷","en":"Network-event selection defect"},"reason":{"zh":"Agent 正确筛选 status=fraud,页面显示 Suspected Fraud,HAR 也有完整 XHR。官方 evaluator 的 navigate + GET 快捷逻辑只检查文档导航,且默认只取同路径最后一条事件,漏掉目标筛选请求;修复后保存证据可通过。","en":"The Agent correctly filtered status=fraud, the UI showed Suspected Fraud, and the HAR contains the complete XHR. The official navigate-plus-GET shortcut checks only document navigation and then defaults to the last same-route event, skipping the target filter request. The saved evidence passes after the fix."}}, "gpt-6-astra-moli-676": {"status":"verified","subcategory":{"code":"evidence_capture","zh":"网络证据缺失","en":"Missing network evidence"},"reason":{"zh":"Agent 发出完整的欺诈订单筛选请求且页面显示 Suspected Fraud;Moli 未记录该 AJAX 请求的 Referer,evaluator 无法匹配。","en":"The Agent issued the complete fraudulent-order filter request and the UI showed Suspected Fraud, but Moli omitted the AJAX Referer, preventing evaluator matching."}}, "gpt-6-astra-chromium-694": {"status":"verified","subcategory":{"code":"site_backend_failure","zh":"站点保存故障","en":"Site save failure"},"reason":{"zh":"合同字段均正确且网络验收通过;Magento 随后返回 The stock item was unable to be saved,商品未创建。","en":"All contract fields were correct and network acceptance passed, but Magento returned The stock item was unable to be saved and did not create the product."}}, "gpt-6-astra-moli-694": {"status":"verified","subcategory":{"code":"site_backend_failure","zh":"站点保存故障","en":"Site save failure"},"reason":{"zh":"合同字段和 Top attribute set 均正确且网络验收通过;Magento 返回库存项保存失败,商品未创建。","en":"The contract fields and Top attribute set were correct and network acceptance passed, but Magento returned a stock-item save failure and did not create the product."}}, - "gpt-6-astra-chromium-699": {"status":"verified","subcategory":{"code":"benchmark_evaluator_defect","zh":"evaluator 实现错误","en":"Evaluator implementation defect"},"reason":{"zh":"规则已按要求保存;evaluator 在比较实际请求前因单元素 Alternatives 配置抛出 Alternatives require 2+ items。","en":"The rule was saved as required, but the evaluator threw Alternatives require 2+ items for a one-item configuration before comparing the request."}}, - "gpt-6-astra-moli-699": {"status":"verified","subcategory":{"code":"benchmark_evaluator_defect","zh":"evaluator 实现错误","en":"Evaluator implementation defect"},"reason":{"zh":"规则已按要求保存并应用;零分来自 evaluator 的单元素 Alternatives 异常。","en":"The rule was saved and applied as required; the zero comes from the evaluator's one-item Alternatives exception."}}, + "gpt-6-astra-chromium-699": {"status":"verified","subcategory":{"code":"benchmark_evaluator_defect","zh":"evaluator 配置错误并伴随 Agent 选型错误","en":"Evaluator configuration defect with an Agent selection error"},"reason":{"zh":"官方 evaluator 把两个单值表单字段写成单元素 Alternatives,尚未比较请求就抛错。用正确字段重放后,证据显示 Agent 保存的是 Catalog Price Rule,而合同要求 Cart Price Rule,因此这条历史结果不能改判为通过。","en":"The official evaluator encodes two scalar form fields as one-item Alternatives and errors before comparing the request. Re-evaluation with corrected fields shows that the Agent saved a Catalog Price Rule while the contract requires a Cart Price Rule, so the historical result cannot be converted to a pass."}}, + "gpt-6-astra-moli-699": {"status":"verified","subcategory":{"code":"benchmark_evaluator_defect","zh":"evaluator 配置错误并伴随 Agent 选型错误","en":"Evaluator configuration defect with an Agent selection error"},"reason":{"zh":"官方 evaluator 把两个单值表单字段写成单元素 Alternatives,尚未比较请求就抛错。用正确字段重放后,证据显示 Agent 保存的是 Catalog Price Rule,而合同要求 Cart Price Rule,因此这条历史结果不能改判为通过。","en":"The official evaluator encodes two scalar form fields as one-item Alternatives and errors before comparing the request. Re-evaluation with corrected fields shows that the Agent saved a Catalog Price Rule while the contract requires a Cart Price Rule, so the historical result cannot be converted to a pass."}}, "gpt-6-astra-chromium-771": {"status":"verified","subcategory":{"code":"benchmark_instruction_contract","zh":"任务状态与验收矛盾","en":"State-evaluator contradiction"},"reason":{"zh":"最终执行开始时评论 352、347 已是 Approved,任务语义已经满足;evaluator 仍强制要求重复提交 status_id=1。","en":"Reviews 352 and 347 were already Approved at the start of the final execution, so the task was already satisfied, while the evaluator still required redundant status_id=1 submissions."}}, "gpt-6-astra-moli-771": {"status":"verified","subcategory":{"code":"benchmark_instruction_contract","zh":"任务状态与验收矛盾","en":"State-evaluator contradiction"},"reason":{"zh":"最终执行开始时评论 352、347 已是 Approved,任务语义已经满足;evaluator 仍强制要求重复提交 status_id=1。","en":"Reviews 352 and 347 were already Approved at the start of the final execution, so the task was already satisfied, while the evaluator still required redundant status_id=1 submissions."}} } diff --git a/research/browser-use-remediation.json b/research/browser-use-remediation.json index b11012323..7d31164a0 100644 --- a/research/browser-use-remediation.json +++ b/research/browser-use-remediation.json @@ -5,15 +5,22 @@ "items": [ { "category": "shared_environment", - "task_ids": [77], + "task_ids": [77, 771], "status": "implementation_fixed_rerun_required", "action": "The full physical-attempt lifecycle is now protected by a cross-process lock. Rerun the affected task before admitting it to the clean comparison." }, { "category": "benchmark_or_evaluator", - "task_ids": [339, 475, 528, 600, 625, 676, 699, 771], - "status": "quarantined_pending_contract_fix", - "action": "Correct and independently validate the task contract or evaluator, then preregister and rerun the complete four-configuration scope." + "task_ids": [339, 475, 600, 625, 676, 699], + "status": "correction_implemented_upstream_review_and_rerun_required", + "upstream_pr": "https://github.com/ServiceNow/webarena-verified/pull/45", + "action": "The corrected contracts and network-event selection logic pass regression tests and replay checks against saved HAR evidence. Merge the upstream correction, then rerun the complete four-configuration scope before readmission." + }, + { + "category": "benchmark_instruction_contract", + "task_ids": [528], + "status": "quarantined_pending_portable_evaluator", + "action": "Replace the synthetic dummy_bin POST requirement with a portable observable-state evaluator for the filled-but-unsubmitted form, then validate and rerun all four configurations." }, { "category": "self_hosted_site", diff --git a/research/report-changes.json b/research/report-changes.json index 2aa8f9268..cb45558c5 100644 --- a/research/report-changes.json +++ b/research/report-changes.json @@ -30,14 +30,14 @@ "status": "已处理", "scope": "GPT-5.6 Sol 与 GPT-6 Astra 两个模型层、360 个逻辑结果、438 次物理执行、75 个最终失败,以及共享 WebArena 站点栈的执行边界。", "findings": "Sol 层为 Chromium 73/90、Moli 70/90;Astra 层两者均为 71/90。Astra Task 77 的两个失败来自共享 Shopping Admin 状态在准备后被并发执行改写,不是 Agent 误读或镜像基线错误。两个模型层使用的 browser-eval 提交不同,因此跨模型变化只能作观察性比较。", - "corrections": "所有模型、浏览器和任务仍只保留一个逻辑结果;公开包补齐每次物理执行的脱敏证据与哈希;75/75 个失败均绑定当前证据并核验根因。运行器现在用跨进程文件锁覆盖 reset、Agent、证据收尾和 evaluator 的完整执行生命周期。", + "corrections": "所有模型、浏览器和任务仍只保留一个逻辑结果;公开包补齐每次物理执行的脱敏证据与哈希;75/75 个失败均绑定当前证据并核验根因。运行器现在用按 Docker daemon 区分的跨进程文件锁覆盖 reset、Agent、证据收尾和 evaluator;同一环境 lane 串行。", "remaining": "历史官方分数不以人工核验覆盖;Task 77 只有在修复后的隔离环境中形成新的完整证据批次后才能更新成绩。真正并行执行仍要求每个 worker 拥有独立的完整站点栈、端口和浏览器容器。", "i18n": {"en": { "title": "Astra model layer, per-execution evidence, and environment-isolation review", "status": "Resolved", "scope": "The GPT-5.6 Sol and GPT-6 Astra model layers, 360 logical results, 438 physical executions, 75 final failures, and the execution boundary of the shared WebArena site stack.", "findings": "In the Sol layer Chromium scored 73/90 and Moli 70/90; in the Astra layer both scored 71/90. The two Astra Task 77 failures came from concurrent modification of shared Shopping Admin state after preparation, not an Agent reading error or a bad image baseline. The model layers used different browser-eval commits, so cross-model changes are observational only.", - "corrections": "Kept one logical result per model, browser, and task; published sanitized evidence and hashes for every physical execution; and bound all 75 failures to verified root causes. The runner now holds a cross-process lock across reset, Agent execution, evidence finalization, and evaluation.", + "corrections": "Kept one logical result per model, browser, and task; published sanitized evidence and hashes for every physical execution; and bound all 75 failures to verified root causes. The runner now holds a Docker-daemon-scoped cross-process lock across reset, Agent execution, evidence finalization, and evaluation, serializing every run within one environment lane.", "remaining": "Human review does not override historical official scores. Task 77 can change only after a new complete evidence cohort is produced under the corrected isolation protocol. Safe parallel execution still requires a separate full site stack, ports, and browser containers for every worker." }} }, @@ -185,7 +185,7 @@ [ "Task 77", "Astra 的两条 Task 77 证据在 reset 后被另一个进程改写共享 Shopping Admin 状态。", - "运行器已用跨进程锁覆盖 reset、Agent、证据收尾和 evaluator;旧污染结果退出分母,只有四配置新证据才能重新纳入。", + "运行器已用按 Docker daemon 区分的跨进程锁覆盖 reset、Agent、证据收尾和 evaluator;同一环境 lane 串行,不同 lane 才能并行。旧污染结果退出分母,只有四配置新证据才能重新纳入。", "Task 77 不再影响首页成绩;历史执行仍可下钻核对。" ], [ @@ -209,7 +209,7 @@ [ "Task 77", "Both Astra Task 77 executions had shared Shopping Admin state modified by another process after reset.", - "A cross-process lock now covers reset, agent execution, evidence finalization, and evaluation. The contaminated results leave the denominator and can return only through new four-configuration evidence.", + "A Docker-daemon-scoped cross-process lock now covers reset, agent execution, evidence finalization, and evaluation. Runs are serial within one environment lane and parallel only across independent lanes. The contaminated results leave the denominator and can return only through new four-configuration evidence.", "Task 77 no longer affects the homepage scores; its historical executions remain available for inspection." ], [ diff --git a/scripts/check_browser_use_report.py b/scripts/check_browser_use_report.py index 6ca5a70e5..bb7745c21 100644 --- a/scripts/check_browser_use_report.py +++ b/scripts/check_browser_use_report.py @@ -857,6 +857,7 @@ def check( in { "shared_environment", "benchmark_or_evaluator", + "benchmark_instruction_contract", "self_hosted_site", "reference_state_mixed", } diff --git a/tests/browser_use/test_runner.py b/tests/browser_use/test_runner.py index e2e9170a1..088536a44 100644 --- a/tests/browser_use/test_runner.py +++ b/tests/browser_use/test_runner.py @@ -14,6 +14,7 @@ _allow_official_container_hostname, _configure_benchmark, _container_host_gateway, + _docker_daemon_lock_namespace, _environment_prepare_receipt, _exhausted_agent_steps, _limit_cowagent_transport_retries, @@ -316,6 +317,21 @@ def test_physical_execution_lock_path_is_user_global_not_evidence_scoped( ) +def test_environment_lane_is_bound_to_the_docker_daemon() -> None: + first = _docker_daemon_lock_namespace({"ID": "daemon-a"}) + second = _docker_daemon_lock_namespace({"ID": "daemon-b"}) + + assert first.startswith(f"{PHYSICAL_EXECUTION_LOCK_NAMESPACE}-") + assert first != second + assert _docker_daemon_lock_namespace({"ID": "daemon-a"}) == first + assert _physical_execution_lock_path(first).name == f"{first}.lock" + + +def test_environment_lane_fails_closed_without_a_docker_daemon_id() -> None: + with pytest.raises(RuntimeError, match="Docker daemon ID"): + _docker_daemon_lock_namespace({}) + + def test_prepare_receipt_records_lock_wait_and_namespace_without_path() -> None: receipt = _environment_prepare_receipt( (WebArenaSite.REDDIT,), From 582162dbefa1059584c15cd83dfa8eeb7fecfe1e Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 14 Sep 2026 11:54:14 +0800 Subject: [PATCH 084/127] Sync browser report publication baseline --- evaluation/browser-use/current-report.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evaluation/browser-use/current-report.json b/evaluation/browser-use/current-report.json index c94b33a1f..31b770596 100644 --- a/evaluation/browser-use/current-report.json +++ b/evaluation/browser-use/current-report.json @@ -39,7 +39,7 @@ "base_url": "https://artifact-hub.app.lexmount.com", "site_slug": "7l77EeXPRfml", "share_url": "https://artifact-hub.app.lexmount.com/v/Ghn1BR6PCy-njsCMqCe2LRIJ-eyJEJoa", - "expected_version": "ver_48bb19785cc1409091de9a936acbcfa3", + "expected_version": "ver_ea2946bde22d42fe84f550cf9b40f885", "state": "verified" } } From db6a5d456ea3df9256dc55c7e8cce49462dccc25 Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 14 Sep 2026 11:56:52 +0800 Subject: [PATCH 085/127] Verify published report by stable shell --- evaluation/browser-use/current-report.json | 2 +- scripts/publish_browser_use_artifact_hub.py | 11 +++++++++-- tests/browser_use/test_publish.py | 15 +++++++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/evaluation/browser-use/current-report.json b/evaluation/browser-use/current-report.json index 31b770596..e5b708f07 100644 --- a/evaluation/browser-use/current-report.json +++ b/evaluation/browser-use/current-report.json @@ -39,7 +39,7 @@ "base_url": "https://artifact-hub.app.lexmount.com", "site_slug": "7l77EeXPRfml", "share_url": "https://artifact-hub.app.lexmount.com/v/Ghn1BR6PCy-njsCMqCe2LRIJ-eyJEJoa", - "expected_version": "ver_ea2946bde22d42fe84f550cf9b40f885", + "expected_version": "ver_e88ebb8afb86489ba2be858e39723538", "state": "verified" } } diff --git a/scripts/publish_browser_use_artifact_hub.py b/scripts/publish_browser_use_artifact_hub.py index f51e441d1..3ef30c19f 100644 --- a/scripts/publish_browser_use_artifact_hub.py +++ b/scripts/publish_browser_use_artifact_hub.py @@ -19,6 +19,7 @@ ATTESTATION = Path("publish/browser-use-sites.attestation.json") RECEIPT = Path("publish/browser-use-artifact-hub-receipt.json") PROFILE = Path("evaluation/browser-use/current-report.json") +PUBLIC_REPORT_MARKER = "首页 · Browser Use 评测" def sha256(path: Path) -> str: @@ -124,6 +125,13 @@ def response_json(status: int, raw: bytes, *, operation: str) -> dict: return value +def validate_public_preview(status: int, body: str) -> None: + """Verify the stable report shell instead of a revision-specific headline.""" + + if status != 200 or PUBLIC_REPORT_MARKER not in body: + raise RuntimeError("public preview is unavailable or missing the report shell") + + def multipart_archive() -> tuple[bytes, str]: if not ARCHIVE.is_file(): raise ValueError(f"missing validated package: {ARCHIVE}") @@ -225,8 +233,7 @@ def main(*, create: bool = False) -> None: ) with urllib.request.urlopen(preview, timeout=60) as response: body = response.read().decode("utf-8", errors="replace") - if response.status != 200 or "Chromium 与 Moli 的 Browser Use 能力对比" not in body: - raise RuntimeError("public preview is unavailable or missing the report heading") + validate_public_preview(response.status, body) print( json.dumps( { diff --git a/tests/browser_use/test_publish.py b/tests/browser_use/test_publish.py index ea663a7cb..0f2747674 100644 --- a/tests/browser_use/test_publish.py +++ b/tests/browser_use/test_publish.py @@ -27,6 +27,21 @@ def test_response_json_preserves_http_failure_context() -> None: raise AssertionError("invalid response must fail with diagnostic context") +def test_public_preview_uses_stable_report_shell_not_editorial_headline() -> None: + publisher.validate_public_preview( + 200, + "首页 · Browser Use 评测

    76 题审查合格比较集

    ", + ) + + for status, body in ((503, publisher.PUBLIC_REPORT_MARKER), (200, "old page")): + try: + publisher.validate_public_preview(status, body) + except RuntimeError as error: + assert "report shell" in str(error) + else: + raise AssertionError("invalid public preview must fail") + + def test_validate_package_requires_and_checks_every_evidence_root( monkeypatch, tmp_path: Path ) -> None: From ef3e9dc7cbc1fdea8aba9939ffaff863d49efc9f Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 14 Sep 2026 12:42:06 +0800 Subject: [PATCH 086/127] Prepare isolated 90-task trajectory replay --- .../webarena-verified/README.md | 24 +- .../webarena-verified/current-run.json | 53 ++ harness/browser_use/failure_cause_review.py | 1 - harness/browser_use/report.py | 7 +- harness/browser_use/report_i18n.py | 8 +- harness/trajectory_replay/webarena_dataset.py | 293 ++++++++- harness/trajectory_replay/webarena_plan.py | 319 +++++++++ harness/trajectory_replay/webarena_replay.py | 611 ++++++++++++++--- research/webarena-mac-expansion.json | 3 +- scripts/promote_webarena_trajectories.py | 5 +- scripts/run_webarena_trajectory_replay.py | 39 ++ .../run_webarena_trajectory_replay_canary.py | 39 -- .../browser_use/test_failure_cause_review.py | 1 - tests/browser_use/test_report.py | 2 +- tests/browser_use/test_report_i18n.py | 21 +- tests/trajectory_replay/test_report.py | 8 +- tests/trajectory_replay/test_runner.py | 4 +- .../test_webarena_dataset.py | 617 +++++++++++++++++- tests/trajectory_replay/test_webarena_plan.py | 209 ++++++ .../trajectory_replay/test_webarena_replay.py | 183 +++++- 20 files changed, 2240 insertions(+), 207 deletions(-) create mode 100644 evaluation/trajectory-replay/webarena-verified/current-run.json create mode 100644 harness/trajectory_replay/webarena_plan.py create mode 100644 scripts/run_webarena_trajectory_replay.py delete mode 100644 scripts/run_webarena_trajectory_replay_canary.py create mode 100644 tests/trajectory_replay/test_webarena_plan.py diff --git a/evaluation/trajectory-replay/webarena-verified/README.md b/evaluation/trajectory-replay/webarena-verified/README.md index f420607a7..b72be9f92 100644 --- a/evaluation/trajectory-replay/webarena-verified/README.md +++ b/evaluation/trajectory-replay/webarena-verified/README.md @@ -3,6 +3,13 @@ `trajectories.json` is the compact, versioned trajectory portion of the benchmark dataset. Each trajectory is one verified feasible BrowserTool path, not the task's only correct path. +`current-run.json` is the run authority for the next formal replay. It freezes the same 90-task +selection used by the current Browser Use evaluation, the two registered browser methods, resource +limits, and the isolation schedule. The private ready-to-run seed manifest must contain exactly one +canonical candidate for each of those 90 tasks. The current inventory is assembled from 76 clean +autonomous successes and 14 reviewed repairs; successful executions from different methods are +candidate evidence, not additional tasks. + - Browser Trajectory Replay consumes `tool_calls` as fixed inputs and uses `evaluation_input` only when invoking the official evaluator. - Browser Use may use the inventory as hidden solvability and diagnostic evidence. The Agent @@ -17,8 +24,21 @@ Each trajectory is one verified feasible BrowserTool path, not the task's only c task hash; historical Browser Use scores remain unchanged. - Full HAR, screenshots, traces, resource samples and replay receipts remain outside Git under `BROWSER_EVAL_DATA_HOME`; this directory stores deterministic actions and provenance hashes. +- The current self-hosted WebArena deployment has one fixed-port site stack, so a formal suite owns + the physical environment exclusively. It runs every task for one browser before starting the next + browser, restores the official fixture before every task, and tears down both the browser and site + containers between browsers. Parallel methods are valid only after separate site stacks also have + separate containers, data stores, ports, gateways, and lock namespaces; multiple browser + containers against one site stack are not independent environments. +- Formal execution requires clean browser-eval and CowAgent checkouts and binds their commits, the + resolved browser and site image identities, and the frozen CPU, memory, and sampling limits into + the resumable receipt. A resume with different runtime identity is rejected. Browser storage, + cache, cookies, permissions, service workers, and IndexedDB are cleared before every task; failed + teardown aborts the suite. Any pre-action context recovery writes a new physical-evidence + directory instead of overwriting the first one. - More than one trajectory may be retained for a task when paths are materially different and independently satisfy the same promotion gate. -Regenerate the dataset with `scripts/promote_webarena_trajectories.py`; never edit a verdict or -verification claim by hand. +Run the frozen suite with `scripts/run_webarena_trajectory_replay.py`. Diagnostic subsets are +explicitly non-promotable. Promote a completed formal receipt with +`scripts/promote_webarena_trajectories.py`; never edit a verdict or verification claim by hand. diff --git a/evaluation/trajectory-replay/webarena-verified/current-run.json b/evaluation/trajectory-replay/webarena-verified/current-run.json new file mode 100644 index 000000000..ab82ff05e --- /dev/null +++ b/evaluation/trajectory-replay/webarena-verified/current-run.json @@ -0,0 +1,53 @@ +{ + "schema": "lex_webarena_trajectory_replay_profile/1", + "benchmark": "WebArena-Verified", + "benchmark_version": "1.2.3", + "task_selection": { + "path": "evaluation/browser-use/webarena-verified-pilot.json", + "sha256": "2cb716552e36df7535e83ed6d2cf3f84283e3aca86072817cd6e039a38547c7b", + "task_count": 90 + }, + "seed_inventory": { + "base_kind": "successful Browser Use executions", + "expert_repairs_path": "evaluation/trajectory-replay/webarena-verified/expert-repairs.json", + "expert_repairs_sha256": "3f8ef291ad3ed92ecef3469763a93a1aa1a53967116202c73f09a9816ea39832", + "required_ready_tasks": 90, + "canonical_trajectories_per_task": 1 + }, + "methods": [ + { + "id": "chromium", + "engine": "chromium", + "label": "Chromium" + }, + { + "id": "moli-1.1.3", + "engine": "moli", + "label": "Moli 1.1.3" + } + ], + "runs_per_task": 1, + "resources": { + "cpus": 2.0, + "memory": "2g", + "sample_interval_ms": 100 + }, + "execution": { + "environment_pool_count": 1, + "method_parallelism": 1, + "task_parallelism": 1, + "schedule": "complete one method before starting the next", + "reset_before_every_task": true, + "teardown_between_methods": true, + "trusted_source_site_hosts": [ + "192.168.5.2", + "host.docker.internal" + ], + "site_stack": "one fixed-port self-hosted WebArena environment" + }, + "scoring": { + "authority": "official programmatic evaluator or a hash-bound replay correction", + "promotion_gate": "the same canonical trajectory passes execution and evaluation on every registered method", + "model_calls": 0 + } +} diff --git a/harness/browser_use/failure_cause_review.py b/harness/browser_use/failure_cause_review.py index 205c5361c..0649067a8 100644 --- a/harness/browser_use/failure_cause_review.py +++ b/harness/browser_use/failure_cause_review.py @@ -7,7 +7,6 @@ from pathlib import Path from typing import Any - LAYERED_REVIEW_SCHEMA = "lex_browser_use_layered_failure_cause_review/2" DEFAULT_REVIEW_PATH = ( Path(__file__).parents[2] / "research/browser-use-failure-cause-review.json" diff --git a/harness/browser_use/report.py b/harness/browser_use/report.py index 4bd506f53..fc46c67a2 100644 --- a/harness/browser_use/report.py +++ b/harness/browser_use/report.py @@ -2161,11 +2161,6 @@ def _article_body( task_count = total_task_count - len(excluded) summary = _primary_summary(eligible_attempts) paired = _paired_summary(eligible_attempts) - failed_result_count = sum( - item["completed"] - item["passed"] - for model_summary in summary.values() - for item in model_summary.values() - ) raw_failed_result_count = sum(float(row.get("score") or 0) < 1 for row in attempts) comparison_rows = [ [ @@ -2924,7 +2919,7 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat ) methodology += _section( "Mac 上可以继续扩展哪些实例", - f"

    官方完整集有 {MAC_COUNTS['full']} 题,hard 子集有 {MAC_COUNTS['hard']} 题;官方没有 179 题版本。若运行 179 题,必须把它标注为本项目自定义样本并冻结任务清单。当前四个已实跑站点可直接进入下一轮预检:覆盖 {MAC_COUNTS['current_four_sites_single_site']} 个单站任务,计入只依赖这四站的跨站任务后为 {MAC_COUNTS['current_four_sites_including_cross_site']} 个候选;hard 子集中可覆盖 {MAC_COUNTS['current_four_sites_hard_including_cross_site']} 个。

    " + f"

    下一轮轨迹重放固定使用本报告当前 {task_count} 题,不扩充任务范围。若未来另行扩充 Browser Use 评测,当前四个已实跑站点可预检 {MAC_COUNTS['current_four_sites_single_site']} 个单站任务;计入只依赖这四站的跨站任务后,共有 {MAC_COUNTS['current_four_sites_including_cross_site']} 个候选,其中 hard 子集 {MAC_COUNTS['current_four_sites_hard_including_cross_site']} 个。

    " + _table( ["环境", "Apple Silicon Mac 状态", "完整集单站题", "hard 单站题", "约束"], MAC_ENVIRONMENT_CANDIDATES, diff --git a/harness/browser_use/report_i18n.py b/harness/browser_use/report_i18n.py index 7d9ef3616..914180fbe 100644 --- a/harness/browser_use/report_i18n.py +++ b/harness/browser_use/report_i18n.py @@ -597,7 +597,7 @@ "先从四个已验证站点的 678 个候选中做可完成性预检和分层抽样;Wikipedia、Map 等大数据环境后置。这样扩容不会先引入新的部署变量。": "Preflight solvability and draw a stratified sample from the 678 candidates on the four verified sites first. Defer the large Wikipedia and Map environments so expansion does not begin by adding deployment variables.", "官方任务集与安装说明": "Official dataset and installation guide", "官方环境容量说明": "Official environment capacity guide", - "官方完整集有 812 题,hard 子集有 258 题;官方没有 179 题版本。若运行 179 题,必须把它标注为本项目自定义样本并冻结任务清单。当前四个已实跑站点可直接进入下一轮预检:覆盖 655 个单站任务,计入只依赖这四站的跨站任务后为 678 个候选;hard 子集中可覆盖 233 个。": "The official full set contains 812 tasks and the hard subset contains 258; there is no official 179-task edition. A 179-task run must be labeled as a project-defined cohort with a frozen task list. The four locally measured sites can enter the next preflight directly: 655 single-site tasks, 678 candidates when cross-site tasks that use only these four sites are included, and 233 candidates in the hard subset.", + "下一轮轨迹重放固定使用本报告当前 90 题,不扩充任务范围。若未来另行扩充 Browser Use 评测,当前四个已实跑站点可预检 655 个单站任务;计入只依赖这四站的跨站任务后,共有 678 个候选,其中 hard 子集 233 个。": "The next trajectory-replay evaluation is fixed to the report's current 90 tasks; its scope will not expand. If Browser Use is expanded separately later, the four locally measured sites support preflight checks for 655 single-site tasks, or 678 candidates including cross-site tasks that use only these four sites, with 233 in the hard subset.", "站点确实部署在本机,不是公网代站。": "The sites were deployed locally, not replaced by public websites.", "为什么五个失败任务会完全相同": "Why the same five tasks failed in both browsers", "二、为什么五个失败任务会完全相同": "2. Why the same five tasks failed in both browsers", @@ -630,7 +630,6 @@ "两个浏览器共享 CowAgent、模型、提示与工具合同、站点状态、30 步上限和 evaluator,只有浏览器实现不同。5 个共同失败都落在共享层,因此结果高度相关并不反常;它更说明这批题没有触发足以改变最终成败的浏览器差异。": "The browsers share CowAgent, model, prompt and tool contract, site state, the 30-step limit, and evaluator; only the browser implementation changes. All five shared failures occurred in shared layers, so correlated outcomes are expected. More importantly, this cohort did not trigger a browser difference large enough to change the verdict.", "结论:": "Conclusion:", "现有证据不支持“网站本身报错”“官方 evaluator 实现错误”或“标准答案错误”。其中 #124 的站内搜索召回过宽会增加歧义,但返回结果仍需要按任务语义过滤。更准确的归因是:四题属于共享 Agent/模型的语义、聚合或输出协议问题,一题属于共享的长链完成与步数预算问题。这不是对模型通用能力的定论,只描述当前 CowAgent、模型与运行协议。": "The evidence does not support a broken site, an official evaluator implementation bug, or an incorrect answer key. Broad site-search recall adds ambiguity to Task #124, but results still need semantic filtering. The best-supported attribution is four shared agent/model semantic, aggregation, or output-protocol errors and one shared long-horizon completion and step-budget error. This is not a general verdict on the model; it applies to the current CowAgent, model, and run protocol.", - "官方包有 812 题和 258 题 hard 子集,没有官方 179 题版本;179 只能是另行声明的自定义样本。当前 Mac 已实跑的四个站点可覆盖 655 个单站任务;再计入只依赖这四站的跨站任务,共有 678 个候选,其中 hard 子集有 233 个。下一轮应在这些候选中先做登录、重置、健康检查与 evaluator 预检,再按简单、中等、困难和内核敏感交互分层抽样。": "The official package contains 812 tasks and a 258-task hard subset; there is no official 179-task edition. Any 179-task run is a separately declared custom cohort. The four sites already measured on this Mac cover 655 single-site tasks and 678 candidates after cross-site tasks using only those four sites are included; 233 are in the hard subset. The next run should preflight sign-in, reset, health, and evaluator behavior, then sample by easy, medium, hard, and browser-sensitive interaction strata.", "Wikipedia 需要约 100 GB 数据,Map 需要约 60 GB 数据且当前公开镜像为 amd64;二者可作为后续扩展,但没有理由成为本机下一批的前置阻塞。": "Wikipedia requires about 100 GB of data. Map requires about 60 GB and its current public image is amd64. Both can be later expansions, but neither should block the next Mac cohort.", "保留 CowAgent、模型、reasoning=low 和每题最多两次执行的固定协议,优先加入长页面、复杂编辑器、弹窗、多标签页、上传下载等更可能诱发浏览器差异的任务。实时网站另开 Online‑Mind2Web / WebVoyager 泛化轨道,LLM judge 与人工抽检单列。": "Keep CowAgent, the model, reasoning=low, and at most two executions per task fixed. Prioritize long pages, complex editors, dialogs, multiple tabs, uploads, and downloads that are more likely to expose browser differences. Use a separate Online-Mind2Web / WebVoyager generalization track for live sites, with LLM judging and human sampling reported separately.", "两种浏览器都返回《The Hobbit》,遗漏《A Christmas Carol》(作者允许为空)。": "Both browsers returned The Hobbit but omitted A Christmas Carol, whose author may be null.", @@ -646,7 +645,6 @@ "表 1|30 个任务的逐题最终结果配对。": "Table 1 | Paired final outcomes for 30 tasks.", # Deployment, timing, browser matrix, and participant framing. "图 1 · 从官方站点镜像、逐题隔离与健康检查,到 CowAgent 观察—决策—操作闭环,再到 Chromium/Moli 对照和官方程序化验收。窄屏可横向滚动查看。": "Figure 1. From official site images, per-task isolation, and health checks through CowAgent's observe-decide-act loop, then Chromium/Moli comparison and official programmatic evaluation. Scroll horizontally on narrow screens.", - "站点确实部署在本机,不是公网代站。": "The sites were deployed locally; they were not public-web substitutes.", "macOS 上的 Docker 由 Colima 虚拟机承载;Shopping、Shopping Admin、Reddit、GitLab 四个官方镜像把应用端口发布到 Colima/Docker 宿主。Chromium 与 Moli 各自在独立浏览器容器中运行,通过容器内解析到的宿主网关": "On macOS, Docker runs inside a Colima VM. The four official Shopping, Shopping Admin, Reddit, and GitLab images publish their application ports to the Colima/Docker host. Chromium and Moli run in separate browser containers and reach those ports through the host gateway resolved inside each container", "访问这些端口。这个地址只是本轮私有网络中的宿主入口,不是评测网站或固定产品配置。": "to access the sites. This address is only the host entry point in this run's private network; it is neither a benchmark website nor a fixed product configuration.", "预注册文件只含任务 ID、层级和入选理由,不含点击序列。CowAgent 收到自然语言目标后,在最多 30 步内循环读取页面观察、请求模型决策并生成下一次 BrowserTool 调用。Chromium 与 Moli 分别连接独立 CDP 浏览器容器;每个逻辑结果保留最终 JSON、Agent 事件流、网络 HAR 和 Playwright trace。": "The preregistration contains task IDs, tiers, and selection rationales, but no click sequence. Given a natural-language objective, CowAgent repeatedly reads the page observation, requests a model decision, and generates the next BrowserTool call for up to 30 steps. Chromium and Moli connect to separate CDP browser containers. Each logical result retains the final JSON, agent event stream, network HAR, and Playwright trace.", @@ -717,7 +715,6 @@ "累计自主操作": "Cumulative autonomous actions", "Agent 链路耗时包含会话准备、登录、模型推理、调度、工具执行和收尾,不包含 Docker 站点重置;BrowserTool 耗时是全部 browser 工具调用的墙钟时间之和,不等同于纯内核时间。": "Agent-stack latency includes session preparation, sign-in, model inference, orchestration, tool execution, and cleanup, but excludes Docker site reset. BrowserTool latency is the sum of wall-clock time for all browser tool calls and is not pure browser-core time.", "执行未完成;错误信息见运行与证据。": "Execution did not complete; see runtime and evidence for the error.", - "全部官方 evaluator 均通过。": "All official evaluators passed.", # Result pages and audit boundaries. "Agent 端到端耗时包含会话准备、登录、模型推理、调度、工具执行和收尾,不包含 Docker 站点重置;BrowserTool 执行耗时是全部 browser 工具调用的墙钟时间之和,不等同于纯内核时间。": "Agent end-to-end latency includes session preparation, sign-in, model inference, orchestration, tool execution, and cleanup, but excludes Docker site reset. BrowserTool execution latency is the sum of wall-clock time for all browser tool calls and is not pure browser-core time.", "公开包包含脱敏的回答、evaluator、事件流与轨迹。原始 HAR 和 Playwright trace 可能携带登录态、Cookie、页面快照或请求正文,因此不公开;其字节数和 SHA-256 保留用于证明证据绑定。": "The public bundle contains sanitized answers, evaluator records, event streams, and trajectories. Raw HAR and Playwright traces may contain authenticated state, cookies, page snapshots, or request bodies, so they are withheld; byte counts and SHA-256 hashes retain the evidence binding.", @@ -842,7 +839,6 @@ "60/60 个逻辑结果已覆盖,0 个未决。它不重新判题,也不是独立语义复核。": "Evidence-integrity checks cover all 60 logical results with no unresolved integrity issue. They do not rescore tasks or constitute independent semantic review.", "60/60 个逻辑结果完成证据完整性审计": "Evidence integrity checked for all 60 logical results", "每题声明一个结构化最终回答合同,并在需要确认页面副作用时附加一个或多个网络事件合同。所有合同都通过才记 1 分。任务页只解释合同;各浏览器的实际回答、失败规则和 CowAgent 自主轨迹统一放在结果页。": "Each task declares a structured final-answer contract and, where required, one or more network-event contracts for page side effects. Every contract must pass for a score of 1. Task pages explain only the contract; browser answers, failed rules, and CowAgent trajectories appear on result pages.", - "本轮在模型执行前固定 30 题,覆盖四个已部署站点以及 RETRIEVE、NAVIGATE、MUTATE 三种最终合同。这里仅描述题目与验收规则;通过率、耗时、轨迹和可完成性观察统一放在结果分析中。": "The 30 tasks were fixed before model execution. They cover four deployed sites and three outcome contracts: RETRIEVE, NAVIGATE, and MUTATE. This page describes tasks and acceptance rules only; pass rates, latency, trajectories, and solvability observations appear in Results.", "运行器代码在每个执行前调用": "Before each execution, the runner calls", "先看 30 个同题配对,再定位 10 个失败结果的官方规则、实际回答、轨迹和日志。": "Start with the 30 same-task comparisons, then inspect the official rules, actual answers, trajectories, and logs for the 10 failed results.", "Agent 链路耗时中位数": "Median agent-stack latency", @@ -895,7 +891,6 @@ "通过任务": "Tasks passed", "每个模型分别比较各浏览器,不把不同模型的结果合并排名。": "Browsers are compared separately within each model; results from different models are not merged into one ranking.", "增加新的浏览器": "Adding another browser", - "新增浏览器时,需要注册其容器镜像、版本与 CDP 端点,并在同一冻结任务集和共同条件下形成完整结果。总榜按浏览器逐行扩展;同题矩阵按浏览器增加列;统计比较按浏览器对单独生成,不能把多个浏览器压成一个总胜负。": "To add a browser, register its container image, version, and CDP endpoint, then run the complete frozen task set under the same controls. Summary tables add rows, the same-task matrix adds a column, and pairwise statistics remain separate instead of collapsing every browser into one winner.", "仓库中的 Browser Trajectory Replay(轨迹重放)是另一项固定动作测试,不计入这里的成绩。": "The repository's Browser Trajectory Replay track is a separate fixed-action test and is not included in these scores.", "评测结果": "Evaluation results", "浏览器 B": "Browser B", @@ -1338,7 +1333,6 @@ def _register_report_change_translations() -> None: "该结果通过全部官方程序化验收;下方可核对任务合同、执行轨迹和运行证据。": "This result passed every official programmatic evaluator. The task contract, execution trajectory, and runtime evidence are available below.", "失败位置": "Failure stage", "人工复核根因": "Reviewed root cause", - "负责对象与处理": "Owner and treatment", "页面画面": "Page frames", "查看 evaluator 断言": "View evaluator assertions", "查看失败附近轨迹": "View trajectory near the failure", diff --git a/harness/trajectory_replay/webarena_dataset.py b/harness/trajectory_replay/webarena_dataset.py index 9d5ee5544..9b83c60a2 100644 --- a/harness/trajectory_replay/webarena_dataset.py +++ b/harness/trajectory_replay/webarena_dataset.py @@ -6,10 +6,246 @@ import hashlib import json from collections import defaultdict +from functools import lru_cache +from importlib.metadata import version +from importlib.resources import files from pathlib import Path from typing import Any +from webarena_verified.core.utils.checksum import compute_evaluator_checksum + +from harness.trajectory_replay.webarena_plan import ( + load_replay_plan, + replay_calls_binding_sha256, + replay_site_hosts, +) + SCHEMA = "lex_webarena_verified_trajectory_dataset/1" +RECEIPT_SCHEMA = "lex_webarena_trajectory_replay_suite/1" + + +@lru_cache(maxsize=1) +def _official_evaluator_names() -> dict[int, tuple[str, ...]]: + dataset = json.loads( + files("webarena_verified") + .joinpath("assets/dataset/webarena-verified.json") + .read_text(encoding="utf-8") + ) + return { + int(task["task_id"]): tuple(str(item["evaluator"]) for item in task["eval"]) + for task in dataset + } + + +def _validate_formal_receipt( + receipt: dict[str, Any], + *, + receipt_path: Path, + seed_sha256: str, + seed_task_ids: set[int], + required_engines: tuple[str, ...], + profile_sha256: str, + expected_resources: dict[str, Any], +) -> None: + if receipt.get("schema") != RECEIPT_SCHEMA: + raise ValueError(f"unsupported replay receipt: {receipt_path}") + if receipt.get("execution_scope") != "formal" or receipt.get("status") != "complete": + raise ValueError(f"replay receipt is not a completed formal suite: {receipt_path}") + if receipt.get("source_seed_manifest_sha256") != seed_sha256: + raise ValueError(f"replay receipt is not bound to the selected seeds: {receipt_path}") + if receipt.get("profile_sha256") != profile_sha256: + raise ValueError(f"replay receipt is not bound to the selected profile: {receipt_path}") + runtime = receipt.get("runtime") or {} + if runtime.get("resources") != expected_resources: + raise ValueError(f"replay receipt has different runtime resources: {receipt_path}") + if not runtime.get("browser_eval_commit") or not runtime.get("cowagent_commit"): + raise ValueError(f"replay receipt is missing code identity: {receipt_path}") + browser_images = runtime.get("browser_images") or {} + if set(browser_images) != set(required_engines) or any( + not (browser_images.get(engine) or {}).get("id") for engine in required_engines + ): + raise ValueError(f"replay receipt is missing browser image identity: {receipt_path}") + if not runtime.get("site_images") or any( + not image.get("id") for image in runtime["site_images"].values() + ): + raise ValueError(f"replay receipt is missing site image identity: {receipt_path}") + receipt_task_ids = [int(task_id) for task_id in receipt.get("task_ids") or []] + if ( + len(receipt_task_ids) != len(set(receipt_task_ids)) + or set(receipt_task_ids) != seed_task_ids + ): + raise ValueError(f"replay receipt does not cover the frozen task set: {receipt_path}") + isolation = receipt.get("isolation") or {} + required_isolation = { + "environment_pool_count": 1, + "method_parallelism": 1, + "task_parallelism": 1, + "reset_before_every_task": True, + "teardown_between_methods": True, + } + if any(isolation.get(key) != value for key, value in required_isolation.items()): + raise ValueError(f"replay receipt does not prove the required isolation: {receipt_path}") + engines = [str(engine) for engine in receipt.get("engines") or []] + if len(engines) != len(set(engines)) or set(engines) != set(required_engines): + raise ValueError(f"replay receipt does not cover the required methods: {receipt_path}") + method_teardown = receipt.get("method_teardown") or {} + if set(method_teardown) != set(required_engines) or not all(method_teardown.values()): + raise ValueError(f"replay receipt does not prove completed method teardown: {receipt_path}") + sessions = receipt.get("sessions") or [] + session_ids = [str(session.get("id") or "") for session in sessions] + if ( + not sessions + or any(not session_id for session_id in session_ids) + or any(not session.get("site_access_host") for session in sessions) + or len(session_ids) != len(set(session_ids)) + ): + raise ValueError(f"replay receipt has invalid browser sessions: {receipt_path}") + sessions_by_id = {str(session["id"]): session for session in sessions} + attempt_keys = [] + for attempt in receipt.get("attempts") or []: + if attempt.get("schema") != "lex_webarena_trajectory_replay_attempt/1": + raise ValueError(f"replay receipt has an invalid attempt schema: {receipt_path}") + session = sessions_by_id.get(str(attempt.get("browser_session_id") or "")) + if ( + session is None + or session.get("engine") != attempt.get("engine") + or int(attempt.get("task_id")) not in session.get("task_ids", []) + ): + raise ValueError(f"replay attempt is not bound to a browser session: {receipt_path}") + attempt_keys.append((str(attempt.get("engine")), int(attempt.get("task_id")))) + expected_keys = { + (engine, task_id) for engine in required_engines for task_id in seed_task_ids + } + if len(attempt_keys) != len(set(attempt_keys)) or set(attempt_keys) != expected_keys: + raise ValueError( + f"replay receipt does not contain the complete method-task matrix: {receipt_path}" + ) + physical_executions = receipt.get("physical_executions") or [] + physical_keys = [ + ( + str(item.get("engine")), + int(item.get("task_id")), + int(item.get("physical_execution")), + ) + for item in physical_executions + ] + if ( + not physical_executions + or any(item.get("status") != "complete" for item in physical_executions) + or len(physical_keys) != len(set(physical_keys)) + ): + raise ValueError(f"replay receipt has invalid physical execution history: {receipt_path}") + physical_by_attempt: dict[tuple[str, int], list[dict[str, Any]]] = defaultdict(list) + for item in physical_executions: + key = (str(item["engine"]), int(item["task_id"])) + if key not in expected_keys: + raise ValueError( + f"replay receipt has a physical execution outside scope: {receipt_path}" + ) + physical_by_attempt[key].append(item) + attempts_by_key = { + (str(item["engine"]), int(item["task_id"])): item + for item in receipt.get("attempts") or [] + } + if set(physical_by_attempt) != expected_keys: + raise ValueError(f"replay receipt has unbound physical executions: {receipt_path}") + for key, attempt in attempts_by_key.items(): + physical = physical_by_attempt[key] + if ( + len(physical) != int(attempt.get("physical_execution_count") or 1) + or max(int(item["physical_execution"]) for item in physical) + != int(attempt.get("physical_execution") or 0) + ): + raise ValueError( + f"replay receipt has inconsistent physical executions: {receipt_path}" + ) + + +def _validate_passing_attempt_evidence( + attempt: dict[str, Any], + candidate: dict[str, Any], + runtime_site_host: str, + expected_data_checksum: str, + trusted_source_site_hosts: set[str], +) -> None: + if attempt.get("process_status") != "pass" or attempt.get("evaluator_status") != "pass": + return + evidence = attempt.get("evidence") or {} + paths = {} + for kind in ("trace", "network_har", "evaluation"): + item = evidence.get(kind) or {} + path = Path(str(item.get("path") or "")).expanduser().resolve() + if not path.is_file() or _sha256(path) != item.get("sha256"): + raise ValueError( + f"passing replay attempt has missing or changed {kind} evidence: " + f"{attempt.get('engine')}/task-{attempt.get('task_id')}" + ) + paths[kind] = path + + if attempt.get("source_tool_calls_sha256") != _json_sha256(candidate["tool_calls"]): + raise ValueError("passing replay attempt is not bound to the canonical tool calls") + source_site_hosts = replay_site_hosts(candidate["tool_calls"]) + if ( + not source_site_hosts <= trusted_source_site_hosts + or set(attempt.get("source_site_hosts") or []) != source_site_hosts + or attempt.get("runtime_site_host") != runtime_site_host + or attempt.get("replay_calls_binding_sha256") + != replay_calls_binding_sha256( + candidate["tool_calls"], allowed_hosts=trusted_source_site_hosts + ) + ): + raise ValueError("passing replay attempt differs from the canonical replay actions") + if attempt.get("evaluation_input_sha256") != _json_sha256( + candidate["recorded_response"] + ): + raise ValueError("passing replay attempt is not bound to the canonical evaluator input") + + trace = _read_json(paths["trace"]) + trace_arguments = [item.get("arguments") for item in trace] if isinstance(trace, list) else [] + if ( + len(trace_arguments) != int(candidate["action_count"]) + or any(item.get("status") != "success" for item in trace) + or attempt.get("executed_tool_calls_sha256") != _json_sha256(trace_arguments) + or attempt.get("replay_calls_binding_sha256") + != replay_calls_binding_sha256( + trace_arguments, allowed_hosts={runtime_site_host} + ) + ): + raise ValueError("passing replay trace does not prove every executed action") + + network_har = _read_json(paths["network_har"]) + if not isinstance(network_har, dict) or not isinstance( + (network_har.get("log") or {}).get("entries"), list + ): + raise ValueError("passing replay attempt has an invalid network HAR") + + evaluation = _read_json(paths["evaluation"]) + expected_evaluators = _official_evaluator_names().get(int(attempt["task_id"])) + if ( + int(evaluation.get("task_id") or -1) != int(attempt["task_id"]) + or float(evaluation.get("score") or 0) != 1.0 + or str(evaluation.get("status") or "").lower() not in {"pass", "success"} + or evaluation.get("webarena_verified_version") != version("webarena-verified") + or evaluation.get("webarena_verified_evaluator_checksum") + != compute_evaluator_checksum() + or not expected_data_checksum + or evaluation.get("webarena_verified_data_checksum") != expected_data_checksum + ): + raise ValueError("passing replay evaluation evidence does not prove a passing score") + evaluator_rows = evaluation.get("evaluators_results") or [] + if ( + expected_evaluators is None + or tuple(str(row.get("evaluator_name") or "") for row in evaluator_rows) + != expected_evaluators + or any( + float(row.get("score") or 0) != 1.0 + or str(row.get("status") or "").lower() not in {"pass", "success"} + for row in evaluator_rows + ) + ): + raise ValueError("passing replay evaluator evidence does not match the canonical input") + if any(item.get("status") != "pass" for item in evaluation.get("replay_assertions") or []): + raise ValueError("passing replay evaluation contains a failed replay assertion") def _read_json(path: Path) -> Any: @@ -20,6 +256,11 @@ def _sha256(path: Path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest() +def _json_sha256(value: Any) -> str: + payload = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + return hashlib.sha256(payload.encode()).hexdigest() + + def _candidate_source(candidate: dict[str, Any]) -> dict[str, Any]: source = candidate.get("source") or {} if candidate.get("candidate_kind") == "expert_repair": @@ -47,7 +288,7 @@ def build_verified_trajectory_dataset( seed_manifest_path: str | Path, replay_receipt_paths: list[str | Path], *, - required_engines: tuple[str, ...] = ("chromium", "moli"), + profile_path: str | Path, prior_dataset_path: str | Path | None = None, ) -> dict[str, Any]: """Return compact trajectories that passed process and official evaluation gates.""" @@ -57,7 +298,15 @@ def build_verified_trajectory_dataset( seeds = _read_json(seed_path) if seeds.get("schema") != "lex_browser_use_replay_seeds/1": raise ValueError("unsupported replay seed manifest") - seed_tasks = {int(item["task_id"]): item for item in seeds.get("tasks") or []} + seed_rows = seeds.get("tasks") or [] + seed_tasks = {int(item["task_id"]): item for item in seed_rows} + if len(seed_tasks) != len(seed_rows): + raise ValueError("replay seed manifest contains duplicate task IDs") + seed_task_ids = set(seed_tasks) + plan = load_replay_plan(profile_path, seed_path) + profile = plan["profile"] + required_engines = tuple(plan["engines"]) + profile_sha256 = plan["profile_sha256"] attempts_by_key: dict[tuple[int, str], list[dict[str, Any]]] = defaultdict(list) receipt_runs_by_key: dict[tuple[int, str], set[str]] = defaultdict(set) @@ -65,13 +314,42 @@ def build_verified_trajectory_dataset( for raw_path in replay_receipt_paths: receipt_path = Path(raw_path).expanduser().resolve() receipt = _read_json(receipt_path) - if receipt.get("schema") != "lex_webarena_trajectory_replay_canary/1": - raise ValueError(f"unsupported replay receipt: {receipt_path}") - if receipt.get("source_seed_manifest_sha256") != seed_sha256: - raise ValueError(f"replay receipt is not bound to the selected seeds: {receipt_path}") + _validate_formal_receipt( + receipt, + receipt_path=receipt_path, + seed_sha256=seed_sha256, + seed_task_ids=seed_task_ids, + required_engines=required_engines, + profile_sha256=profile_sha256, + expected_resources=profile["resources"], + ) receipts.append({"sha256": _sha256(receipt_path)}) for attempt in receipt.get("attempts") or []: - key = (int(attempt["task_id"]), str(attempt["source_candidate_id"])) + task_id = int(attempt["task_id"]) + candidate_id = str(attempt["source_candidate_id"]) + if candidate_id != seed_tasks[task_id].get("canonical_candidate_id"): + raise ValueError(f"receipt uses a non-canonical trajectory for task {task_id}") + candidate = next( + ( + item + for item in seed_tasks[task_id].get("candidates") or [] + if item.get("id") == candidate_id + ), + None, + ) + if candidate is None: + raise ValueError(f"receipt references candidate missing from seeds: {candidate_id}") + session = { + str(item["id"]): item for item in receipt.get("sessions") or [] + }[str(attempt["browser_session_id"])] + _validate_passing_attempt_evidence( + attempt, + candidate, + str(session.get("site_access_host") or ""), + str((seeds.get("source") or {}).get("official_data_checksum") or ""), + plan["trusted_source_site_hosts"], + ) + key = (task_id, candidate_id) attempts_by_key[key].append(attempt) receipt_runs_by_key[key].add(str(receipt.get("run_id") or "")) @@ -199,6 +477,7 @@ def build_verified_trajectory_dataset( }, "provenance": { "seed_manifest_sha256": seed_sha256, + "profile_sha256": profile_sha256, "replay_receipts": receipts, "prior_dataset_sha256": prior_dataset_sha256, }, diff --git a/harness/trajectory_replay/webarena_plan.py b/harness/trajectory_replay/webarena_plan.py new file mode 100644 index 000000000..8ca97622b --- /dev/null +++ b/harness/trajectory_replay/webarena_plan.py @@ -0,0 +1,319 @@ +"""Validate the frozen WebArena trajectory-replay run contract.""" + +from __future__ import annotations + +import hashlib +import json +from pathlib import Path +from typing import Any +from urllib.parse import urlsplit, urlunsplit + +from harness.browser_use.runner import SITE_PORTS +from harness.trajectory_replay.browser_use_seeds import ( + _authoritative_rows, + extract_browser_calls, +) +from harness.trajectory_replay.runner import ENGINE_IMAGES + +PROFILE_SCHEMA = "lex_webarena_trajectory_replay_profile/1" +SEED_SCHEMA = "lex_browser_use_replay_seeds/1" + + +def _read_json(path: Path) -> Any: + return json.loads(path.read_text(encoding="utf-8")) + + +def _sha256(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def replay_site_hosts(calls: list[dict[str, Any]]) -> set[str]: + site_ports = {port for pair in SITE_PORTS.values() for port in pair} + return { + str(parsed.hostname) + for call in calls + if isinstance(call.get("url"), str) + for parsed in [urlsplit(call["url"])] + if parsed.scheme in {"http", "https"} + and parsed.port in site_ports + and parsed.hostname + } + + +def replay_calls_binding_sha256( + calls: list[dict[str, Any]], *, allowed_hosts: set[str] +) -> str: + """Hash calls while ignoring only the deployment-specific WebArena gateway host.""" + + site_ports = {port for pair in SITE_PORTS.values() for port in pair} + normalized = [] + for raw_call in calls: + call = dict(raw_call) + url = call.get("url") + if isinstance(url, str): + parsed = urlsplit(url) + if ( + parsed.scheme in {"http", "https"} + and parsed.port in site_ports + and parsed.hostname in allowed_hosts + ): + call["url"] = urlunsplit( + ( + parsed.scheme, + f"__WEBARENA_GATEWAY__:{parsed.port}", + parsed.path, + parsed.query, + parsed.fragment, + ) + ) + normalized.append(call) + payload = json.dumps( + normalized, ensure_ascii=False, sort_keys=True, separators=(",", ":") + ) + return hashlib.sha256(payload.encode()).hexdigest() + + +def _validate_canonical_candidate( + task: dict[str, Any], + candidate: dict[str, Any], + repair_sha256: str, + repair_record: dict[str, Any] | None, + authoritative_row: dict[str, Any] | None, +) -> None: + task_id = int(task["task_id"]) + calls = candidate.get("tool_calls") + if ( + not isinstance(calls, list) + or not calls + or int(candidate.get("action_count") or 0) != len(calls) + or not isinstance(candidate.get("recorded_response"), dict) + ): + raise ValueError(f"task {task_id} has an invalid canonical trajectory payload") + source = candidate.get("source") or {} + if candidate.get("candidate_kind") == "expert_repair": + if ( + repair_record is None + or source.get("kind") != "expert_repair_spec" + or source.get("repair_spec_sha256") != repair_sha256 + or candidate.get("id") + != str( + repair_record.get("candidate_id") or f"expert__task-{task_id}" + ) + or calls != repair_record.get("tool_calls") + or candidate["recorded_response"] != repair_record.get("expected_response") + or source.get("details") != repair_record.get("source") + or candidate.get("repair_basis") != repair_record.get("repair_basis") + ): + raise ValueError(f"task {task_id} expert trajectory is not bound to the repair spec") + return + + if candidate.get("official_score") != 1.0 or candidate.get("clean") is not True: + raise ValueError(f"task {task_id} autonomous trajectory is not a clean official success") + method = candidate.get("method") or {} + if ( + authoritative_row is None + or int(candidate.get("source_execution") or 0) + != int(authoritative_row.get("attempt") or 0) + or str(candidate.get("source_run_id") or "") + != str(authoritative_row.get("run_id") or "") + or method.get("model") != authoritative_row.get("model") + or method.get("engine") != authoritative_row.get("engine") + or float(authoritative_row.get("score") or 0) != 1.0 + ): + raise ValueError( + f"task {task_id} autonomous trajectory is not the authoritative final execution" + ) + paths = { + kind: Path(str(source.get(f"{kind}_path") or "")).expanduser().resolve() + for kind in ("events", "response", "evaluation") + } + for kind, path in paths.items(): + if not path.is_file() or _sha256(path) != source.get(f"{kind}_sha256"): + raise ValueError(f"task {task_id} autonomous {kind} evidence is missing or changed") + if path != Path( + str((authoritative_row.get("paths") or {}).get(kind) or "") + ).expanduser().resolve(): + raise ValueError( + f"task {task_id} autonomous {kind} evidence is not from the final execution" + ) + source_calls = extract_browser_calls(paths["events"]) + if ( + [item["arguments"] for item in source_calls] != calls + or any(item["status"] != "success" for item in source_calls) + or _read_json(paths["response"]) != candidate["recorded_response"] + ): + raise ValueError(f"task {task_id} autonomous trajectory differs from source evidence") + evaluation = _read_json(paths["evaluation"]) + if int(evaluation.get("task_id") or -1) != task_id or float( + evaluation.get("score") or 0 + ) != 1.0: + raise ValueError(f"task {task_id} autonomous source evaluation is not passing") + + +def load_replay_plan( + profile_path: str | Path, seed_manifest_path: str | Path +) -> dict[str, Any]: + """Return a validated, exact method and task plan for one formal replay suite.""" + + profile_file = Path(profile_path).expanduser().resolve() + repo_root = profile_file.parents[3] + profile = _read_json(profile_file) + if profile.get("schema") != PROFILE_SCHEMA: + raise ValueError("unsupported trajectory replay profile") + + selection = profile.get("task_selection") or {} + selection_file = (repo_root / str(selection.get("path") or "")).resolve() + if repo_root not in selection_file.parents: + raise ValueError("task selection must stay inside the repository") + if not selection_file.is_file() or _sha256(selection_file) != selection.get("sha256"): + raise ValueError("trajectory replay task selection does not match the frozen profile") + selection_payload = _read_json(selection_file) + task_ids = [int(item["task_id"]) for item in selection_payload.get("tasks") or []] + if len(task_ids) != int(selection.get("task_count") or 0) or len(task_ids) != len( + set(task_ids) + ): + raise ValueError("trajectory replay task selection cardinality is invalid") + + repair = profile.get("seed_inventory") or {} + repair_file = (repo_root / str(repair.get("expert_repairs_path") or "")).resolve() + if repo_root not in repair_file.parents: + raise ValueError("expert repairs must stay inside the repository") + if not repair_file.is_file() or _sha256(repair_file) != repair.get( + "expert_repairs_sha256" + ): + raise ValueError("expert repairs do not match the frozen replay profile") + repair_payload = _read_json(repair_file) + repair_records = { + int(item["task_id"]): item for item in repair_payload.get("repairs") or [] + } + if len(repair_records) != len(repair_payload.get("repairs") or []): + raise ValueError("expert repairs contain duplicate task IDs") + + seed_file = Path(seed_manifest_path).expanduser().resolve() + seeds = _read_json(seed_file) + if seeds.get("schema") != SEED_SCHEMA: + raise ValueError("unsupported replay seed manifest") + seed_source = seeds.get("source") or {} + if seed_source.get("selection_sha256") != selection.get("sha256"): + raise ValueError("replay seeds are not bound to the frozen task selection") + seed_repairs = seeds.get("expert_repairs") or {} + if seed_repairs.get("repair_spec_sha256") != repair.get("expert_repairs_sha256"): + raise ValueError("replay seeds are not bound to the frozen expert repairs") + authoritative: dict[tuple[str, str, int], dict[str, Any]] = {} + for declared_model, declared in (seed_source.get("evidence") or {}).items(): + root = Path(str((declared or {}).get("root") or "")).expanduser().resolve() + attempts_file = root / "attempts.jsonl" + manifest_file = root / "manifest.json" + if ( + not attempts_file.is_file() + or not manifest_file.is_file() + or _sha256(attempts_file) != declared.get("attempts_sha256") + or _sha256(manifest_file) != declared.get("manifest_sha256") + ): + raise ValueError("replay seed authority manifest is missing or changed") + manifest, rows = _authoritative_rows(root) + model = str((manifest.get("protocol") or {}).get("model") or "") + if model != declared_model or manifest.get("selection_sha256") != selection.get( + "sha256" + ): + raise ValueError("replay seed authority differs from the frozen selection") + for row in rows: + key = (model, str(row["engine"]), int(row["task_id"])) + if key in authoritative: + raise ValueError("replay seed authority contains duplicate final executions") + authoritative[key] = row + seed_tasks = seeds.get("tasks") or [] + seed_task_ids = [int(item["task_id"]) for item in seed_tasks] + if len(seed_task_ids) != len(set(seed_task_ids)) or set(seed_task_ids) != set(task_ids): + raise ValueError("replay seeds do not match the frozen task selection") + ready = [item for item in seed_tasks if item.get("replay_status") == "ready"] + if len(ready) != int(repair.get("required_ready_tasks") or 0): + raise ValueError("replay seeds do not provide the required ready-task coverage") + for item in seed_tasks: + candidate_ids = [ + str(candidate.get("id") or "") for candidate in item.get("candidates") or [] + ] + canonical_id = str(item.get("canonical_candidate_id") or "") + if ( + not canonical_id + or candidate_ids.count(canonical_id) != 1 + or len(candidate_ids) != len(set(candidate_ids)) + ): + raise ValueError("every replay task must identify one unique canonical trajectory") + canonical = next( + candidate + for candidate in item.get("candidates") or [] + if candidate.get("id") == canonical_id + ) + _validate_canonical_candidate( + item, + canonical, + str(repair["expert_repairs_sha256"]), + repair_records.get(int(item["task_id"])), + authoritative.get( + ( + str((canonical.get("method") or {}).get("model") or ""), + str((canonical.get("method") or {}).get("engine") or ""), + int(item["task_id"]), + ) + ), + ) + + methods = profile.get("methods") or [] + method_ids = [str(item.get("id") or "") for item in methods] + engines = [str(item.get("engine") or "") for item in methods] + if ( + not methods + or len(method_ids) != len(set(method_ids)) + or len(engines) != len(set(engines)) + or any(engine not in ENGINE_IMAGES for engine in engines) + ): + raise ValueError("trajectory replay methods must be unique registered engines") + + execution = profile.get("execution") or {} + if execution.get("environment_pool_count") != 1: + raise ValueError("the fixed-port WebArena stack supports exactly one environment pool") + if execution.get("method_parallelism") != 1 or execution.get("task_parallelism") != 1: + raise ValueError("one environment pool requires serial methods and serial tasks") + if not execution.get("reset_before_every_task") or not execution.get( + "teardown_between_methods" + ): + raise ValueError("formal replay requires task resets and method teardown") + trusted_source_site_hosts = { + str(host) for host in execution.get("trusted_source_site_hosts") or [] + } + if not trusted_source_site_hosts or any( + not host or ":" in host or "/" in host for host in trusted_source_site_hosts + ): + raise ValueError("formal replay requires explicit trusted source site hosts") + for task in seed_tasks: + canonical = next( + candidate + for candidate in task.get("candidates") or [] + if candidate.get("id") == task.get("canonical_candidate_id") + ) + untrusted = replay_site_hosts(canonical["tool_calls"]) - trusted_source_site_hosts + if untrusted: + raise ValueError( + f"task {task['task_id']} canonical trajectory uses untrusted site hosts: " + f"{sorted(untrusted)}" + ) + if profile.get("runs_per_task") != 1: + raise ValueError("formal replay requires exactly one logical run per task and method") + if repair.get("canonical_trajectories_per_task") != 1: + raise ValueError("formal replay requires exactly one canonical trajectory per task") + if (profile.get("scoring") or {}).get("model_calls") != 0: + raise ValueError("trajectory replay must not call a model") + + return { + "profile": profile, + "profile_path": str(profile_file), + "profile_sha256": _sha256(profile_file), + "seed_manifest": seeds, + "seed_manifest_path": str(seed_file), + "seed_manifest_sha256": _sha256(seed_file), + "task_ids": task_ids, + "methods": methods, + "engines": engines, + "trusted_source_site_hosts": trusted_source_site_hosts, + } diff --git a/harness/trajectory_replay/webarena_replay.py b/harness/trajectory_replay/webarena_replay.py index 0e1c238c0..1e881a506 100644 --- a/harness/trajectory_replay/webarena_replay.py +++ b/harness/trajectory_replay/webarena_replay.py @@ -18,13 +18,17 @@ from harness.browser_use.runner import ( SITE_PORTS, _allow_official_container_hostname, + _cdp_metadata, _configure_benchmark, _container_host_gateway, + _docker_image_metadata, _login_context, _physical_execution_lock, _physical_execution_lock_path, _prepare_sites, + _require_clean_git_checkout, _resource_summary, + _site_container_manager, ) from harness.trajectory_replay.metrics import DockerStatsResources, ResourceSampler from harness.trajectory_replay.runner import ( @@ -40,6 +44,33 @@ evaluate_corrected_contract, load_corrections, ) +from harness.trajectory_replay.webarena_plan import ( + load_replay_plan, + replay_calls_binding_sha256, + replay_site_hosts, +) + +RECEIPT_SCHEMA = "lex_webarena_trajectory_replay_suite/1" + + +def _sha256(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def _json_sha256(value: Any) -> str: + payload = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + return hashlib.sha256(payload.encode()).hexdigest() + + +def _origin(url: str) -> str: + parsed = urlsplit(url) + if parsed.scheme not in {"http", "https"} or not parsed.netloc: + raise ValueError(f"invalid WebArena environment URL: {url}") + return urlunsplit((parsed.scheme, parsed.netloc, "", "", "")) + + +def _evidence_file(path: Path) -> dict[str, str] | None: + return {"path": str(path), "sha256": _sha256(path)} if path.is_file() else None def _candidate(task: dict[str, Any]) -> dict[str, Any]: @@ -62,27 +93,65 @@ def select_tasks(seed_manifest: dict[str, Any], task_ids: list[int]) -> list[dic return selected -def _rebind_url(value: str, site_access_host: str) -> str: +def _rebind_url( + value: str, site_access_host: str, trusted_source_site_hosts: set[str] +) -> str: parsed = urlsplit(value) - if parsed.scheme not in {"http", "https"} or parsed.port not in { - port for pair in SITE_PORTS.values() for port in pair - }: + if ( + parsed.scheme not in {"http", "https"} + or parsed.port not in {port for pair in SITE_PORTS.values() for port in pair} + or parsed.hostname not in trusted_source_site_hosts + ): return value host = f"[{site_access_host}]" if ":" in site_access_host else site_access_host netloc = f"{host}:{parsed.port}" return urlunsplit((parsed.scheme, netloc, parsed.path, parsed.query, parsed.fragment)) -def _rebind_calls(calls: list[dict[str, Any]], site_access_host: str) -> list[dict[str, Any]]: +def _rebind_calls( + calls: list[dict[str, Any]], + site_access_host: str, + trusted_source_site_hosts: set[str], +) -> list[dict[str, Any]]: rebound = [] for call in calls: item = dict(call) if isinstance(item.get("url"), str): - item["url"] = _rebind_url(item["url"], site_access_host) + item["url"] = _rebind_url( + item["url"], site_access_host, trusted_source_site_hosts + ) rebound.append(item) return rebound +def _clear_browser_state(service: Any, origins: set[str]) -> None: + """Fail closed unless all persistent browser state is cleared before a task.""" + + def clear() -> None: + context = service._context + page = service._page + if context is None or page is None: + raise RuntimeError("browser context is not ready for state reset") + context.clear_cookies() + context.clear_permissions() + cdp = context.new_cdp_session(page) + try: + cdp.send("Network.enable") + cdp.send("Network.clearBrowserCache") + cdp.send("Network.clearBrowserCookies") + for origin in sorted(origins): + cdp.send( + "Storage.clearDataForOrigin", + {"origin": origin, "storageTypes": "all"}, + ) + page.goto("about:blank", wait_until="load", timeout=10_000) + cdp.send("Page.resetNavigationHistory") + finally: + cdp.detach() + + service._submit(clear) + + def _run_one( # noqa: PLR0913 *, task_seed: dict[str, Any], @@ -93,6 +162,8 @@ def _run_one( # noqa: PLR0913 cowagent_repo: Path, output_dir: Path, sample_interval_ms: int, + physical_execution: int, + trusted_source_site_hosts: set[str], ) -> dict[str, Any]: task_id = int(task_seed["task_id"]) candidate = _candidate(task_seed) @@ -105,8 +176,12 @@ def _run_one( # noqa: PLR0913 ) sites = tuple(official_task.sites) start_urls = list(benchmark.config.render_url(list(official_task.start_urls), list(sites))) - calls = _rebind_calls(candidate["tool_calls"], _container_host_gateway(container)) - attempt_dir = output_dir / engine / str(task_id) + site_access_host = _container_host_gateway(container) + calls = _rebind_calls( + candidate["tool_calls"], site_access_host, trusted_source_site_hosts + ) + source_site_hosts = replay_site_hosts(candidate["tool_calls"]) + attempt_dir = output_dir / engine / str(task_id) / f"physical-{physical_execution}" attempt_dir.mkdir(parents=True, exist_ok=True) har_path = attempt_dir / "network.har" trace_path = attempt_dir / "trace.json" @@ -147,6 +222,10 @@ def _run_one( # noqa: PLR0913 setup_started = time.perf_counter() service = tool._get_service() service._submit(lambda: None) + origins = { + _origin(benchmark.config.get_environment(site).active_url) for site in SITE_PORTS + } + _clear_browser_state(service, origins) readiness_retries = _login_context(service, sites, benchmark, start_urls) service._submit(lambda: recorder.start(service._context)) timings["browser_session_setup_ms"] = (time.perf_counter() - setup_started) * 1000 @@ -266,7 +345,16 @@ def _run_one( # noqa: PLR0913 "schema": "lex_webarena_trajectory_replay_attempt/1", "engine": engine, "task_id": task_id, + "physical_execution": physical_execution, "source_candidate_id": candidate["id"], + "source_tool_calls_sha256": _json_sha256(candidate["tool_calls"]), + "executed_tool_calls_sha256": _json_sha256(calls), + "source_site_hosts": sorted(source_site_hosts), + "runtime_site_host": site_access_host, + "replay_calls_binding_sha256": replay_calls_binding_sha256( + calls, allowed_hosts={site_access_host} + ), + "evaluation_input_sha256": _json_sha256(candidate["recorded_response"]), "source_model": (candidate.get("method") or {}).get("model"), "source_engine": (candidate.get("method") or {}).get("engine"), "source_kind": (candidate.get("source") or {}).get("kind", "browser_use_execution"), @@ -294,6 +382,12 @@ def _run_one( # noqa: PLR0913 else {} ), "error": error, + "evidence": { + "trace": _evidence_file(trace_path), + "network_har": _evidence_file(har_path), + "evaluation": _evidence_file(evaluation_path), + "failure_screenshot": _evidence_file(screenshot_path), + }, "paths": { "trace": str(trace_path), "network_har": str(har_path), @@ -312,132 +406,453 @@ def _browser_context_needs_restart(attempt: dict[str, Any]) -> bool: ) -def run_canary_replay( # noqa: PLR0913 +def _stop_all_sites() -> None: + """Stop the fixed-port site stack before another method can acquire it.""" + + for site in SITE_PORTS: + manager = _site_container_manager(site) + if manager.is_running(): + manager.stop() + + +def _teardown_method(client: Any, run_id: str, engine: str) -> None: + """Remove every runtime object for a method and fail if isolation is incomplete.""" + + errors = [] + labels = [ + f"lex.browser-use-eval.run=replay-{run_id}", + f"lex.browser-use-eval.engine={engine}", + ] + for container in client.containers.list(all=True, filters={"label": labels}): + try: + container.remove(force=True) + except Exception as exc: + errors.append(f"browser container {container.name}: {exc}") + network_name = f"lex-webarena-replay-{run_id}-{engine}" + for network in client.networks.list(names=[network_name]): + try: + network.remove() + except Exception as exc: + errors.append(f"browser network {network.name}: {exc}") + try: + _stop_all_sites() + except Exception as exc: + errors.append(f"site stack: {exc}") + if errors: + raise RuntimeError("method teardown failed: " + "; ".join(errors)) + + +def _remove_orphan_replay_runtimes(client: Any, current_run_id: str) -> None: + """Remove stale replay containers and networks while the global stack lock is held.""" + + errors = [] + for container in client.containers.list( + all=True, filters={"label": "lex.browser-use-eval.run"} + ): + run_label = str((container.labels or {}).get("lex.browser-use-eval.run") or "") + if run_label.startswith("replay-") and run_label != f"replay-{current_run_id}": + try: + container.remove(force=True) + except Exception as exc: + errors.append(f"orphan container {container.name}: {exc}") + for network in client.networks.list(): + if network.name.startswith("lex-webarena-replay-") and current_run_id not in network.name: + try: + network.remove() + except Exception as exc: + errors.append(f"orphan network {network.name}: {exc}") + if errors: + raise RuntimeError("orphan replay cleanup failed: " + "; ".join(errors)) + + +def _write_receipt(path: Path, receipt: dict[str, Any]) -> None: + temporary = path.with_suffix(".tmp") + temporary.write_text(json.dumps(receipt, ensure_ascii=False, indent=2), encoding="utf-8") + temporary.replace(path) + + +def _next_physical_execution(output: Path, engine: str, task_id: int) -> int: + task_dir = output / engine / str(task_id) + existing = { + int(path.name.removeprefix("physical-")) + for path in task_dir.glob("physical-*") + if path.is_dir() and path.name.removeprefix("physical-").isdigit() + } + return max(existing, default=0) + 1 + + +def _start_physical_execution( + receipt: dict[str, Any], *, engine: str, task_id: int, physical_execution: int +) -> dict[str, Any]: + entry = { + "engine": engine, + "task_id": task_id, + "physical_execution": physical_execution, + "status": "running", + "started_at_unix_ms": round(time.time() * 1000), + } + receipt.setdefault("physical_executions", []).append(entry) + return entry + + +def _finish_physical_execution(entry: dict[str, Any], attempt: dict[str, Any]) -> None: + entry.update( + { + "status": "complete", + "finished_at_unix_ms": round(time.time() * 1000), + "attempt_status": attempt["status"], + "duration_ms": attempt["duration_ms"], + "evidence": attempt["evidence"], + } + ) + + +def _validated_resume( + receipt_path: Path, *, - seed_manifest_path: str | Path, + plan: dict[str, Any], task_ids: list[int], engines: list[str], + scope: str, + runtime: dict[str, Any], +) -> tuple[ + str, + list[dict[str, Any]], + dict[str, bool], + list[dict[str, Any]], + list[dict[str, Any]], +]: + if not receipt_path.is_file(): + return uuid.uuid4().hex[:12], [], {engine: False for engine in engines}, [], [] + receipt = json.loads(receipt_path.read_text(encoding="utf-8")) + expected = { + "schema": RECEIPT_SCHEMA, + "execution_scope": scope, + "profile_sha256": plan["profile_sha256"], + "source_seed_manifest_sha256": plan["seed_manifest_sha256"], + "task_ids": task_ids, + "engines": engines, + "runtime": runtime, + } + for key, value in expected.items(): + if receipt.get(key) != value: + raise ValueError(f"existing replay receipt differs on {key}") + attempts = list(receipt.get("attempts") or []) + canonical_ids = { + int(task["task_id"]): str(task["canonical_candidate_id"]) + for task in plan["seed_manifest"].get("tasks") or [] + } + allowed_keys = {(engine, task_id) for engine in engines for task_id in task_ids} + keys = [] + for item in attempts: + if item.get("schema") != "lex_webarena_trajectory_replay_attempt/1": + raise ValueError("existing replay receipt contains an invalid attempt schema") + key = (str(item.get("engine")), int(item.get("task_id"))) + if key not in allowed_keys: + raise ValueError("existing replay receipt contains an attempt outside the frozen plan") + if item.get("source_candidate_id") != canonical_ids[key[1]]: + raise ValueError("existing replay receipt contains a non-canonical trajectory") + keys.append(key) + if len(keys) != len(set(keys)): + raise ValueError("existing replay receipt contains duplicate method/task attempts") + method_teardown = receipt.get("method_teardown") or {} + if set(method_teardown) != set(engines) or any( + not isinstance(value, bool) for value in method_teardown.values() + ): + raise ValueError("existing replay receipt has invalid method teardown state") + physical_executions = list(receipt.get("physical_executions") or []) + if any(item.get("status") != "complete" for item in physical_executions): + raise ValueError( + "existing replay receipt contains an interrupted physical execution; " + "restart the complete formal scope in a new output directory" + ) + physical_keys = [ + (str(item.get("engine")), int(item.get("task_id")), int(item.get("physical_execution"))) + for item in physical_executions + ] + if len(physical_keys) != len(set(physical_keys)): + raise ValueError("existing replay receipt contains duplicate physical executions") + physical_by_logical: dict[tuple[str, int], list[dict[str, Any]]] = {} + for item in physical_executions: + key = (str(item.get("engine")), int(item.get("task_id"))) + if key not in allowed_keys: + raise ValueError( + "existing replay receipt contains a physical execution outside the plan" + ) + physical_by_logical.setdefault(key, []).append(item) + attempts_by_key = { + (str(item["engine"]), int(item["task_id"])): item for item in attempts + } + if set(physical_by_logical) != set(attempts_by_key): + raise ValueError("existing replay receipt has an unbound physical execution") + for key, attempt in attempts_by_key.items(): + physical = physical_by_logical[key] + if ( + len(physical) != int(attempt.get("physical_execution_count") or 1) + or max(int(item["physical_execution"]) for item in physical) + != int(attempt["physical_execution"]) + ): + raise ValueError("existing replay receipt has inconsistent physical execution history") + sessions = list(receipt.get("sessions") or []) + session_ids = [str(session.get("id") or "") for session in sessions] + if len(session_ids) != len(set(session_ids)) or any(not item for item in session_ids): + raise ValueError("existing replay receipt has invalid browser sessions") + sessions_by_id = {str(session["id"]): session for session in sessions} + for attempt in attempts: + session = sessions_by_id.get(str(attempt.get("browser_session_id") or "")) + if ( + session is None + or session.get("engine") != attempt.get("engine") + or int(attempt["task_id"]) not in session.get("task_ids", []) + ): + raise ValueError("existing replay attempt is not bound to a browser session") + return ( + str(receipt["run_id"]), + attempts, + dict(method_teardown), + sessions, + physical_executions, + ) + + +def run_replay_suite( # noqa: PLR0913 + *, + profile_path: str | Path, + seed_manifest_path: str | Path, output_dir: str | Path, cowagent_repo: str | Path, - cpus: float = 2.0, - memory: str = "2g", - sample_interval_ms: int = 100, + diagnostic_task_ids: list[int] | None = None, ) -> dict[str, Any]: - seeds = json.loads(Path(seed_manifest_path).read_text(encoding="utf-8")) + plan = load_replay_plan(profile_path, seed_manifest_path) + seeds = plan["seed_manifest"] + task_ids = list(plan["task_ids"] if diagnostic_task_ids is None else diagnostic_task_ids) tasks = select_tasks(seeds, task_ids) - unknown_engines = sorted(set(engines) - set(ENGINE_IMAGES)) - if unknown_engines: - raise ValueError(f"unsupported replay engines: {unknown_engines}") + engines = list(plan["engines"]) + scope = "formal" if diagnostic_task_ids is None else "diagnostic" + resources = plan["profile"].get("resources") or {} + cpus = float(resources["cpus"]) + memory = str(resources["memory"]) + sample_interval_ms = int(resources["sample_interval_ms"]) output = Path(output_dir).expanduser().resolve() output.mkdir(parents=True, exist_ok=True) + receipt_path = output / "receipt.json" + if any(output.iterdir()) and not receipt_path.is_file(): + raise ValueError("replay output directory is not empty and has no resumable receipt") cowagent = Path(cowagent_repo).expanduser().resolve() if not (cowagent / "agent/tools/browser/browser_tool.py").is_file(): raise ValueError("cowagent_repo does not contain BrowserTool") + repo_root = Path(__file__).resolve().parents[2] + browser_eval_commit = _require_clean_git_checkout(repo_root) + cowagent_commit = _require_clean_git_checkout(cowagent) client = docker.from_env() - run_id = uuid.uuid4().hex[:12] - containers: dict[str, Any] = {} - networks: dict[str, Any] = {} - endpoints: dict[str, str] = {} - attempts: list[dict[str, Any]] = [] - try: - for engine in engines: - network = client.networks.create(f"lex-webarena-replay-{run_id}-{engine}") - networks[engine] = network - container = _start_browser_container( - client, - engine=engine, - run_id=f"replay-{run_id}", - cpus=cpus, - memory=memory, - network=network.name, - ) - containers[engine] = container - container.reload() - http_endpoint = _published_endpoint( - container, engine=engine, cdp_port=ENGINE_PORTS[engine] - ) - endpoints[engine] = _wait_cdp(http_endpoint, container=container) - - gateway_addresses = { - _container_host_gateway(container) for container in containers.values() + browser_images = { + engine: _docker_image_metadata(client.images.get(ENGINE_IMAGES[engine])) + for engine in engines + } + site_images = {} + for site in SITE_PORTS: + reference = _site_container_manager(site).config.docker_img + site_images[site.value] = { + "reference": reference, + **_docker_image_metadata(client.images.get(reference)), } - if len(gateway_addresses) != 1: - raise RuntimeError(f"browser containers disagree on host gateway: {gateway_addresses}") - benchmark = _configure_benchmark(hostname=gateway_addresses.pop()) - lock_path = _physical_execution_lock_path() - for task_index, task in enumerate(tasks): - order = engines if task_index % 2 == 0 else list(reversed(engines)) - for engine in order: - with _physical_execution_lock(lock_path): + runtime = { + "browser_eval_commit": browser_eval_commit, + "cowagent_commit": cowagent_commit, + "resources": { + "cpus": cpus, + "memory": memory, + "sample_interval_ms": sample_interval_ms, + }, + "browser_images": browser_images, + "site_images": site_images, + } + run_id, attempts, method_teardown, sessions, physical_executions = _validated_resume( + receipt_path, + plan=plan, + task_ids=task_ids, + engines=engines, + scope=scope, + runtime=runtime, + ) + completed = {(str(item["engine"]), int(item["task_id"])) for item in attempts} + lock_path = _physical_execution_lock_path() + receipt = { + "schema": RECEIPT_SCHEMA, + "run_id": run_id, + "execution_scope": scope, + "status": "running", + "profile": plan["profile_path"], + "profile_sha256": plan["profile_sha256"], + "source_seed_manifest": plan["seed_manifest_path"], + "source_seed_manifest_sha256": plan["seed_manifest_sha256"], + "task_ids": task_ids, + "engines": engines, + "runtime": runtime, + "method_teardown": method_teardown, + "sessions": sessions, + "physical_executions": physical_executions, + "isolation": { + **plan["profile"]["execution"], + "physical_execution_lock_namespace": lock_path.stem, + }, + "attempts": attempts, + } + _write_receipt(receipt_path, receipt) + with _physical_execution_lock(lock_path) as lock_wait_ms: + _remove_orphan_replay_runtimes(client, run_id) + for engine in engines: + _teardown_method(client, run_id, engine) + method_teardown[engine] = True + receipt["method_teardown"] = method_teardown + receipt["isolation"]["lock_wait_ms"] = round(lock_wait_ms, 3) + _write_receipt(receipt_path, receipt) + for method in plan["methods"]: + engine = str(method["engine"]) + pending = [task for task in tasks if (engine, int(task["task_id"])) not in completed] + if not pending: + continue + network = None + container = None + try: + _stop_all_sites() + method_teardown[engine] = False + receipt["method_teardown"] = method_teardown + _write_receipt(receipt_path, receipt) + network = client.networks.create(f"lex-webarena-replay-{run_id}-{engine}") + container = _start_browser_container( + client, + engine=engine, + run_id=f"replay-{run_id}", + cpus=cpus, + memory=memory, + network=network.name, + ) + container.reload() + http_endpoint = _published_endpoint( + container, engine=engine, cdp_port=ENGINE_PORTS[engine] + ) + endpoint = _wait_cdp(http_endpoint, container=container) + benchmark = _configure_benchmark(hostname=_container_host_gateway(container)) + session = { + "id": uuid.uuid4().hex[:12], + "engine": engine, + "container_id": container.id, + "browser_image": _docker_image_metadata(container.image), + "browser": _cdp_metadata(http_endpoint), + "site_access_host": _container_host_gateway(container), + "task_ids": [], + } + sessions.append(session) + receipt["sessions"] = sessions + _write_receipt(receipt_path, receipt) + for task in pending: + physical_execution = _next_physical_execution( + output, engine, int(task["task_id"]) + ) + physical_entry = _start_physical_execution( + receipt, + engine=engine, + task_id=int(task["task_id"]), + physical_execution=physical_execution, + ) + _write_receipt(receipt_path, receipt) attempt = _run_one( task_seed=task, engine=engine, - endpoint=endpoints[engine], - container=containers[engine], + endpoint=endpoint, + container=container, benchmark=benchmark, cowagent_repo=cowagent, output_dir=output, sample_interval_ms=sample_interval_ms, + physical_execution=physical_execution, + trusted_source_site_hosts=plan["trusted_source_site_hosts"], ) + attempt["browser_session_id"] = session["id"] + _finish_physical_execution(physical_entry, attempt) + _write_receipt(receipt_path, receipt) if _browser_context_needs_restart(attempt): # A long-lived Moli process can leave its default context permanently # unloaded. Waiting on that same context cannot repair it, so restart # the browser container and give this pre-action setup one opportunity. - containers[engine].restart() - containers[engine].reload() + container.restart() + container.reload() http_endpoint = _published_endpoint( - containers[engine], engine=engine, cdp_port=ENGINE_PORTS[engine] + container, engine=engine, cdp_port=ENGINE_PORTS[engine] ) - endpoints[engine] = _wait_cdp( - http_endpoint, container=containers[engine] + endpoint = _wait_cdp(http_endpoint, container=container) + prior_setup_attempt = attempt + session = { + "id": uuid.uuid4().hex[:12], + "engine": engine, + "container_id": container.id, + "browser_image": _docker_image_metadata(container.image), + "browser": _cdp_metadata(http_endpoint), + "site_access_host": _container_host_gateway(container), + "task_ids": [], + } + sessions.append(session) + receipt["sessions"] = sessions + retry_entry = _start_physical_execution( + receipt, + engine=engine, + task_id=int(task["task_id"]), + physical_execution=physical_execution + 1, ) + _write_receipt(receipt_path, receipt) attempt = _run_one( task_seed=task, engine=engine, - endpoint=endpoints[engine], - container=containers[engine], + endpoint=endpoint, + container=container, benchmark=benchmark, cowagent_repo=cowagent, output_dir=output, sample_interval_ms=sample_interval_ms, + physical_execution=physical_execution + 1, + trusted_source_site_hosts=plan["trusted_source_site_hosts"], ) - attempts.append(attempt) - print( - json.dumps( - { - "task_id": attempt["task_id"], - "engine": attempt["engine"], - "process_status": attempt["process_status"], - "evaluator_status": attempt["evaluator_status"], - } - ), - flush=True, - ) - finally: - for container in containers.values(): - try: - container.remove(force=True) - except Exception: - pass - for network in networks.values(): - try: - network.remove() - except Exception: - pass + attempt["browser_session_id"] = session["id"] + attempt["prior_setup_attempts"] = [prior_setup_attempt] + attempt["physical_execution_count"] = 2 + attempt["duration_ms"] = round( + float(prior_setup_attempt["duration_ms"]) + + float(attempt["duration_ms"]), + 3, + ) + _finish_physical_execution(retry_entry, attempt) + _write_receipt(receipt_path, receipt) + attempts.append(attempt) + session["task_ids"].append(int(task["task_id"])) + completed.add((engine, int(task["task_id"]))) + receipt["attempts"] = attempts + _write_receipt(receipt_path, receipt) + print( + json.dumps( + { + "task_id": attempt["task_id"], + "engine": attempt["engine"], + "process_status": attempt["process_status"], + "evaluator_status": attempt["evaluator_status"], + } + ), + flush=True, + ) + finally: + _teardown_method(client, run_id, engine) + method_teardown[engine] = True + receipt["method_teardown"] = method_teardown + _write_receipt(receipt_path, receipt) - result = { - "schema": "lex_webarena_trajectory_replay_canary/1", - "run_id": run_id, - "source_seed_manifest": str(Path(seed_manifest_path).resolve()), - "source_seed_manifest_sha256": hashlib.sha256( - Path(seed_manifest_path).read_bytes() - ).hexdigest(), - "task_ids": task_ids, - "engines": engines, - "attempts": attempts, - } - (output / "canary.json").write_text( - json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8" - ) - return result + expected = {(engine, task_id) for engine in engines for task_id in task_ids} + actual = {(str(item["engine"]), int(item["task_id"])) for item in attempts} + if actual != expected or len(attempts) != len(expected): + raise RuntimeError("formal replay did not produce the complete method-task matrix") + if not all(method_teardown.values()): + raise RuntimeError("formal replay did not complete method teardown") + receipt["status"] = "complete" + receipt["attempts"] = attempts + _write_receipt(receipt_path, receipt) + return receipt diff --git a/research/webarena-mac-expansion.json b/research/webarena-mac-expansion.json index 5edd97025..08ed7c3b7 100644 --- a/research/webarena-mac-expansion.json +++ b/research/webarena-mac-expansion.json @@ -6,8 +6,7 @@ "hard": 258, "current_four_sites_single_site": 655, "current_four_sites_including_cross_site": 678, - "current_four_sites_hard_including_cross_site": 233, - "official_179_subset": false + "current_four_sites_hard_including_cross_site": 233 }, "environments": [ ["Shopping", "已在本机实跑", "187", "56", "amd64 镜像;已通过 Colima 转译运行"], diff --git a/scripts/promote_webarena_trajectories.py b/scripts/promote_webarena_trajectories.py index dfa0da3ea..948d39330 100644 --- a/scripts/promote_webarena_trajectories.py +++ b/scripts/promote_webarena_trajectories.py @@ -9,12 +9,13 @@ from harness.trajectory_replay.webarena_dataset import build_verified_trajectory_dataset +CURRENT_PROFILE = "evaluation/trajectory-replay/webarena-verified/current-run.json" + def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("--seeds", required=True) parser.add_argument("--receipt", action="append", required=True) - parser.add_argument("--required-engine", action="append", default=[]) parser.add_argument("--prior-dataset") parser.add_argument("--output", required=True) return parser.parse_args() @@ -25,7 +26,7 @@ def main() -> None: data = build_verified_trajectory_dataset( args.seeds, args.receipt, - required_engines=tuple(args.required_engine or ("chromium", "moli")), + profile_path=CURRENT_PROFILE, prior_dataset_path=args.prior_dataset, ) output = Path(args.output) diff --git a/scripts/run_webarena_trajectory_replay.py b/scripts/run_webarena_trajectory_replay.py new file mode 100644 index 000000000..2f3e97e32 --- /dev/null +++ b/scripts/run_webarena_trajectory_replay.py @@ -0,0 +1,39 @@ +#!/usr/bin/env python3 +"""Run the frozen model-free WebArena trajectory-replay suite.""" + +from __future__ import annotations + +import argparse + +from harness.trajectory_replay.webarena_replay import run_replay_suite + +CURRENT_PROFILE = "evaluation/trajectory-replay/webarena-verified/current-run.json" + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument("--seeds", required=True) + parser.add_argument("--output", required=True) + parser.add_argument("--cowagent-repo", required=True) + parser.add_argument( + "--diagnostic-task", + action="append", + type=int, + help="run an explicitly non-promotable subset instead of the frozen full suite", + ) + return parser.parse_args() + + +def main() -> None: + args = parse_args() + run_replay_suite( + profile_path=CURRENT_PROFILE, + seed_manifest_path=args.seeds, + output_dir=args.output, + cowagent_repo=args.cowagent_repo, + diagnostic_task_ids=args.diagnostic_task, + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/run_webarena_trajectory_replay_canary.py b/scripts/run_webarena_trajectory_replay_canary.py deleted file mode 100644 index 520a5d1de..000000000 --- a/scripts/run_webarena_trajectory_replay_canary.py +++ /dev/null @@ -1,39 +0,0 @@ -#!/usr/bin/env python3 -"""Run a small model-free WebArena trajectory replay canary.""" - -from __future__ import annotations - -import argparse - -from harness.trajectory_replay.webarena_replay import run_canary_replay - - -def parse_args() -> argparse.Namespace: - parser = argparse.ArgumentParser() - parser.add_argument("--seeds", required=True) - parser.add_argument("--task", action="append", required=True, type=int) - parser.add_argument("--engine", action="append", choices=("chromium", "moli"), required=True) - parser.add_argument("--output", required=True) - parser.add_argument("--cowagent-repo", required=True) - parser.add_argument("--cpus", type=float, default=2.0) - parser.add_argument("--memory", default="2g") - parser.add_argument("--sample-interval-ms", type=int, default=100) - return parser.parse_args() - - -def main() -> None: - args = parse_args() - run_canary_replay( - seed_manifest_path=args.seeds, - task_ids=args.task, - engines=args.engine, - output_dir=args.output, - cowagent_repo=args.cowagent_repo, - cpus=args.cpus, - memory=args.memory, - sample_interval_ms=args.sample_interval_ms, - ) - - -if __name__ == "__main__": - main() diff --git a/tests/browser_use/test_failure_cause_review.py b/tests/browser_use/test_failure_cause_review.py index e9594b7d2..75fa16f5d 100644 --- a/tests/browser_use/test_failure_cause_review.py +++ b/tests/browser_use/test_failure_cause_review.py @@ -6,7 +6,6 @@ validate_failure_cause_review, ) - MODEL_ID = "gpt-5.6-sol" RESULT_ID = f"{MODEL_ID}-chromium-124" diff --git a/tests/browser_use/test_report.py b/tests/browser_use/test_report.py index b36a02c48..63620f370 100644 --- a/tests/browser_use/test_report.py +++ b/tests/browser_use/test_report.py @@ -10,10 +10,10 @@ _attempt_table, _automatic_failure_type, _best, + _common_success_resource_rows, _failure_diagnosis, _failure_scope_counts, _first_final_rows, - _common_success_resource_rows, _load_evidence_bundles, _logical_results, _paired_summary, diff --git a/tests/browser_use/test_report_i18n.py b/tests/browser_use/test_report_i18n.py index d953f0dec..7997361c9 100644 --- a/tests/browser_use/test_report_i18n.py +++ b/tests/browser_use/test_report_i18n.py @@ -42,8 +42,16 @@ def test_browser_use_change_records_have_complete_structured_english() -> None: assert not re.search(r"[\u4e00-\u9fff]", english[field]) if collection.endswith("releases"): assert len(english["changes"]) == len(record.get("changes") or []) - assert all(len(target) == len(source) == 4 for source, target in zip(record.get("changes") or [], english["changes"], strict=True)) - assert not re.search(r"[\u4e00-\u9fff]", json.dumps(english["changes"], ensure_ascii=False)) + assert all( + len(target) == len(source) == 4 + for source, target in zip( + record.get("changes") or [], english["changes"], strict=True + ) + ) + assert not re.search( + r"[\u4e00-\u9fff]", + json.dumps(english["changes"], ensure_ascii=False), + ) def test_english_ui_is_localized_but_measured_text_is_preserved() -> None: @@ -57,7 +65,14 @@ def test_english_ui_is_localized_but_measured_text_is_preserved() -> None: parser = _VisibleText() parser.feed(localized) visible = " ".join(parser.values) - for leaked in ("联合多模型能力实测", "查看本题运行结果", "评测集页面保持", "程序自动定位", "核验根因子类", "核验原因"): + for leaked in ( + "联合多模型能力实测", + "查看本题运行结果", + "评测集页面保持", + "程序自动定位", + "核验根因子类", + "核验原因", + ): assert leaked not in visible assert "原始测量证据" in localized assert "创建一个命名明确的空仓库" in visible diff --git a/tests/trajectory_replay/test_report.py b/tests/trajectory_replay/test_report.py index bf40130cf..924088e3f 100644 --- a/tests/trajectory_replay/test_report.py +++ b/tests/trajectory_replay/test_report.py @@ -222,7 +222,9 @@ def test_report_uses_fixed_latest_filename_and_removes_legacy_versions(tmp_path) assert "人工复核" not in html_path.read_text(encoding="utf-8") audit = json.loads((reports / "trajectory-replay-evaluation-latest-data.json").read_text()) assert "human_review" not in audit["ground_truth_validation"] - method = (reports / "trajectory-replay-evaluation-latest-method.html").read_text(encoding="utf-8") + method = (reports / "trajectory-replay-evaluation-latest-method.html").read_text( + encoding="utf-8" + ) assert "RX = Receive" in method assert "P50 与中位数是同一个统计量" in method assert "Codex" in method @@ -487,7 +489,9 @@ def test_all_published_suite_tasks_link_to_public_web_targets(tmp_path): manifest={"run_id": "suite-links", "suite": suite}, summary={"functional": {}, "resource_comparison": {"by_engine": {}}}, ) - rendered = (reports / "trajectory-replay-evaluation-latest-suite.html").read_text(encoding="utf-8") + rendered = (reports / "trajectory-replay-evaluation-latest-suite.html").read_text( + encoding="utf-8" + ) links = re.findall( r'公网测试页面]+href="(https://[^\"]+)"', rendered, diff --git a/tests/trajectory_replay/test_runner.py b/tests/trajectory_replay/test_runner.py index d882c8040..b0c64e598 100644 --- a/tests/trajectory_replay/test_runner.py +++ b/tests/trajectory_replay/test_runner.py @@ -118,7 +118,9 @@ def diagnostic_failure(*a, **k): def test_moli_image_installs_its_verified_dynamic_font_dependency(): - dockerfile = Path("examples/trajectory_replay/docker/moli.Dockerfile").read_text(encoding="utf-8") + dockerfile = Path("examples/trajectory_replay/docker/moli.Dockerfile").read_text( + encoding="utf-8" + ) assert "fontconfig" in dockerfile diff --git a/tests/trajectory_replay/test_webarena_dataset.py b/tests/trajectory_replay/test_webarena_dataset.py index f274b977a..abd3bc64b 100644 --- a/tests/trajectory_replay/test_webarena_dataset.py +++ b/tests/trajectory_replay/test_webarena_dataset.py @@ -1,11 +1,22 @@ import hashlib import json +from importlib.metadata import version from pathlib import Path -from harness.trajectory_replay.webarena_dataset import build_verified_trajectory_dataset +from webarena_verified.core.utils.checksum import compute_evaluator_checksum + +from harness.trajectory_replay.webarena_dataset import ( + _official_evaluator_names, + build_verified_trajectory_dataset, +) +from harness.trajectory_replay.webarena_plan import ( + replay_calls_binding_sha256, + replay_site_hosts, +) def _write(path: Path, value: object) -> Path: + path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(value), encoding="utf-8") return path @@ -14,15 +25,298 @@ def _sha256(path: Path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest() +def _json_sha256(value: object) -> str: + payload = json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) + return hashlib.sha256(payload.encode()).hexdigest() + + +def _materialize_autonomous_sources(seeds: Path) -> dict[str, object]: + payload = json.loads(seeds.read_text(encoding="utf-8")) + payload["source"].setdefault("official_data_checksum", "data-checksum") + authority_rows: dict[str, list[dict[str, object]]] = {} + for task in payload["tasks"]: + for candidate in task.get("candidates") or []: + if candidate.get("candidate_kind") == "expert_repair": + continue + candidate.setdefault("method", {"model": "model", "engine": "chromium"}) + candidate.setdefault("source_execution", 1) + candidate.setdefault("source_run_id", "source-run") + candidate.setdefault("action_count", len(candidate.get("tool_calls") or [])) + candidate.setdefault("official_score", 1.0) + candidate.setdefault("clean", True) + candidate.setdefault("recorded_response", {}) + events = [] + for index, arguments in enumerate(candidate.get("tool_calls") or [], start=1): + call_id = f"call-{index}" + events.extend( + [ + { + "type": "tool_execution_start", + "data": { + "tool_call_id": call_id, + "tool_name": "BrowserTool", + "arguments": arguments, + }, + }, + { + "type": "tool_execution_end", + "data": {"tool_call_id": call_id, "status": "success"}, + }, + ] + ) + source_payloads = { + "events": events, + "response": candidate["recorded_response"], + "evaluation": { + "task_id": task["task_id"], + "status": "success", + "score": 1.0, + }, + } + source = candidate.setdefault("source", {}) + for kind, value in source_payloads.items(): + path = _write( + seeds.parent / f"source-{task['task_id']}-{candidate['id']}-{kind}.json", + value, + ) + source[f"{kind}_path"] = str(path) + source[f"{kind}_sha256"] = _sha256(path) + model = str(candidate["method"]["model"]) + authority_rows.setdefault(model, []).append( + { + "engine": candidate["method"]["engine"], + "task_id": task["task_id"], + "attempt": candidate["source_execution"], + "run_id": candidate["source_run_id"], + "score": 1.0, + "paths": { + kind: source[f"{kind}_path"] + for kind in ("events", "response", "evaluation") + }, + } + ) + source_evidence = {} + for model, rows in authority_rows.items(): + root = seeds.parent / f"authority-{model}" + manifest = _write( + root / "manifest.json", + { + "protocol": {"model": model}, + "selection_sha256": payload["source"]["selection_sha256"], + }, + ) + attempts = root / "attempts.jsonl" + attempts.parent.mkdir(parents=True, exist_ok=True) + attempts.write_text( + "".join(json.dumps(row) + "\n" for row in rows), encoding="utf-8" + ) + source_evidence[model] = { + "root": str(root), + "attempts_sha256": _sha256(attempts), + "manifest_sha256": _sha256(manifest), + } + payload["source"]["evidence"] = source_evidence + seeds.write_text(json.dumps(payload), encoding="utf-8") + return payload + + +def _profile(tmp_path: Path, task_ids: list[int]) -> tuple[Path, str, str]: + repo = tmp_path / "repo" + selection = _write( + repo / "evaluation/browser-use/selection.json", + {"tasks": [{"task_id": task_id} for task_id in task_ids]}, + ) + selection_sha256 = _sha256(selection) + repairs = _write( + repo / "evaluation/trajectory-replay/repairs.json", + { + "schema": "repairs", + "repairs": [ + { + "task_id": task_id, + "candidate_id": f"expert-{task_id}", + "source": {"author": "reviewed"}, + "repair_basis": {"root_cause": "wrong route selection"}, + "tool_calls": [{"action": "navigate", "url": "http://host"}], + "expected_response": {"status": "SUCCESS"}, + } + for task_id in task_ids + ], + }, + ) + profile = _write( + repo / "evaluation/trajectory-replay/webarena-verified/current-run.json", + { + "schema": "lex_webarena_trajectory_replay_profile/1", + "task_selection": { + "path": "evaluation/browser-use/selection.json", + "task_count": len(task_ids), + "sha256": selection_sha256, + }, + "seed_inventory": { + "expert_repairs_path": "evaluation/trajectory-replay/repairs.json", + "expert_repairs_sha256": _sha256(repairs), + "required_ready_tasks": len(task_ids), + "canonical_trajectories_per_task": 1, + }, + "methods": [ + {"id": "chromium", "engine": "chromium"}, + {"id": "moli", "engine": "moli"}, + ], + "runs_per_task": 1, + "resources": {"cpus": 2.0, "memory": "2g", "sample_interval_ms": 100}, + "execution": { + "environment_pool_count": 1, + "method_parallelism": 1, + "task_parallelism": 1, + "reset_before_every_task": True, + "teardown_between_methods": True, + "trusted_source_site_hosts": ["host.docker.internal"], + }, + "scoring": {"model_calls": 0}, + }, + ) + return profile, selection_sha256, _sha256(repairs) + + +def _receipt( + seeds: Path, + profile: Path, + *, + task_ids: list[int], + attempts: list[dict[str, object]], + run_id: str = "run", + execution_scope: str = "formal", +) -> dict[str, object]: + attempts = json.loads(json.dumps(attempts)) + seed_payload = _materialize_autonomous_sources(seeds) + seed_tasks = {int(item["task_id"]): item for item in seed_payload["tasks"]} + sessions = [] + physical_executions = [] + for attempt in attempts: + attempt["schema"] = "lex_webarena_trajectory_replay_attempt/1" + attempt["physical_execution"] = 1 + session_id = f"session-{attempt['engine']}" + attempt["browser_session_id"] = session_id + session = next((item for item in sessions if item["id"] == session_id), None) + if session is None: + session = { + "id": session_id, + "engine": attempt["engine"], + "site_access_host": "gateway", + "task_ids": [], + } + sessions.append(session) + session["task_ids"].append(attempt["task_id"]) + task = seed_tasks[int(attempt["task_id"])] + candidate = next( + item + for item in task["candidates"] + if item["id"] == attempt["source_candidate_id"] + ) + trace_arguments = candidate["tool_calls"] + attempt["source_tool_calls_sha256"] = _json_sha256(candidate["tool_calls"]) + attempt["executed_tool_calls_sha256"] = _json_sha256(trace_arguments) + attempt["source_site_hosts"] = sorted(replay_site_hosts(candidate["tool_calls"])) + attempt["runtime_site_host"] = "gateway" + attempt["replay_calls_binding_sha256"] = replay_calls_binding_sha256( + trace_arguments, allowed_hosts=replay_site_hosts(trace_arguments) + ) + attempt["evaluation_input_sha256"] = _json_sha256(candidate["recorded_response"]) + payloads = { + "trace": [ + {"index": index, "arguments": arguments, "status": "success"} + for index, arguments in enumerate(trace_arguments, start=1) + ], + "network_har": {"log": {"entries": []}}, + "evaluation": { + "task_id": attempt["task_id"], + "status": "success", + "score": 1.0, + "webarena_verified_version": version("webarena-verified"), + "webarena_verified_evaluator_checksum": compute_evaluator_checksum(), + "webarena_verified_data_checksum": seed_payload["source"][ + "official_data_checksum" + ], + "evaluators_results": [ + { + "evaluator_name": evaluator, + "status": "success", + "score": 1.0, + "actual": ( + candidate["recorded_response"] + if evaluator == "AgentResponseEvaluator" + else [{"method": "POST", "url": "http://site/action"}] + ), + } + for evaluator in _official_evaluator_names()[int(attempt["task_id"])] + ], + }, + } + evidence = {} + for kind, payload in payloads.items(): + path = _write( + seeds.parent + / f"{run_id}-{attempt['engine']}-{attempt['task_id']}-{kind}.json", + payload, + ) + evidence[kind] = {"path": str(path), "sha256": _sha256(path)} + attempt["evidence"] = evidence + physical_executions.append( + { + "engine": attempt["engine"], + "task_id": attempt["task_id"], + "physical_execution": 1, + "status": "complete", + "evidence": evidence, + } + ) + return { + "schema": "lex_webarena_trajectory_replay_suite/1", + "execution_scope": execution_scope, + "status": "complete", + "profile_sha256": _sha256(profile), + "source_seed_manifest_sha256": _sha256(seeds), + "task_ids": task_ids, + "engines": ["chromium", "moli"], + "runtime": { + "browser_eval_commit": "browser-eval-commit", + "cowagent_commit": "cowagent-commit", + "resources": {"cpus": 2.0, "memory": "2g", "sample_interval_ms": 100}, + "browser_images": { + "chromium": {"id": "chromium-image"}, + "moli": {"id": "moli-image"}, + }, + "site_images": {"gitlab": {"id": "gitlab-image"}}, + }, + "method_teardown": {"chromium": True, "moli": True}, + "sessions": sessions, + "physical_executions": physical_executions, + "isolation": { + "environment_pool_count": 1, + "method_parallelism": 1, + "task_parallelism": 1, + "reset_before_every_task": True, + "teardown_between_methods": True, + }, + "run_id": run_id, + "attempts": attempts, + } + + def test_promotion_requires_same_candidate_to_pass_every_browser(tmp_path: Path) -> None: + profile, selection_sha256, repair_sha256 = _profile(tmp_path, [44]) seeds = _write( tmp_path / "seeds.json", { "schema": "lex_browser_use_replay_seeds/1", - "source": {"selection_sha256": "selection"}, + "source": {"selection_sha256": selection_sha256}, + "expert_repairs": {"repair_spec_sha256": repair_sha256}, "tasks": [ { "task_id": 44, + "canonical_candidate_id": "candidate-44", + "replay_status": "ready", "intent": "Open todos", "sites": ["gitlab"], "start_urls": ["__GITLAB__"], @@ -47,11 +341,12 @@ def test_promotion_requires_same_candidate_to_pass_every_browser(tmp_path: Path) ) receipt = _write( tmp_path / "receipt.json", - { - "schema": "lex_webarena_trajectory_replay_canary/1", - "source_seed_manifest_sha256": _sha256(seeds), - "run_id": "run-44", - "attempts": [ + _receipt( + seeds, + profile, + task_ids=[44], + run_id="run-44", + attempts=[ { "task_id": 44, "source_candidate_id": "candidate-44", @@ -62,10 +357,10 @@ def test_promotion_requires_same_candidate_to_pass_every_browser(tmp_path: Path) } for engine in ("chromium", "moli") ], - }, + ), ) - result = build_verified_trajectory_dataset(seeds, [receipt]) + result = build_verified_trajectory_dataset(seeds, [receipt], profile_path=profile) assert result["counts"] == {"verified_tasks": 1, "verified_trajectories": 1} assert result["trajectories"][0]["verification"]["model_calls"] == 0 @@ -76,15 +371,19 @@ def test_promotion_requires_same_candidate_to_pass_every_browser(tmp_path: Path) assert result["semantics"]["browser_use_agent_visibility"] == "hidden" -def test_promotion_skips_partial_browser_verification(tmp_path: Path) -> None: +def test_promotion_rejects_incomplete_method_task_matrix(tmp_path: Path) -> None: + profile, selection_sha256, repair_sha256 = _profile(tmp_path, [1]) seeds = _write( tmp_path / "seeds.json", { "schema": "lex_browser_use_replay_seeds/1", - "source": {"selection_sha256": "selection"}, + "source": {"selection_sha256": selection_sha256}, + "expert_repairs": {"repair_spec_sha256": repair_sha256}, "tasks": [ { "task_id": 1, + "canonical_candidate_id": "candidate", + "replay_status": "ready", "candidates": [ { "id": "candidate", @@ -106,10 +405,11 @@ def test_promotion_skips_partial_browser_verification(tmp_path: Path) -> None: ) receipt = _write( tmp_path / "receipt.json", - { - "schema": "lex_webarena_trajectory_replay_canary/1", - "source_seed_manifest_sha256": _sha256(seeds), - "attempts": [ + _receipt( + seeds, + profile, + task_ids=[1], + attempts=[ { "task_id": 1, "source_candidate_id": "candidate", @@ -119,23 +419,30 @@ def test_promotion_skips_partial_browser_verification(tmp_path: Path) -> None: "completed_action_count": 1, } ], - }, + ), ) - result = build_verified_trajectory_dataset(seeds, [receipt]) - - assert result["counts"] == {"verified_tasks": 0, "verified_trajectories": 0} + try: + build_verified_trajectory_dataset(seeds, [receipt], profile_path=profile) + except ValueError as error: + assert "complete method-task matrix" in str(error) + else: + raise AssertionError("partial formal receipt must not be promotable") def test_promotion_supports_hash_bound_expert_repair(tmp_path: Path) -> None: + profile, selection_sha256, repair_sha256 = _profile(tmp_path, [339]) seeds = _write( tmp_path / "seeds.json", { "schema": "lex_browser_use_replay_seeds/1", - "source": {"selection_sha256": "selection"}, + "source": {"selection_sha256": selection_sha256}, + "expert_repairs": {"repair_spec_sha256": repair_sha256}, "tasks": [ { "task_id": 339, + "canonical_candidate_id": "expert-339", + "replay_status": "ready", "candidates": [ { "id": "expert-339", @@ -145,7 +452,7 @@ def test_promotion_supports_hash_bound_expert_repair(tmp_path: Path) -> None: "recorded_response": {"status": "SUCCESS"}, "source": { "kind": "expert_repair_spec", - "repair_spec_sha256": "repair-sha", + "repair_spec_sha256": repair_sha256, "details": {"author": "reviewed"}, }, "repair_basis": {"root_cause": "wrong route selection"}, @@ -157,11 +464,12 @@ def test_promotion_supports_hash_bound_expert_repair(tmp_path: Path) -> None: ) receipt = _write( tmp_path / "receipt.json", - { - "schema": "lex_webarena_trajectory_replay_canary/1", - "source_seed_manifest_sha256": _sha256(seeds), - "run_id": "run-339", - "attempts": [ + _receipt( + seeds, + profile, + task_ids=[339], + run_id="run-339", + attempts=[ { "task_id": 339, "source_candidate_id": "expert-339", @@ -175,14 +483,14 @@ def test_promotion_supports_hash_bound_expert_repair(tmp_path: Path) -> None: } for engine in ("chromium", "moli") ], - }, + ), ) - result = build_verified_trajectory_dataset(seeds, [receipt]) + result = build_verified_trajectory_dataset(seeds, [receipt], profile_path=profile) trajectory = result["trajectories"][0] assert trajectory["source"]["kind"] == "expert_repair_spec" - assert trajectory["source"]["repair_spec_sha256"] == "repair-sha" + assert trajectory["source"]["repair_spec_sha256"] == repair_sha256 assert trajectory["verification"]["contract"] == { "kind": "corrected_replay_contract", "correction_id": "wv-339-correction", @@ -199,8 +507,257 @@ def test_promotion_supports_hash_bound_expert_repair(tmp_path: Path) -> None: }, ) incremented = build_verified_trajectory_dataset( - seeds, [receipt], prior_dataset_path=prior + seeds, [receipt], profile_path=profile, prior_dataset_path=prior ) assert [item["task_id"] for item in incremented["trajectories"]] == [44, 339] assert incremented["counts"] == {"verified_tasks": 2, "verified_trajectories": 2} assert incremented["provenance"]["prior_dataset_sha256"] == _sha256(prior) + + +def test_promotion_rejects_diagnostic_or_nonisolated_receipts(tmp_path: Path) -> None: + profile, selection_sha256, repair_sha256 = _profile(tmp_path, [1]) + seeds = _write( + tmp_path / "seeds.json", + { + "schema": "lex_browser_use_replay_seeds/1", + "source": {"selection_sha256": selection_sha256}, + "expert_repairs": {"repair_spec_sha256": repair_sha256}, + "tasks": [ + { + "task_id": 1, + "replay_status": "ready", + "canonical_candidate_id": "expert-1", + "candidates": [ + { + "id": "expert-1", + "candidate_kind": "expert_repair", + "action_count": 1, + "tool_calls": [{"action": "navigate", "url": "http://host"}], + "recorded_response": {"status": "SUCCESS"}, + "source": { + "kind": "expert_repair_spec", + "repair_spec_sha256": repair_sha256, + "details": {"author": "reviewed"}, + }, + "repair_basis": {"root_cause": "wrong route selection"}, + } + ], + } + ], + }, + ) + diagnostic_path = _write( + tmp_path / "diagnostic.json", + _receipt(seeds, profile, task_ids=[1], attempts=[], execution_scope="diagnostic"), + ) + try: + build_verified_trajectory_dataset(seeds, [diagnostic_path], profile_path=profile) + except ValueError as error: + assert "completed formal suite" in str(error) + else: + raise AssertionError("diagnostic receipt must not be promotable") + + nonisolated = _receipt(seeds, profile, task_ids=[1], attempts=[]) + isolation = nonisolated["isolation"] + assert isinstance(isolation, dict) + isolation["method_parallelism"] = 2 + nonisolated_path = _write(tmp_path / "nonisolated.json", nonisolated) + try: + build_verified_trajectory_dataset(seeds, [nonisolated_path], profile_path=profile) + except ValueError as error: + assert "required isolation" in str(error) + else: + raise AssertionError("nonisolated receipt must not be promotable") + + +def test_promotion_rejects_receipt_claim_when_evaluation_evidence_failed( + tmp_path: Path, +) -> None: + profile, selection_sha256, repair_sha256 = _profile(tmp_path, [1]) + seeds = _write( + tmp_path / "seeds.json", + { + "schema": "lex_browser_use_replay_seeds/1", + "source": {"selection_sha256": selection_sha256}, + "expert_repairs": {"repair_spec_sha256": repair_sha256}, + "tasks": [ + { + "task_id": 1, + "replay_status": "ready", + "canonical_candidate_id": "candidate-1", + "candidates": [ + { + "id": "candidate-1", + "method": {"model": "model", "engine": "chromium"}, + "source_execution": 1, + "action_count": 1, + "tool_calls": [{"action": "snapshot"}], + "recorded_response": {"status": "SUCCESS"}, + "source": { + "events_sha256": "events", + "response_sha256": "response", + "evaluation_sha256": "evaluation", + }, + } + ], + } + ], + }, + ) + receipt_payload = _receipt( + seeds, + profile, + task_ids=[1], + attempts=[ + { + "task_id": 1, + "source_candidate_id": "candidate-1", + "engine": engine, + "process_status": "pass", + "evaluator_status": "pass", + "completed_action_count": 1, + } + for engine in ("chromium", "moli") + ], + ) + evaluation = Path(receipt_payload["attempts"][0]["evidence"]["evaluation"]["path"]) + evaluation.write_text( + json.dumps({"task_id": 1, "status": "fail", "score": 0}), encoding="utf-8" + ) + receipt_payload["attempts"][0]["evidence"]["evaluation"]["sha256"] = _sha256(evaluation) + receipt = _write(tmp_path / "receipt.json", receipt_payload) + + try: + build_verified_trajectory_dataset(seeds, [receipt], profile_path=profile) + except ValueError as error: + assert "passing score" in str(error) + else: + raise AssertionError("a receipt pass claim must not override failed evaluator evidence") + + +def test_promotion_rejects_trace_that_differs_from_canonical_actions(tmp_path: Path) -> None: + profile, selection_sha256, repair_sha256 = _profile(tmp_path, [1]) + seeds = _write( + tmp_path / "seeds.json", + { + "schema": "lex_browser_use_replay_seeds/1", + "source": {"selection_sha256": selection_sha256}, + "expert_repairs": {"repair_spec_sha256": repair_sha256}, + "tasks": [ + { + "task_id": 1, + "replay_status": "ready", + "canonical_candidate_id": "candidate-1", + "candidates": [ + { + "id": "candidate-1", + "method": {"model": "model", "engine": "chromium"}, + "source_execution": 1, + "action_count": 1, + "tool_calls": [{"action": "snapshot"}], + "recorded_response": {"status": "SUCCESS"}, + "source": {}, + } + ], + } + ], + }, + ) + receipt_payload = _receipt( + seeds, + profile, + task_ids=[1], + attempts=[ + { + "task_id": 1, + "source_candidate_id": "candidate-1", + "engine": engine, + "process_status": "pass", + "evaluator_status": "pass", + "completed_action_count": 1, + } + for engine in ("chromium", "moli") + ], + ) + attempt = receipt_payload["attempts"][0] + trace_path = Path(attempt["evidence"]["trace"]["path"]) + different_arguments = [{"action": "navigate", "url": "https://example.com"}] + trace_path.write_text( + json.dumps( + [{"index": 1, "arguments": different_arguments[0], "status": "success"}] + ), + encoding="utf-8", + ) + attempt["evidence"]["trace"]["sha256"] = _sha256(trace_path) + attempt["executed_tool_calls_sha256"] = _json_sha256(different_arguments) + attempt["replay_calls_binding_sha256"] = replay_calls_binding_sha256( + different_arguments, allowed_hosts=replay_site_hosts(different_arguments) + ) + receipt = _write(tmp_path / "receipt.json", receipt_payload) + + try: + build_verified_trajectory_dataset(seeds, [receipt], profile_path=profile) + except ValueError as error: + assert "canonical replay actions" in str(error) + else: + raise AssertionError("a different successful trace must not be promotable") + + +def test_promotion_rejects_missing_official_evaluator(tmp_path: Path) -> None: + profile, selection_sha256, repair_sha256 = _profile(tmp_path, [44]) + seeds = _write( + tmp_path / "seeds.json", + { + "schema": "lex_browser_use_replay_seeds/1", + "source": {"selection_sha256": selection_sha256}, + "expert_repairs": {"repair_spec_sha256": repair_sha256}, + "tasks": [ + { + "task_id": 44, + "replay_status": "ready", + "canonical_candidate_id": "candidate-44", + "candidates": [ + { + "id": "candidate-44", + "method": {"model": "model", "engine": "chromium"}, + "source_execution": 1, + "action_count": 1, + "tool_calls": [{"action": "snapshot"}], + "recorded_response": {"status": "SUCCESS"}, + "source": {}, + } + ], + } + ], + }, + ) + receipt_payload = _receipt( + seeds, + profile, + task_ids=[44], + attempts=[ + { + "task_id": 44, + "source_candidate_id": "candidate-44", + "engine": engine, + "process_status": "pass", + "evaluator_status": "pass", + "completed_action_count": 1, + } + for engine in ("chromium", "moli") + ], + ) + attempt = receipt_payload["attempts"][0] + evaluation_path = Path(attempt["evidence"]["evaluation"]["path"]) + evaluation = json.loads(evaluation_path.read_text(encoding="utf-8")) + evaluation["evaluators_results"] = evaluation["evaluators_results"][:1] + evaluation_path.write_text(json.dumps(evaluation), encoding="utf-8") + attempt["evidence"]["evaluation"]["sha256"] = _sha256(evaluation_path) + receipt = _write(tmp_path / "receipt.json", receipt_payload) + + try: + build_verified_trajectory_dataset(seeds, [receipt], profile_path=profile) + except ValueError as error: + assert "canonical input" in str(error) + else: + raise AssertionError("omitting an official evaluator must not be promotable") diff --git a/tests/trajectory_replay/test_webarena_plan.py b/tests/trajectory_replay/test_webarena_plan.py new file mode 100644 index 000000000..7dcead1e1 --- /dev/null +++ b/tests/trajectory_replay/test_webarena_plan.py @@ -0,0 +1,209 @@ +from __future__ import annotations + +import copy +import hashlib +import json +from pathlib import Path + +import pytest + +from harness.trajectory_replay.webarena_plan import ( + load_replay_plan, + replay_calls_binding_sha256, +) + + +def _write(path: Path, value: object) -> Path: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(json.dumps(value), encoding="utf-8") + return path + + +def _profile(tmp_path: Path) -> tuple[Path, Path]: + repo = tmp_path / "repo" + selection = _write( + repo / "evaluation/browser-use/selection.json", + {"tasks": [{"task_id": 1}, {"task_id": 2}]}, + ) + repairs = _write( + repo / "evaluation/trajectory-replay/repairs.json", + { + "schema": "repairs", + "repairs": [ + { + "task_id": task_id, + "candidate_id": f"candidate-{task_id}", + "source": {"kind": "fixture"}, + "repair_basis": {"root_cause": "fixture"}, + "tool_calls": [{"action": "snapshot"}], + "expected_response": {"status": "SUCCESS"}, + } + for task_id in (1, 2) + ], + }, + ) + profile = { + "schema": "lex_webarena_trajectory_replay_profile/1", + "task_selection": { + "path": "evaluation/browser-use/selection.json", + "sha256": hashlib.sha256(selection.read_bytes()).hexdigest(), + "task_count": 2, + }, + "seed_inventory": { + "expert_repairs_path": "evaluation/trajectory-replay/repairs.json", + "expert_repairs_sha256": hashlib.sha256(repairs.read_bytes()).hexdigest(), + "required_ready_tasks": 2, + "canonical_trajectories_per_task": 1, + }, + "methods": [ + {"id": "chromium", "engine": "chromium"}, + {"id": "moli", "engine": "moli"}, + ], + "execution": { + "environment_pool_count": 1, + "method_parallelism": 1, + "task_parallelism": 1, + "reset_before_every_task": True, + "teardown_between_methods": True, + "trusted_source_site_hosts": ["host.docker.internal"], + }, + "runs_per_task": 1, + "scoring": {"model_calls": 0}, + } + profile_path = _write( + repo / "evaluation/trajectory-replay/webarena-verified/current-run.json", profile + ) + seeds = _write( + tmp_path / "seeds.json", + { + "schema": "lex_browser_use_replay_seeds/1", + "source": {"selection_sha256": profile["task_selection"]["sha256"]}, + "expert_repairs": { + "repair_spec_sha256": profile["seed_inventory"][ + "expert_repairs_sha256" + ] + }, + "tasks": [ + { + "task_id": task_id, + "replay_status": "ready", + "canonical_candidate_id": f"candidate-{task_id}", + "candidates": [ + { + "id": f"candidate-{task_id}", + "candidate_kind": "expert_repair", + "action_count": 1, + "tool_calls": [{"action": "snapshot"}], + "recorded_response": {"status": "SUCCESS"}, + "source": { + "kind": "expert_repair_spec", + "repair_spec_sha256": profile["seed_inventory"][ + "expert_repairs_sha256" + ], + "details": {"kind": "fixture"}, + }, + "repair_basis": {"root_cause": "fixture"}, + } + ], + } + for task_id in (1, 2) + ], + }, + ) + return profile_path, seeds + + +def test_plan_binds_all_tasks_and_serial_methods(tmp_path: Path) -> None: + profile, seeds = _profile(tmp_path) + + plan = load_replay_plan(profile, seeds) + + assert plan["task_ids"] == [1, 2] + assert plan["engines"] == ["chromium", "moli"] + assert len(plan["profile_sha256"]) == len(plan["seed_manifest_sha256"]) == 64 + + +def test_plan_uses_frozen_order_not_seed_file_order(tmp_path: Path) -> None: + profile, seeds = _profile(tmp_path) + payload = json.loads(seeds.read_text(encoding="utf-8")) + payload["tasks"].reverse() + seeds.write_text(json.dumps(payload), encoding="utf-8") + + plan = load_replay_plan(profile, seeds) + + assert plan["task_ids"] == [1, 2] + + +@pytest.mark.parametrize("field", ["method_parallelism", "task_parallelism"]) +def test_plan_rejects_parallel_work_on_one_environment(tmp_path: Path, field: str) -> None: + profile, seeds = _profile(tmp_path) + payload = json.loads(profile.read_text(encoding="utf-8")) + payload["execution"][field] = 2 + profile.write_text(json.dumps(payload), encoding="utf-8") + + with pytest.raises(ValueError, match="serial methods and serial tasks"): + load_replay_plan(profile, seeds) + + +def test_plan_rejects_incomplete_seed_coverage(tmp_path: Path) -> None: + profile, seeds = _profile(tmp_path) + payload = json.loads(seeds.read_text(encoding="utf-8")) + payload["tasks"][1]["replay_status"] = "all_methods_failed" + payload["tasks"][1]["canonical_candidate_id"] = None + seeds.write_text(json.dumps(payload), encoding="utf-8") + + with pytest.raises(ValueError, match="required ready-task coverage"): + load_replay_plan(profile, seeds) + + +def test_plan_rejects_changed_task_selection(tmp_path: Path) -> None: + profile, seeds = _profile(tmp_path) + selection = profile.parents[2] / "browser-use/selection.json" + changed = copy.deepcopy(json.loads(selection.read_text(encoding="utf-8"))) + changed["tasks"].append({"task_id": 3}) + selection.write_text(json.dumps(changed), encoding="utf-8") + + with pytest.raises(ValueError, match="does not match the frozen profile"): + load_replay_plan(profile, seeds) + + +@pytest.mark.parametrize( + ("section", "field", "message"), + [ + ("source", "selection_sha256", "frozen task selection"), + ("expert_repairs", "repair_spec_sha256", "frozen expert repairs"), + ], +) +def test_plan_rejects_unbound_seed_sources( + tmp_path: Path, section: str, field: str, message: str +) -> None: + profile, seeds = _profile(tmp_path) + payload = json.loads(seeds.read_text(encoding="utf-8")) + payload[section][field] = "0" * 64 + seeds.write_text(json.dumps(payload), encoding="utf-8") + + with pytest.raises(ValueError, match=message): + load_replay_plan(profile, seeds) + + +def test_plan_rejects_expert_candidate_changed_after_review(tmp_path: Path) -> None: + profile, seeds = _profile(tmp_path) + payload = json.loads(seeds.read_text(encoding="utf-8")) + payload["tasks"][0]["candidates"][0]["tool_calls"] = [ + {"action": "navigate", "url": "https://example.com"} + ] + seeds.write_text(json.dumps(payload), encoding="utf-8") + + with pytest.raises(ValueError, match="repair spec"): + load_replay_plan(profile, seeds) + + +def test_replay_binding_does_not_normalize_an_unapproved_host() -> None: + expected = [{"action": "navigate", "url": "http://host.docker.internal:8023/path"}] + attacker = [{"action": "navigate", "url": "http://attacker.invalid:8023/path"}] + + assert replay_calls_binding_sha256( + expected, allowed_hosts={"host.docker.internal"} + ) != replay_calls_binding_sha256( + attacker, allowed_hosts={"host.docker.internal"} + ) diff --git a/tests/trajectory_replay/test_webarena_replay.py b/tests/trajectory_replay/test_webarena_replay.py index 640409261..380c20a42 100644 --- a/tests/trajectory_replay/test_webarena_replay.py +++ b/tests/trajectory_replay/test_webarena_replay.py @@ -1,6 +1,17 @@ +import ast +import inspect +import json +from pathlib import Path + +import pytest + from harness.trajectory_replay.webarena_replay import ( + RECEIPT_SCHEMA, _browser_context_needs_restart, + _origin, _rebind_url, + _validated_resume, + run_replay_suite, select_tasks, ) @@ -18,13 +29,26 @@ def test_select_tasks_requires_clean_canonical_candidates() -> None: def test_rebind_url_changes_only_known_local_site_ports() -> None: assert ( - _rebind_url("http://192.168.5.2:8023/group/project/-/issues?state=opened", "192.168.9.2") + _rebind_url( + "http://192.168.5.2:8023/group/project/-/issues?state=opened", + "192.168.9.2", + {"192.168.5.2"}, + ) == "http://192.168.9.2:8023/group/project/-/issues?state=opened" ) - assert _rebind_url("https://example.com:8023/path", "192.168.9.2") == ( - "https://192.168.9.2:8023/path" - ) - assert _rebind_url("https://example.com/path", "192.168.9.2") == ("https://example.com/path") + assert _rebind_url( + "https://example.com:8023/path", "192.168.9.2", {"example.com"} + ) == ("https://192.168.9.2:8023/path") + assert _rebind_url( + "https://attacker.invalid:8023/path", "192.168.9.2", {"example.com"} + ) == "https://attacker.invalid:8023/path" + assert _rebind_url( + "https://example.com/path", "192.168.9.2", {"example.com"} + ) == ("https://example.com/path") + + +def test_origin_removes_environment_paths_before_storage_reset() -> None: + assert _origin("http://192.168.5.2:7780/admin") == "http://192.168.5.2:7780" def test_browser_context_restart_is_limited_to_pre_action_context_failure() -> None: @@ -37,3 +61,152 @@ def test_browser_context_restart_is_limited_to_pre_action_context_failure() -> N assert not _browser_context_needs_restart({**failed_setup, "completed_action_count": 1}) assert not _browser_context_needs_restart({**failed_setup, "error": "navigation failed"}) assert not _browser_context_needs_restart({**failed_setup, "process_status": "pass"}) + + +def test_formal_orchestration_passes_trusted_hosts_only_to_replay_calls() -> None: + tree = ast.parse(inspect.getsource(run_replay_suite)) + calls = [node for node in ast.walk(tree) if isinstance(node, ast.Call)] + run_one_calls = [ + node for node in calls if isinstance(node.func, ast.Name) and node.func.id == "_run_one" + ] + journal_calls = [ + node + for node in calls + if isinstance(node.func, ast.Name) and node.func.id == "_start_physical_execution" + ] + + assert len(run_one_calls) == len(journal_calls) == 2 + assert all( + "trusted_source_site_hosts" in {keyword.arg for keyword in node.keywords} + for node in run_one_calls + ) + assert all( + "trusted_source_site_hosts" not in {keyword.arg for keyword in node.keywords} + for node in journal_calls + ) + + +def test_resume_rejects_noncanonical_or_out_of_plan_attempts(tmp_path: Path) -> None: + runtime = {"resources": {"cpus": 2.0}} + plan = { + "profile_sha256": "profile", + "seed_manifest_sha256": "seeds", + "seed_manifest": { + "tasks": [{"task_id": 1, "canonical_candidate_id": "canonical-1"}] + }, + } + receipt = { + "schema": RECEIPT_SCHEMA, + "execution_scope": "formal", + "profile_sha256": "profile", + "source_seed_manifest_sha256": "seeds", + "task_ids": [1], + "engines": ["chromium"], + "runtime": runtime, + "run_id": "run", + "attempts": [ + { + "schema": "lex_webarena_trajectory_replay_attempt/1", + "engine": "chromium", + "task_id": 1, + "source_candidate_id": "wrong", + } + ], + } + receipt_path = tmp_path / "receipt.json" + receipt_path.write_text(json.dumps(receipt), encoding="utf-8") + + with pytest.raises(ValueError, match="non-canonical"): + _validated_resume( + receipt_path, + plan=plan, + task_ids=[1], + engines=["chromium"], + scope="formal", + runtime=runtime, + ) + + +def test_resume_rejects_changed_runtime_binding(tmp_path: Path) -> None: + plan = { + "profile_sha256": "profile", + "seed_manifest_sha256": "seeds", + "seed_manifest": { + "tasks": [{"task_id": 1, "canonical_candidate_id": "canonical-1"}] + }, + } + receipt_path = tmp_path / "receipt.json" + receipt_path.write_text( + json.dumps( + { + "schema": RECEIPT_SCHEMA, + "execution_scope": "formal", + "profile_sha256": "profile", + "source_seed_manifest_sha256": "seeds", + "task_ids": [1], + "engines": ["chromium"], + "runtime": {"resources": {"cpus": 1.0}}, + "run_id": "run", + "attempts": [], + } + ), + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="runtime"): + _validated_resume( + receipt_path, + plan=plan, + task_ids=[1], + engines=["chromium"], + scope="formal", + runtime={"resources": {"cpus": 2.0}}, + ) + + +def test_resume_rejects_interrupted_physical_execution(tmp_path: Path) -> None: + runtime = {"resources": {"cpus": 2.0}} + plan = { + "profile_sha256": "profile", + "seed_manifest_sha256": "seeds", + "seed_manifest": { + "tasks": [{"task_id": 1, "canonical_candidate_id": "canonical-1"}] + }, + } + receipt_path = tmp_path / "receipt.json" + receipt_path.write_text( + json.dumps( + { + "schema": RECEIPT_SCHEMA, + "execution_scope": "formal", + "profile_sha256": "profile", + "source_seed_manifest_sha256": "seeds", + "task_ids": [1], + "engines": ["chromium"], + "runtime": runtime, + "run_id": "run", + "attempts": [], + "method_teardown": {"chromium": False}, + "sessions": [], + "physical_executions": [ + { + "engine": "chromium", + "task_id": 1, + "physical_execution": 1, + "status": "running", + } + ], + } + ), + encoding="utf-8", + ) + + with pytest.raises(ValueError, match="interrupted physical execution"): + _validated_resume( + receipt_path, + plan=plan, + task_ids=[1], + engines=["chromium"], + scope="formal", + runtime=runtime, + ) From 6a780b172c7e7275ab6bab73b29d0796452dbd58 Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 14 Sep 2026 13:46:38 +0800 Subject: [PATCH 087/127] Compile replay refs into stable selectors --- .../webarena-verified/README.md | 4 + .../trajectory_replay/browser_use_seeds.py | 151 +++++++++++++++++- harness/trajectory_replay/expert_seeds.py | 2 +- harness/trajectory_replay/webarena_dataset.py | 2 +- harness/trajectory_replay/webarena_plan.py | 10 +- .../test_browser_use_seeds.py | 110 ++++++++++++- tests/trajectory_replay/test_expert_seeds.py | 2 +- .../test_webarena_dataset.py | 14 +- tests/trajectory_replay/test_webarena_plan.py | 14 +- 9 files changed, 291 insertions(+), 18 deletions(-) diff --git a/evaluation/trajectory-replay/webarena-verified/README.md b/evaluation/trajectory-replay/webarena-verified/README.md index b72be9f92..76ad23da4 100644 --- a/evaluation/trajectory-replay/webarena-verified/README.md +++ b/evaluation/trajectory-replay/webarena-verified/README.md @@ -12,6 +12,10 @@ candidate evidence, not additional tasks. - Browser Trajectory Replay consumes `tool_calls` as fixed inputs and uses `evaluation_input` only when invoking the official evaluator. +- Autonomous Browser Use snapshots assign run-local numeric element refs. Seed generation compiles + those refs from the bound snapshot text into fixed Playwright selectors; a canonical trajectory + containing a raw ref is invalid. This also restores Playwright's navigation waiting instead of + depending on the Agent's historical thinking delay between actions. - Browser Use may use the inventory as hidden solvability and diagnostic evidence. The Agent must not receive the trajectory or evaluator input, and its score remains determined by the official task outcome contract. diff --git a/harness/trajectory_replay/browser_use_seeds.py b/harness/trajectory_replay/browser_use_seeds.py index 0aa9b7718..45bcab40b 100644 --- a/harness/trajectory_replay/browser_use_seeds.py +++ b/harness/trajectory_replay/browser_use_seeds.py @@ -4,11 +4,16 @@ import hashlib import json +import re from collections import defaultdict from pathlib import Path from typing import Any -SCHEMA = "lex_browser_use_replay_seeds/1" +SCHEMA = "lex_browser_use_replay_seeds/2" + +_REF_ACTIONS = {"click", "fill", "select"} +_NODE_RE = re.compile(r"^(?P\s*)\[(?P\d+)\]\s+(?P[a-zA-Z][\w-]*)(?P.*)$") +_ATTR_RE = re.compile(r'(?P[A-Za-z][\w-]*)="(?P(?:[^"\\]|\\.)*)"') def _sha256(path: Path) -> str: @@ -19,6 +24,132 @@ def _read_json(path: Path) -> Any: return json.loads(path.read_text(encoding="utf-8")) +def _contains_ref(value: Any) -> bool: + if isinstance(value, dict): + return "ref" in value or any(_contains_ref(item) for item in value.values()) + if isinstance(value, list): + return any(_contains_ref(item) for item in value) + return False + + +def _snapshot_nodes(result: str) -> list[dict[str, Any]]: + """Parse the stable, reader-visible element facts emitted by BrowserTool snapshots.""" + + nodes = [] + for line in result.splitlines(): + match = _NODE_RE.match(line) + if match is None: + continue + body = match.group("body") + attributes = { + item.group("name"): json.loads(f'"{item.group("value")}"') + for item in _ATTR_RE.finditer(body) + } + body_without_attributes = _ATTR_RE.sub("", body).strip() + text = body_without_attributes[1:].strip() if body_without_attributes.startswith(":") else "" + nodes.append( + { + "ref": int(match.group("ref")), + "tag": match.group("tag").lower(), + "attributes": attributes, + "text": text, + } + ) + return nodes + + +def _quoted(value: str) -> str: + return json.dumps(value, ensure_ascii=False) + + +def _base_selector(node: dict[str, Any]) -> str: + tag = str(node["tag"]) + attributes = node["attributes"] + for snapshot_name, dom_name in ( + ("name", "name"), + ("href", "href"), + ("ariaLabel", "aria-label"), + ("placeholder", "placeholder"), + ("alt", "alt"), + ("role", "role"), + ): + value = attributes.get(snapshot_name) + if value and not str(value).endswith("..."): + return f"{tag}[{dom_name}={_quoted(str(value))}]:visible" + text = str(node.get("text") or "").strip() + if text and not text.endswith("..."): + return f"{tag}:visible:has-text({_quoted(text)})" + type_value = attributes.get("type") + if type_value: + return f"{tag}[type={_quoted(str(type_value))}]:visible" + return f"{tag}:visible" + + +def _same_selector_population(target: dict[str, Any], other: dict[str, Any]) -> bool: + """Mirror the DOM population matched by the target's generated selector.""" + + if target["tag"] != other["tag"]: + return False + target_attributes = target["attributes"] + other_attributes = other["attributes"] + for name in ("name", "href", "ariaLabel", "placeholder", "alt", "role"): + value = target_attributes.get(name) + if value and not str(value).endswith("..."): + return other_attributes.get(name) == value + text = str(target.get("text") or "").strip() + if text and not text.endswith("..."): + return text in str(other.get("text") or "") + type_value = target_attributes.get("type") + if type_value: + return other_attributes.get("type") == type_value + return True + + +def _selector_for_ref(nodes: list[dict[str, Any]], ref: int, *, context: str) -> str: + target = next((node for node in nodes if node["ref"] == ref), None) + if target is None: + raise ValueError(f"{context}: ref {ref} is absent from the preceding snapshot") + base = _base_selector(target) + matches = [node for node in nodes if _same_selector_population(target, node)] + if len(matches) == 1: + return base + occurrence = next(index for index, node in enumerate(matches, start=1) if node is target) + return f":nth-match({base}, {occurrence})" + + +def compile_replay_calls(events_path: str | Path) -> list[dict[str, Any]]: + """Compile ephemeral snapshot refs into fixed Playwright selectors.""" + + calls = extract_browser_calls(events_path) + latest_nodes: list[dict[str, Any]] = [] + compiled: list[dict[str, Any]] = [] + for index, call in enumerate(calls, start=1): + arguments = dict(call.get("arguments") or {}) + action = str(arguments.get("action") or "") + if action in _REF_ACTIONS and "ref" in arguments: + if arguments.get("selector"): + arguments.pop("ref", None) + else: + arguments["selector"] = _selector_for_ref( + latest_nodes, + int(arguments.pop("ref")), + context=f"{events_path}: call {index}", + ) + elif "ref" in arguments: + # BrowserTool ignores refs for actions such as ``press``. Keeping that + # inert model-supplied field would make the fixed trajectory appear to + # depend on an ephemeral snapshot identity when it does not. + arguments.pop("ref") + if _contains_ref(arguments): + raise ValueError(f"{events_path}: call {index} contains an unsupported ref") + compiled.append(arguments) + result = str(call.get("result") or "") + snapshot_nodes = _snapshot_nodes(result) + if snapshot_nodes: + latest_nodes = snapshot_nodes + return compiled + + def extract_browser_calls(events_path: str | Path) -> list[dict[str, Any]]: """Return complete BrowserTool calls in execution order, including call status.""" @@ -46,6 +177,7 @@ def extract_browser_calls(events_path: str | Path) -> list[dict[str, Any]]: "status": str(data.get("status") or ""), "duration_seconds": data.get("execution_time"), "error": data.get("error"), + "result": data.get("result"), "ended_at": event.get("timestamp"), } ) @@ -131,7 +263,15 @@ def build_replay_seed_manifest( response_path = Path(str((row.get("paths") or {}).get("response") or "")) evaluation_path = Path(str((row.get("paths") or {}).get("evaluation") or "")) calls = extract_browser_calls(events_path) - clean = bool(calls) and all(call["status"] == "success" for call in calls) + try: + replay_calls = compile_replay_calls(events_path) + except (TypeError, ValueError): + replay_calls = [] + clean = ( + bool(calls) + and bool(replay_calls) + and all(call["status"] == "success" for call in calls) + ) candidate = { "id": f"{method_id}__task-{task_id}", "method": {"model": model, "engine": engine}, @@ -141,7 +281,7 @@ def build_replay_seed_manifest( "clean": clean, "action_count": len(calls), "actions": [str((call.get("arguments") or {}).get("action") or "") for call in calls], - "tool_calls": [dict(call.get("arguments") or {}) for call in calls], + "tool_calls": replay_calls, "recorded_response": _read_json(response_path), "source": { "events_path": str(events_path), @@ -198,7 +338,10 @@ def build_replay_seed_manifest( }, "selection_policy": { "authority": "final execution declared by each frozen Browser Use manifest", - "eligibility": "official score 1.0 and every BrowserTool call completed successfully", + "eligibility": ( + "official score 1.0, every BrowserTool call completed successfully, and " + "every ephemeral snapshot ref compiled to a fixed selector" + ), "canonical_priority": [f"{model}__{engine}" for model, engine in method_priority], "performance_used_for_selection": False, }, diff --git a/harness/trajectory_replay/expert_seeds.py b/harness/trajectory_replay/expert_seeds.py index 578e57a8a..6d124a7a8 100644 --- a/harness/trajectory_replay/expert_seeds.py +++ b/harness/trajectory_replay/expert_seeds.py @@ -8,7 +8,7 @@ from pathlib import Path from typing import Any -BASE_SCHEMA = "lex_browser_use_replay_seeds/1" +BASE_SCHEMA = "lex_browser_use_replay_seeds/2" REPAIR_SCHEMA = "lex_webarena_expert_seed_repairs/1" diff --git a/harness/trajectory_replay/webarena_dataset.py b/harness/trajectory_replay/webarena_dataset.py index 9b83c60a2..b280097b1 100644 --- a/harness/trajectory_replay/webarena_dataset.py +++ b/harness/trajectory_replay/webarena_dataset.py @@ -296,7 +296,7 @@ def build_verified_trajectory_dataset( seed_path = Path(seed_manifest_path).expanduser().resolve() seed_sha256 = _sha256(seed_path) seeds = _read_json(seed_path) - if seeds.get("schema") != "lex_browser_use_replay_seeds/1": + if seeds.get("schema") != "lex_browser_use_replay_seeds/2": raise ValueError("unsupported replay seed manifest") seed_rows = seeds.get("tasks") or [] seed_tasks = {int(item["task_id"]): item for item in seed_rows} diff --git a/harness/trajectory_replay/webarena_plan.py b/harness/trajectory_replay/webarena_plan.py index 8ca97622b..3fef9f98c 100644 --- a/harness/trajectory_replay/webarena_plan.py +++ b/harness/trajectory_replay/webarena_plan.py @@ -11,12 +11,14 @@ from harness.browser_use.runner import SITE_PORTS from harness.trajectory_replay.browser_use_seeds import ( _authoritative_rows, + _contains_ref, + compile_replay_calls, extract_browser_calls, ) from harness.trajectory_replay.runner import ENGINE_IMAGES PROFILE_SCHEMA = "lex_webarena_trajectory_replay_profile/1" -SEED_SCHEMA = "lex_browser_use_replay_seeds/1" +SEED_SCHEMA = "lex_browser_use_replay_seeds/2" def _read_json(path: Path) -> Any: @@ -138,7 +140,7 @@ def _validate_canonical_candidate( ) source_calls = extract_browser_calls(paths["events"]) if ( - [item["arguments"] for item in source_calls] != calls + compile_replay_calls(paths["events"]) != calls or any(item["status"] != "success" for item in source_calls) or _read_json(paths["response"]) != candidate["recorded_response"] ): @@ -245,6 +247,10 @@ def load_replay_plan( for candidate in item.get("candidates") or [] if candidate.get("id") == canonical_id ) + if _contains_ref(canonical["tool_calls"]): + raise ValueError( + f"task {item['task_id']} canonical trajectory contains an ephemeral ref" + ) _validate_canonical_candidate( item, canonical, diff --git a/tests/trajectory_replay/test_browser_use_seeds.py b/tests/trajectory_replay/test_browser_use_seeds.py index f8d06063f..f30888ea0 100644 --- a/tests/trajectory_replay/test_browser_use_seeds.py +++ b/tests/trajectory_replay/test_browser_use_seeds.py @@ -3,7 +3,10 @@ import json from pathlib import Path -from harness.trajectory_replay.browser_use_seeds import build_replay_seed_manifest +from harness.trajectory_replay.browser_use_seeds import ( + build_replay_seed_manifest, + compile_replay_calls, +) def _write_attempt( @@ -177,3 +180,108 @@ def test_seed_manifest_does_not_promote_superseded_success(tmp_path: Path) -> No assert manifest["counts"]["tasks_with_authoritative_success"] == 0 assert manifest["tasks"][0]["replay_status"] == "all_methods_failed" + + +def test_compile_replay_calls_replaces_snapshot_refs_with_selectors(tmp_path: Path) -> None: + events = tmp_path / "events.json" + events.write_text( + json.dumps( + [ + { + "type": "tool_execution_start", + "data": { + "tool_call_id": "navigate", + "tool_name": "browser", + "arguments": {"action": "navigate", "url": "https://example.com"}, + }, + }, + { + "type": "tool_execution_end", + "data": { + "tool_call_id": "navigate", + "status": "success", + "result": "Page: Example\nInteractive elements: 2\n---\n [1] a href=\"/one\": One\n [2] a href=\"/two\": Two", + }, + }, + { + "type": "tool_execution_start", + "data": { + "tool_call_id": "click", + "tool_name": "browser", + "arguments": {"action": "click", "ref": 2}, + }, + }, + { + "type": "tool_execution_end", + "data": {"tool_call_id": "click", "status": "success", "result": "ok"}, + }, + ] + ), + encoding="utf-8", + ) + + assert compile_replay_calls(events) == [ + {"action": "navigate", "url": "https://example.com"}, + {"action": "click", "selector": 'a[href="/two"]:visible'}, + ] + + +def test_compile_replay_calls_counts_the_full_dom_selector_population(tmp_path: Path) -> None: + events = tmp_path / "events.json" + events.write_text( + json.dumps( + [ + { + "type": "tool_execution_start", + "data": { + "tool_call_id": "snapshot", + "tool_name": "browser", + "arguments": {"action": "snapshot"}, + }, + }, + { + "type": "tool_execution_end", + "data": { + "tool_call_id": "snapshot", + "status": "success", + "result": ( + "Page: Example\nInteractive elements: 3\n---\n" + " [1] button ariaLabel=\"Menu\": Menu\n" + " [2] button\n" + " [3] a href=\"/stable\" ariaLabel=\"Stable link\": Stable" + ), + }, + }, + { + "type": "tool_execution_start", + "data": { + "tool_call_id": "button", + "tool_name": "browser", + "arguments": {"action": "click", "ref": 2}, + }, + }, + { + "type": "tool_execution_end", + "data": {"tool_call_id": "button", "status": "success"}, + }, + { + "type": "tool_execution_start", + "data": { + "tool_call_id": "link", + "tool_name": "browser", + "arguments": {"action": "click", "ref": 3}, + }, + }, + { + "type": "tool_execution_end", + "data": {"tool_call_id": "link", "status": "success"}, + }, + ] + ), + encoding="utf-8", + ) + + assert compile_replay_calls(events)[1:] == [ + {"action": "click", "selector": ":nth-match(button:visible, 2)"}, + {"action": "click", "selector": 'a[href="/stable"]:visible'}, + ] diff --git a/tests/trajectory_replay/test_expert_seeds.py b/tests/trajectory_replay/test_expert_seeds.py index 03f144d68..6846a65b2 100644 --- a/tests/trajectory_replay/test_expert_seeds.py +++ b/tests/trajectory_replay/test_expert_seeds.py @@ -15,7 +15,7 @@ def _base_manifest() -> dict: return { - "schema": "lex_browser_use_replay_seeds/1", + "schema": "lex_browser_use_replay_seeds/2", "counts": { "tasks": 3, "tasks_with_authoritative_success": 2, diff --git a/tests/trajectory_replay/test_webarena_dataset.py b/tests/trajectory_replay/test_webarena_dataset.py index abd3bc64b..e9e9a083c 100644 --- a/tests/trajectory_replay/test_webarena_dataset.py +++ b/tests/trajectory_replay/test_webarena_dataset.py @@ -309,7 +309,7 @@ def test_promotion_requires_same_candidate_to_pass_every_browser(tmp_path: Path) seeds = _write( tmp_path / "seeds.json", { - "schema": "lex_browser_use_replay_seeds/1", + "schema": "lex_browser_use_replay_seeds/2", "source": {"selection_sha256": selection_sha256}, "expert_repairs": {"repair_spec_sha256": repair_sha256}, "tasks": [ @@ -376,7 +376,7 @@ def test_promotion_rejects_incomplete_method_task_matrix(tmp_path: Path) -> None seeds = _write( tmp_path / "seeds.json", { - "schema": "lex_browser_use_replay_seeds/1", + "schema": "lex_browser_use_replay_seeds/2", "source": {"selection_sha256": selection_sha256}, "expert_repairs": {"repair_spec_sha256": repair_sha256}, "tasks": [ @@ -435,7 +435,7 @@ def test_promotion_supports_hash_bound_expert_repair(tmp_path: Path) -> None: seeds = _write( tmp_path / "seeds.json", { - "schema": "lex_browser_use_replay_seeds/1", + "schema": "lex_browser_use_replay_seeds/2", "source": {"selection_sha256": selection_sha256}, "expert_repairs": {"repair_spec_sha256": repair_sha256}, "tasks": [ @@ -519,7 +519,7 @@ def test_promotion_rejects_diagnostic_or_nonisolated_receipts(tmp_path: Path) -> seeds = _write( tmp_path / "seeds.json", { - "schema": "lex_browser_use_replay_seeds/1", + "schema": "lex_browser_use_replay_seeds/2", "source": {"selection_sha256": selection_sha256}, "expert_repairs": {"repair_spec_sha256": repair_sha256}, "tasks": [ @@ -577,7 +577,7 @@ def test_promotion_rejects_receipt_claim_when_evaluation_evidence_failed( seeds = _write( tmp_path / "seeds.json", { - "schema": "lex_browser_use_replay_seeds/1", + "schema": "lex_browser_use_replay_seeds/2", "source": {"selection_sha256": selection_sha256}, "expert_repairs": {"repair_spec_sha256": repair_sha256}, "tasks": [ @@ -640,7 +640,7 @@ def test_promotion_rejects_trace_that_differs_from_canonical_actions(tmp_path: P seeds = _write( tmp_path / "seeds.json", { - "schema": "lex_browser_use_replay_seeds/1", + "schema": "lex_browser_use_replay_seeds/2", "source": {"selection_sha256": selection_sha256}, "expert_repairs": {"repair_spec_sha256": repair_sha256}, "tasks": [ @@ -708,7 +708,7 @@ def test_promotion_rejects_missing_official_evaluator(tmp_path: Path) -> None: seeds = _write( tmp_path / "seeds.json", { - "schema": "lex_browser_use_replay_seeds/1", + "schema": "lex_browser_use_replay_seeds/2", "source": {"selection_sha256": selection_sha256}, "expert_repairs": {"repair_spec_sha256": repair_sha256}, "tasks": [ diff --git a/tests/trajectory_replay/test_webarena_plan.py b/tests/trajectory_replay/test_webarena_plan.py index 7dcead1e1..e18eb417f 100644 --- a/tests/trajectory_replay/test_webarena_plan.py +++ b/tests/trajectory_replay/test_webarena_plan.py @@ -76,7 +76,7 @@ def _profile(tmp_path: Path) -> tuple[Path, Path]: seeds = _write( tmp_path / "seeds.json", { - "schema": "lex_browser_use_replay_seeds/1", + "schema": "lex_browser_use_replay_seeds/2", "source": {"selection_sha256": profile["task_selection"]["sha256"]}, "expert_repairs": { "repair_spec_sha256": profile["seed_inventory"][ @@ -198,6 +198,18 @@ def test_plan_rejects_expert_candidate_changed_after_review(tmp_path: Path) -> N load_replay_plan(profile, seeds) +def test_plan_rejects_ephemeral_refs_in_any_canonical_trajectory(tmp_path: Path) -> None: + profile, seeds = _profile(tmp_path) + payload = json.loads(seeds.read_text(encoding="utf-8")) + payload["tasks"][0]["candidates"][0]["tool_calls"] = [ + {"action": "snapshot", "metadata": {"ref": 3}} + ] + seeds.write_text(json.dumps(payload), encoding="utf-8") + + with pytest.raises(ValueError, match="ephemeral ref"): + load_replay_plan(profile, seeds) + + def test_replay_binding_does_not_normalize_an_unapproved_host() -> None: expected = [{"action": "navigate", "url": "http://host.docker.internal:8023/path"}] attacker = [{"action": "navigate", "url": "http://attacker.invalid:8023/path"}] From e48d0b4ca36b85ec24b0bf71d74741c0d03b30df Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 14 Sep 2026 14:02:22 +0800 Subject: [PATCH 088/127] Add reference-browser replay qualification --- .../trajectory-replay/webarena-verified/README.md | 4 +++- harness/trajectory_replay/webarena_replay.py | 14 +++++++++++++- scripts/run_webarena_trajectory_replay.py | 6 ++++++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/evaluation/trajectory-replay/webarena-verified/README.md b/evaluation/trajectory-replay/webarena-verified/README.md index 76ad23da4..b7129df80 100644 --- a/evaluation/trajectory-replay/webarena-verified/README.md +++ b/evaluation/trajectory-replay/webarena-verified/README.md @@ -44,5 +44,7 @@ candidate evidence, not additional tasks. independently satisfy the same promotion gate. Run the frozen suite with `scripts/run_webarena_trajectory_replay.py`. Diagnostic subsets are -explicitly non-promotable. Promote a completed formal receipt with +explicitly non-promotable. `--diagnostic-engine chromium` runs the complete frozen task set only on +the reference browser so trajectory defects can be corrected before the measured method matrix. +Promote a completed formal receipt with `scripts/promote_webarena_trajectories.py`; never edit a verdict or verification claim by hand. diff --git a/harness/trajectory_replay/webarena_replay.py b/harness/trajectory_replay/webarena_replay.py index 1e881a506..9edbbe9fc 100644 --- a/harness/trajectory_replay/webarena_replay.py +++ b/harness/trajectory_replay/webarena_replay.py @@ -622,13 +622,25 @@ def run_replay_suite( # noqa: PLR0913 output_dir: str | Path, cowagent_repo: str | Path, diagnostic_task_ids: list[int] | None = None, + diagnostic_engines: list[str] | None = None, ) -> dict[str, Any]: plan = load_replay_plan(profile_path, seed_manifest_path) seeds = plan["seed_manifest"] task_ids = list(plan["task_ids"] if diagnostic_task_ids is None else diagnostic_task_ids) tasks = select_tasks(seeds, task_ids) engines = list(plan["engines"]) - scope = "formal" if diagnostic_task_ids is None else "diagnostic" + if diagnostic_engines is not None: + unknown_engines = sorted(set(diagnostic_engines) - set(engines)) + if unknown_engines: + raise ValueError(f"diagnostic engines are not registered: {unknown_engines}") + engines = [engine for engine in engines if engine in set(diagnostic_engines)] + if not engines: + raise ValueError("diagnostic engine selection is empty") + scope = ( + "formal" + if diagnostic_task_ids is None and diagnostic_engines is None + else "diagnostic" + ) resources = plan["profile"].get("resources") or {} cpus = float(resources["cpus"]) memory = str(resources["memory"]) diff --git a/scripts/run_webarena_trajectory_replay.py b/scripts/run_webarena_trajectory_replay.py index 2f3e97e32..b019592c9 100644 --- a/scripts/run_webarena_trajectory_replay.py +++ b/scripts/run_webarena_trajectory_replay.py @@ -21,6 +21,11 @@ def parse_args() -> argparse.Namespace: type=int, help="run an explicitly non-promotable subset instead of the frozen full suite", ) + parser.add_argument( + "--diagnostic-engine", + action="append", + help="run only a registered browser as an explicitly non-promotable qualification scan", + ) return parser.parse_args() @@ -32,6 +37,7 @@ def main() -> None: output_dir=args.output, cowagent_repo=args.cowagent_repo, diagnostic_task_ids=args.diagnostic_task, + diagnostic_engines=args.diagnostic_engine, ) From 92c3fbb0f7b9be925f2db1bf3f3d2b7cd8426ec3 Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 14 Sep 2026 15:19:44 +0800 Subject: [PATCH 089/127] Preserve snapshot-ref replay semantics --- AGENTS.md | 1 + .../webarena-verified/README.md | 8 +- harness/browser_use/har.py | 9 +- .../trajectory_replay/browser_use_seeds.py | 7 +- harness/trajectory_replay/webarena_replay.py | 83 +++++++++++- tests/browser_use/test_har.py | 35 +++++ .../test_browser_use_seeds.py | 28 +++- .../trajectory_replay/test_webarena_replay.py | 125 ++++++++++++++++++ 8 files changed, 278 insertions(+), 18 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 5857bb61c..1863aeaf6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -14,6 +14,7 @@ - Keep pass, fail and all result filters, defaulting to fail, with failure classification. Every ranking must state its task family, denominator, acceptance meaning, typical input-to-output route and local limits. Preserve every registered provider's interface examples and adoption statistics. - Maintain `research/report-changes.json` for substantive releases and `research/provider-pricing.json` for public pricing. Release notes explain reader-visible or result-visible effects without narrating assistant workflow; prices retain verification dates, commitments, billing units and unknowns. - Use `Browser Use` for the autonomous track and `Browser Trajectory Replay` (Chinese: `轨迹重放`) for the fixed BrowserTool-action track. Their implementation roots are `harness/browser_use/` and `harness/trajectory_replay/`; do not add aliases or compatibility paths using the retired names. +- Browser Trajectory Replay canonical inputs must not contain run-local snapshot refs. Compile each ref to a hash-bound stable selector while preserving the original ref action semantics; do not silently replace a DOM ref click/fill/select with BrowserTool's Playwright selector fallback because that adds different viewport, overlay and navigation-wait behavior. - Browser Use reports expose one manifest-authoritative logical result per browser and task. Dataset routes contain only frozen task definitions and acceptance contracts; result routes own verdicts, timings and trajectories. Keep one filterable result inventory instead of adding duplicate success/failure tables. - Keep automatic evaluator failure location separate from reviewed root cause. A root-cause subtype and reason must come from a per-browser, per-task review bound to the current attempts hash; render both fields blank while that result is unverified, and require complete verified coverage before a formal release when the current profile enables the gate. - Generate browser rows and comparisons from the participant registry. The same-task explorer uses one baseline browser, multi-select comparison browsers and explicit outcome relations, with browser labels linking to full results; never add one fixed table column per future browser. List Agent, model, prompt/tool contract, resources, environment and evaluator as shared controls. Standard summaries use mean, P50 and P95; compare every registered model-by-browser configuration together and mark only globally unique winners on directionally comparable metrics. For action latency, compare all configurations by per-call latency and keep unequal-workload totals and call counts neutral. diff --git a/evaluation/trajectory-replay/webarena-verified/README.md b/evaluation/trajectory-replay/webarena-verified/README.md index b7129df80..f3bdd0c80 100644 --- a/evaluation/trajectory-replay/webarena-verified/README.md +++ b/evaluation/trajectory-replay/webarena-verified/README.md @@ -13,9 +13,11 @@ candidate evidence, not additional tasks. - Browser Trajectory Replay consumes `tool_calls` as fixed inputs and uses `evaluation_input` only when invoking the official evaluator. - Autonomous Browser Use snapshots assign run-local numeric element refs. Seed generation compiles - those refs from the bound snapshot text into fixed Playwright selectors; a canonical trajectory - containing a raw ref is invalid. This also restores Playwright's navigation waiting instead of - depending on the Agent's historical thinking delay between actions. + those refs from the bound snapshot text into fixed selectors; a canonical trajectory containing + a raw ref is invalid. The compiled call records that it came from a snapshot ref and preserves + BrowserTool's original DOM click, focused keyboard fill, or change-event select semantics. It + must not silently switch to Playwright's selector fallback, whose viewport, overlay and navigation + waiting rules would change the measured trajectory. - Browser Use may use the inventory as hidden solvability and diagnostic evidence. The Agent must not receive the trajectory or evaluator input, and its score remains determined by the official task outcome contract. diff --git a/harness/browser_use/har.py b/harness/browser_use/har.py index c5b1b586d..6628a2c10 100644 --- a/harness/browser_use/har.py +++ b/harness/browser_use/har.py @@ -92,13 +92,18 @@ def stop(self, context: Any, path: Path) -> None: except Exception as exc: self.errors.append(f"page listener detach failed: {exc}") entries: list[dict[str, Any]] = [] + # CDP callbacks run on the browser thread and may append a final + # response while stop() is serializing. Use one immutable view for + # both the redirect lookup and entry loop so a late event cannot + # create a response whose key is absent from last_response_by_key. + responses = list(self._responses) last_response_by_key = { (id(session), request_id): index for index, (session, request_id, _request, _response) in enumerate( - self._responses + responses ) } - for index, (session, request_id, request, response) in enumerate(self._responses): + for index, (session, request_id, request, response) in enumerate(responses): key = (id(session), request_id) body = self._response_bodies.get(key) request = dict(request) diff --git a/harness/trajectory_replay/browser_use_seeds.py b/harness/trajectory_replay/browser_use_seeds.py index 45bcab40b..aa1fa5e98 100644 --- a/harness/trajectory_replay/browser_use_seeds.py +++ b/harness/trajectory_replay/browser_use_seeds.py @@ -46,7 +46,9 @@ def _snapshot_nodes(result: str) -> list[dict[str, Any]]: for item in _ATTR_RE.finditer(body) } body_without_attributes = _ATTR_RE.sub("", body).strip() - text = body_without_attributes[1:].strip() if body_without_attributes.startswith(":") else "" + text = ( + body_without_attributes[1:].strip() if body_without_attributes.startswith(":") else "" + ) nodes.append( { "ref": int(match.group("ref")), @@ -118,7 +120,7 @@ def _selector_for_ref(nodes: list[dict[str, Any]], ref: int, *, context: str) -> def compile_replay_calls(events_path: str | Path) -> list[dict[str, Any]]: - """Compile ephemeral snapshot refs into fixed Playwright selectors.""" + """Compile ephemeral refs into fixed selectors while preserving ref action semantics.""" calls = extract_browser_calls(events_path) latest_nodes: list[dict[str, Any]] = [] @@ -135,6 +137,7 @@ def compile_replay_calls(events_path: str | Path) -> list[dict[str, Any]]: int(arguments.pop("ref")), context=f"{events_path}: call {index}", ) + arguments["replay_mode"] = "snapshot_ref_semantics" elif "ref" in arguments: # BrowserTool ignores refs for actions such as ``press``. Keeping that # inert model-supplied field would make the fixed trajectory appear to diff --git a/harness/trajectory_replay/webarena_replay.py b/harness/trajectory_replay/webarena_replay.py index 9edbbe9fc..6bd199555 100644 --- a/harness/trajectory_replay/webarena_replay.py +++ b/harness/trajectory_replay/webarena_replay.py @@ -52,6 +52,13 @@ RECEIPT_SCHEMA = "lex_webarena_trajectory_replay_suite/1" +_CLICK_DISPATCHED_NAVIGATION_TIMEOUT_MARKERS = ( + "Click failed: Page.click: Timeout", + "performing click action", + "click action done", + "waiting for scheduled navigations to finish", +) + def _sha256(path: Path) -> str: return hashlib.sha256(path.read_bytes()).hexdigest() @@ -73,6 +80,63 @@ def _evidence_file(path: Path) -> dict[str, str] | None: return {"path": str(path), "sha256": _sha256(path)} if path.is_file() else None +def _click_was_dispatched_before_navigation_timeout( + arguments: dict[str, Any], result_status: str, result_text: str +) -> bool: + """Separate a completed click from Playwright's later navigation wait timeout. + + Replaying the click would risk applying a mutation twice. The following + trajectory step and official evaluator remain responsible for proving that + the resulting page state is usable and correct. + """ + + return ( + arguments.get("action") == "click" + and result_status != "success" + and all(marker in result_text for marker in _CLICK_DISPATCHED_NAVIGATION_TIMEOUT_MARKERS) + ) + + +def _execute_replay_call(tool: Any, arguments: dict[str, Any]) -> tuple[str, str]: + """Execute one call without changing the semantics of a compiled snapshot ref.""" + + replay_mode = arguments.get("replay_mode") + public_arguments = {key: value for key, value in arguments.items() if key != "replay_mode"} + if replay_mode != "snapshot_ref_semantics": + result = tool.execute(public_arguments) + return str(result.status), str(result.result) + + action = str(public_arguments.get("action") or "") + selector = str(public_arguments.get("selector") or "") + if action not in {"click", "fill", "select"} or not selector: + return "error", "Invalid compiled snapshot-ref replay call" + service = tool._get_service() + + try: + def execute() -> None: + page = service._page + locator = page.locator(selector).first + if action == "click": + locator.evaluate("element => element.click()") + page.wait_for_timeout(500) + elif action == "fill": + locator.evaluate("element => { element.focus(); element.value = ''; }") + page.keyboard.type(str(public_arguments.get("text", ""))) + else: + value = str(public_arguments.get("value", "")) + locator.evaluate( + "(element, value) => { if (element.tagName.toLowerCase() !== 'select') " + "throw new Error('target is not a select element'); element.value = value; " + "element.dispatchEvent(new Event('change', {bubbles: true})); }", + value, + ) + + service._submit(execute) + except Exception as exc: + return "error", f"{action.capitalize()} failed: {exc}" + return "success", f"{action.capitalize()} completed with snapshot-ref semantics." + + def _candidate(task: dict[str, Any]) -> dict[str, Any]: candidate_id = task.get("canonical_candidate_id") for item in task.get("candidates") or []: @@ -237,17 +301,26 @@ def _run_one( # noqa: PLR0913 replay_started = time.perf_counter() for index, arguments in enumerate(calls, start=1): call_started = time.perf_counter() - result = tool.execute(arguments) + result_status, result_text = _execute_replay_call(tool, arguments) + dispatched_before_timeout = _click_was_dispatched_before_navigation_timeout( + arguments, result_status, result_text + ) record = { "index": index, "arguments": arguments, - "status": result.status, + "status": "success" if dispatched_before_timeout else result_status, + "tool_status": result_status, "duration_ms": round((time.perf_counter() - call_started) * 1000, 3), - "result": result.result, + "result": result_text, } + if dispatched_before_timeout: + record["recovery"] = { + "classification": "click_dispatched_navigation_wait_timeout", + "action_retried": False, + } records.append(record) - if result.status != "success": - raise RuntimeError(f"step {index} failed: {result.result}") + if result_status != "success" and not dispatched_before_timeout: + raise RuntimeError(f"step {index} failed: {result_text}") timings["trajectory_replay_ms"] = (time.perf_counter() - replay_started) * 1000 process_status = "pass" diff --git a/tests/browser_use/test_har.py b/tests/browser_use/test_har.py index 8845ca686..e34aeadcb 100644 --- a/tests/browser_use/test_har.py +++ b/tests/browser_use/test_har.py @@ -77,6 +77,41 @@ def test_stop_is_idempotent_and_does_not_overwrite_har(tmp_path) -> None: assert path.read_text() == "sentinel" +def test_stop_serializes_one_response_snapshot_when_callbacks_append(tmp_path) -> None: + class AppendOnSecondIteration(list): + iterations = 0 + + def __iter__(self): + self.iterations += 1 + yield from super().__iter__() + if self.iterations == 2: + yield ( + object(), + "late", + {"method": "GET", "url": "http://example.test/late", "headers": {}}, + {"status": 200, "mimeType": "text/html", "headers": {}}, + ) + + session = object() + recorder = CdpHarRecorder() + recorder._responses = AppendOnSecondIteration( + [ + ( + session, + "1", + {"method": "GET", "url": "http://example.test/first", "headers": {}}, + {"status": 200, "mimeType": "text/html", "headers": {}}, + ) + ] + ) + path = tmp_path / "network.har" + + recorder.stop(object(), path) + + entries = json.loads(path.read_text())["log"]["entries"] + assert [entry["request"]["url"] for entry in entries] == ["http://example.test/first"] + + def test_stop_decodes_base64_json_for_official_har_parser(tmp_path) -> None: class Session: def send(self, _method, _params): diff --git a/tests/trajectory_replay/test_browser_use_seeds.py b/tests/trajectory_replay/test_browser_use_seeds.py index f30888ea0..0313c1576 100644 --- a/tests/trajectory_replay/test_browser_use_seeds.py +++ b/tests/trajectory_replay/test_browser_use_seeds.py @@ -200,7 +200,11 @@ def test_compile_replay_calls_replaces_snapshot_refs_with_selectors(tmp_path: Pa "data": { "tool_call_id": "navigate", "status": "success", - "result": "Page: Example\nInteractive elements: 2\n---\n [1] a href=\"/one\": One\n [2] a href=\"/two\": Two", + "result": ( + 'Page: Example\nInteractive elements: 2\n---\n' + ' [1] a href="/one": One\n' + ' [2] a href="/two": Two' + ), }, }, { @@ -222,7 +226,11 @@ def test_compile_replay_calls_replaces_snapshot_refs_with_selectors(tmp_path: Pa assert compile_replay_calls(events) == [ {"action": "navigate", "url": "https://example.com"}, - {"action": "click", "selector": 'a[href="/two"]:visible'}, + { + "action": "click", + "selector": 'a[href="/two"]:visible', + "replay_mode": "snapshot_ref_semantics", + }, ] @@ -246,9 +254,9 @@ def test_compile_replay_calls_counts_the_full_dom_selector_population(tmp_path: "status": "success", "result": ( "Page: Example\nInteractive elements: 3\n---\n" - " [1] button ariaLabel=\"Menu\": Menu\n" + ' [1] button ariaLabel="Menu": Menu\n' " [2] button\n" - " [3] a href=\"/stable\" ariaLabel=\"Stable link\": Stable" + ' [3] a href="/stable" ariaLabel="Stable link": Stable' ), }, }, @@ -282,6 +290,14 @@ def test_compile_replay_calls_counts_the_full_dom_selector_population(tmp_path: ) assert compile_replay_calls(events)[1:] == [ - {"action": "click", "selector": ":nth-match(button:visible, 2)"}, - {"action": "click", "selector": 'a[href="/stable"]:visible'}, + { + "action": "click", + "selector": ":nth-match(button:visible, 2)", + "replay_mode": "snapshot_ref_semantics", + }, + { + "action": "click", + "selector": 'a[href="/stable"]:visible', + "replay_mode": "snapshot_ref_semantics", + }, ] diff --git a/tests/trajectory_replay/test_webarena_replay.py b/tests/trajectory_replay/test_webarena_replay.py index 380c20a42..45560dee4 100644 --- a/tests/trajectory_replay/test_webarena_replay.py +++ b/tests/trajectory_replay/test_webarena_replay.py @@ -8,6 +8,8 @@ from harness.trajectory_replay.webarena_replay import ( RECEIPT_SCHEMA, _browser_context_needs_restart, + _click_was_dispatched_before_navigation_timeout, + _execute_replay_call, _origin, _rebind_url, _validated_resume, @@ -63,6 +65,129 @@ def test_browser_context_restart_is_limited_to_pre_action_context_failure() -> N assert not _browser_context_needs_restart({**failed_setup, "process_status": "pass"}) +def test_completed_click_is_not_replayed_when_only_navigation_wait_times_out() -> None: + error = """Click failed: Page.click: Timeout 5000ms exceeded. + - performing click action + - click action done + - waiting for scheduled navigations to finish""" + + assert _click_was_dispatched_before_navigation_timeout( + {"action": "click", "selector": "a[href='/next']"}, "error", error + ) + assert not _click_was_dispatched_before_navigation_timeout( + {"action": "click", "selector": "a[href='/next']"}, + "error", + "Click failed: Page.click: Timeout 5000ms exceeded; waiting for locator", + ) + assert not _click_was_dispatched_before_navigation_timeout( + {"action": "fill", "selector": "input"}, "error", error + ) + + +def test_compiled_snapshot_ref_click_preserves_dom_click_semantics() -> None: + events = [] + + class Locator: + first = None + + def __init__(self) -> None: + self.first = self + + def evaluate(self, script, *args): + events.append(("evaluate", script, args)) + + class Page: + def locator(self, selector): + events.append(("locator", selector)) + return Locator() + + def wait_for_timeout(self, timeout): + events.append(("wait", timeout)) + + class Service: + _page = Page() + + def _submit(self, callback): + return callback() + + class Tool: + def _get_service(self): + return Service() + + def execute(self, _arguments): + raise AssertionError("compiled ref call must not use Playwright actionability click") + + status, _result = _execute_replay_call( + Tool(), + { + "action": "click", + "selector": "a[href='/next']:visible", + "replay_mode": "snapshot_ref_semantics", + }, + ) + + assert status == "success" + assert events[0] == ("locator", "a[href='/next']:visible") + assert events[-1] == ("wait", 500) + + +def test_compiled_snapshot_ref_fill_and_select_preserve_dom_semantics() -> None: + events = [] + + class Locator: + first = None + + def __init__(self) -> None: + self.first = self + + def evaluate(self, script, *args): + events.append(("evaluate", script, args)) + + class Keyboard: + def type(self, text): + events.append(("type", text)) + + class Page: + keyboard = Keyboard() + + def locator(self, selector): + events.append(("locator", selector)) + return Locator() + + class Service: + _page = Page() + + def _submit(self, callback): + return callback() + + class Tool: + def _get_service(self): + return Service() + + fill_status, _ = _execute_replay_call( + Tool(), + { + "action": "fill", + "selector": "input[name='query']:visible", + "text": "needle", + "replay_mode": "snapshot_ref_semantics", + }, + ) + select_status, _ = _execute_replay_call( + Tool(), + { + "action": "select", + "selector": "select[name='status']:visible", + "value": "open", + "replay_mode": "snapshot_ref_semantics", + }, + ) + + assert fill_status == select_status == "success" + assert ("type", "needle") in events + assert any(event[0] == "evaluate" and event[2] == ("open",) for event in events) + + def test_formal_orchestration_passes_trusted_hosts_only_to_replay_calls() -> None: tree = ast.parse(inspect.getsource(run_replay_suite)) calls = [node for node in ast.walk(tree) if isinstance(node, ast.Call)] From f399e67240a33d8fe82d92c521783a7bd31098ee Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 14 Sep 2026 15:28:55 +0800 Subject: [PATCH 090/127] Bind published Browser Use report version --- evaluation/browser-use/current-report.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/evaluation/browser-use/current-report.json b/evaluation/browser-use/current-report.json index e5b708f07..4a3af2b4a 100644 --- a/evaluation/browser-use/current-report.json +++ b/evaluation/browser-use/current-report.json @@ -39,7 +39,7 @@ "base_url": "https://artifact-hub.app.lexmount.com", "site_slug": "7l77EeXPRfml", "share_url": "https://artifact-hub.app.lexmount.com/v/Ghn1BR6PCy-njsCMqCe2LRIJ-eyJEJoa", - "expected_version": "ver_e88ebb8afb86489ba2be858e39723538", + "expected_version": "ver_d889cfd55798439ea9183587e7129cf4", "state": "verified" } } From f2c25425c8abb528e1525004e7f942c257d768ef Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 14 Sep 2026 16:22:40 +0800 Subject: [PATCH 091/127] Restore full Browser Use report scope --- evaluation/browser-use/current-report.json | 2 +- harness/browser_use/report.py | 88 ++++++------------- harness/browser_use/report_i18n.py | 17 ++++ harness/trajectory_replay/webarena_replay.py | 12 ++- research/report-changes.json | 18 ++++ scripts/check_browser_use_report.py | 4 +- tests/browser_use/test_report.py | 4 +- .../trajectory_replay/test_webarena_replay.py | 15 ++++ 8 files changed, 97 insertions(+), 63 deletions(-) diff --git a/evaluation/browser-use/current-report.json b/evaluation/browser-use/current-report.json index 4a3af2b4a..bf54bb78a 100644 --- a/evaluation/browser-use/current-report.json +++ b/evaluation/browser-use/current-report.json @@ -39,7 +39,7 @@ "base_url": "https://artifact-hub.app.lexmount.com", "site_slug": "7l77EeXPRfml", "share_url": "https://artifact-hub.app.lexmount.com/v/Ghn1BR6PCy-njsCMqCe2LRIJ-eyJEJoa", - "expected_version": "ver_d889cfd55798439ea9183587e7129cf4", + "expected_version": "ver_68a8dddd4c0b4cbd86d82db4b54f7d6d", "state": "verified" } } diff --git a/harness/browser_use/report.py b/harness/browser_use/report.py index fc46c67a2..ae11b7fb0 100644 --- a/harness/browser_use/report.py +++ b/harness/browser_use/report.py @@ -2158,9 +2158,10 @@ def _article_body( eligible_attempts = [ row for row in attempts if int(row["task_id"]) not in excluded ] - task_count = total_task_count - len(excluded) - summary = _primary_summary(eligible_attempts) - paired = _paired_summary(eligible_attempts) + diagnostic_task_count = total_task_count - len(excluded) + task_count = total_task_count + summary = _primary_summary(attempts) + paired = _paired_summary(attempts) raw_failed_result_count = sum(float(row.get("score") or 0) < 1 for row in attempts) comparison_rows = [ [ @@ -2200,9 +2201,8 @@ def _article_body( ) else: cause_section = '

    本轮没有失败结果,无需生成失败根因记录。

    ' - first_final = _first_final_rows(eligible_attempts) - common_count, resource_rows = _common_success_resource_rows(eligible_attempts) - clean = summary + common_count, resource_rows = _common_success_resource_rows(attempts) + clean = _primary_summary(eligible_attempts) clean_rows = [ [ MODEL_LABELS[model], @@ -2216,51 +2216,21 @@ def _article_body( coverage = review.get("coverage") or {} reviewed_attempts = int(coverage.get("reviewed_attempts") or 0) recorded_attempts = int(coverage.get("recorded_attempts") or 0) - commit_note = "、".join( - f"{MODEL_LABELS[model]} {str(commit)[:8]}" for model, commit in cohort_commits.items() - ) - clean_task_count = task_count - first_by_model = { - model: [row for row in first_final if row[0] == label] - for model, label in MODEL_LABELS.items() - } - sol_rows = first_by_model.get("gpt-5.6-sol") or [] - astra_rows = first_by_model.get("gpt-6-astra") or [] - if sol_rows and astra_rows: - sol_chromium = next(row for row in sol_rows if row[1] == ENGINE_LABELS["chromium"]) - astra_chromium = next(row for row in astra_rows if row[1] == ENGINE_LABELS["chromium"]) - model_card_title = "首次结果与重试恢复分开统计" - model_card_body = ( - f"Sol × Chromium 首次 {sol_chromium[2]},有 {str(sol_chromium[4]).lstrip('+')} 个任务在重试中恢复,最终 {sol_chromium[5]};" - f"Astra × Chromium 首次和最终均为 {astra_chromium[5]}。" - ) - astra_explanation = ( - f"只有在 Chromium 的最终分数上,Astra 的 {astra_chromium[5]} 低于 Sol 的 {sol_chromium[5]}。" - f"首次执行时,Sol 为 {sol_chromium[2]},Astra 为 {astra_chromium[2]};差值来自 Sol 恢复的 {str(sol_chromium[4]).lstrip('+')} 个任务。" - "两批模型运行使用了不同的 browser‑eval 提交,因此这里保留为观察,不作严格模型排名。" - ) - else: - model_card_title = "首次成功与重试恢复需要分开" - model_card_body = "最终分数包含失败后的恢复结果;首次通过率单独列出。" - astra_explanation = "首次与最终分数使用不同口径,模型批次之间只作观察性比较。" - pair_details = ";".join( - f"{row[0]}:{row[4]} 题共同通过、{row[5]} 题仅 {row[1]} 通过、{row[6]} 题仅 {row[2]} 通过" - for row in comparison_rows - ) + clean_task_count = diagnostic_task_count return ( '
    Browser Use 能力评测
    ' - f"

    {task_count} 题审查合格比较集:Chromium 与 Moli 的结果、代价与失败位置

    " - f'

    预注册的 {total_task_count} 题中,{len(excluded)} 题确认受到验收合同、站点、共享状态或参考状态干扰。首页只用剩余 {task_count} 题比较四种配置;全部原始结果仍可下钻审计。

    ' + f"

    {task_count} 题 Browser Use 完整结果:Chromium 与 Moli 的成功率、代价与失败根因

    " + f'

    首页按预注册的 {total_task_count} 题展示四种配置的官方 evaluator 结果。其中 {len(excluded)} 题经复核存在验收合同、站点、共享状态或参考状态干扰;这些题仍保留在 90 题官方结果中,另外提供 {diagnostic_task_count} 题事后诊断子集。

    ' '

    WebArena‑Verified 1.2.3 · CowAgent · reasoning=low · 官方程序化 evaluator

    ' '
    ' - f'
    {_e(model_card_title)}

    {_e(model_card_body)}

    两层模型共有 {discordant} 个单边通过配对

    {_e(pair_details)}。

    {len(excluded)} 题退出正式比较

    Task 77 的并发污染已经由串行锁修复,但旧结果不会人工改分;合同或站点尚不能可靠验收的题也不进入分母。

    ' - '

    首次成功与重试恢复

    报告的主成绩按预先固定的规则取最后一次执行,失败后最多重试一次。因此首次通过率回答“第一次能否完成”,最终通过率还包含“失败后能否恢复”。

    ' - + _score_flow_figure(eligible_attempts) - + _table(["模型", "浏览器", "首次通过", "重试任务", "重试恢复", "最终通过"], first_final, table_id="home-core-metrics", classes="core-comparison-table") - + f'

    为什么看起来 Astra 更低? {_e(astra_explanation)}

    ' - + '

    两个模型层在不同日期、不同 browser‑eval 提交上运行(' - + _e(commit_note) - + ')。任务、CowAgent 提交、浏览器镜像与 evaluator 数据相同,但这仍不是严格的模型 A/B。

    ' + '

    90 题核心指标

    ' + + _table( + ["模型", "浏览器", "通过任务", "通过率", "Agent 总耗时", "BrowserTool 耗时", "Tokens"], + _summary_comparison_rows(summary), + table_id="home-core-metrics", + classes="core-comparison-table", + ) + + f'

    每个模型×浏览器配置都保留完整 {task_count} 题。以上是官方 evaluator 决定的最终逻辑结果;物理执行记录可在单题证据页下钻。

    ' '

    按同题比较浏览器

    浏览器对比必须留在同一模型层内。汇总数会隐藏互换的成败任务;下表给出同题对照。

    ' + _table(["模型", "浏览器 A", "浏览器 B", "同题数", "两边都通过", "A 通过、B 失败", "A 失败、B 通过", "两边都失败"], comparison_rows, caption=f"每行只比较同一模型下的 {task_count} 个配对任务。") + f'

    两个模型层合计有 {discordant} 个单边通过,可用于定位兼容性问题;样本量和方向都不支持总体优劣判断。查看 {task_count} 题的四配置矩阵 →

    ' @@ -2270,13 +2240,13 @@ def _article_body( + _failure_scope_figure(verified_causes) + cause_section + '

    Agent 行为

    例如漏掉商品、选错最优候选、写错字段,或在 40 步前提前结束。这些属于当前 Agent × 模型配置的真实失败。

    验收规则

    Task 699 的 evaluator 在比较实际提交前就因单元素 Alternatives 配置抛错;Task 528 要求“不提交”,验收却要求 POST。这些 0 分不能归给模型或浏览器。

    站点与环境

    Magento 搜索或库存保存服务失效,以及 Task 77 的并发状态污染,都会让正确操作无法通过。

    浏览器证据

    Moli 的部分 HAR 没有 Referer 或响应正文,例如 Task 595 和 Task 676。页面可能已经操作成功,但 evaluator 无法从网络记录确认;这是浏览器集成层的有效信号。

    ' - + f'

    正式比较与原始记录分开看

    对已证实受任务 / evaluator、站点、共享环境或参考状态干扰的 {len(excluded)} 题,按整题排除四种配置,剩余 {clean_task_count} 题。原始 evaluator 输出不改写,但不再用作首页比较分母。

    ' + + f'

    90 题官方结果与事后诊断分开看

    首页主结果始终使用完整 {total_task_count} 题官方 evaluator 分母。为了观察排除已知外部干扰后的差异,下表再按整题、四种配置一致排除 {len(excluded)} 题,得到 {clean_task_count} 题事后诊断子集。

    ' + _table(["模型", "浏览器", f"{clean_task_count} 题通过", "通过率"], clean_rows, table_id="home-qualified-results", classes="core-comparison-table") - + f'

    这是本报告的正式比较分母。被排除任务及其 {raw_failed_result_count} 个原始失败记录继续保存在结果页和核验记录中;修复并按同一协议补跑四种配置后,任务才可重新进入比较集。

    ' + + f'

    这是事后诊断子集,不是正式分数,也不替代 90 题官方结果。{raw_failed_result_count} 个原始失败记录继续保存在结果页和核验记录中。

    ' '

    审查覆盖与边界

    1证据完整性

    ' + f'{reviewed_attempts}/{recorded_attempts} 个证据包由程序核对文件存在性、哈希、JSON 可读性、回答 / 分数 / 状态一致性和运行身份。

    2官方程序验收

    每题按最终回答和必要网络副作用判分;没有 LLM judge。

    3失败根因复核

    {len(verified_causes)}/{raw_failed_result_count} 个原始最终失败结果逐条阅读错误、断言、最终回答、HAR 和 Agent 轨迹,归类并给出处理边界。

    ' + '

    审查分为完整性检查和失败根因复核。 前者确认“证据包与 evaluator 记录一致”;后者检查 evaluator 语义,并识别出 Task 699 等问题。已通过结果没有再做独立语义盲审,其判定依据仍是官方 evaluator。查看核验记录 →

    ' - f'

    全部 {clean_task_count} 题的速度与浏览器资源

    均值、P50、P95 都在四种完整配置中统一比较。CPU、内存和网络只计浏览器容器,不含 CowAgent 或模型服务;模型会改变动作数量和页面停留时间,因此这里比较整套配置,不能单独归因于浏览器内核。

    ' + f'

    完整 {task_count} 题的速度与浏览器资源

    均值、P50、P95 都在四种完整配置中统一比较。CPU、内存和网络只计浏览器容器,不含 CowAgent 或模型服务;模型会改变动作数量和页面停留时间,因此这里比较整套配置,不能单独归因于浏览器内核。

    ' + _resource_comparison_table(summary) + '

    每项只标记四种配置中数值唯一最低者。CPU 时间与网络总量累计逻辑结果使用的全部执行;内存来自 Browser Use 执行阶段的浏览器容器采样。

    ' '

    共同成功任务的性能与资源

    下表限定为首次执行共同成功的 ' @@ -2285,7 +2255,7 @@ def _article_body( + _table(["模型", "浏览器", "Agent 链路 P50", "BrowserTool P50", "Tokens P50", "峰值内存 P50", "CPU 时间 P50"], resource_rows, table_id="home-resource-table", classes="core-comparison-table") + _resource_summary_cards((common_count, resource_rows)) + '

    Agent 链路还包含模型推理和页面等待,因此不能解读为纯内核性能。

    ' - f'

    工程结论

    {clean_task_count} 题审查合格比较集显示,四种端到端配置都完成了约九成任务,浏览器之间的单边差异集中在少数任务。有效失败主要来自 Agent 行为和浏览器证据接口,不再混入已确认的合同、站点或共享状态问题。

    Moli 的内存占用明显更低,但 HAR 完整性、工具耗时和部分任务兼容性还需修复;Chromium 在两个模型层都多通过任务,但当前每种配置只有一个正式样本,不能外推成总体优势。

    下一轮验证

    Task 77 与 771 可在串行锁下补跑;Task 339、475、600、625、676、699 的合同或 evaluator 修复已提交 上游 PR #45,合并后补跑;Task 528 在具备可移植的未提交表单验收前继续排除。站点题先通过读写预检。任何补跑都覆盖同题四种配置,并作为新的完整证据批次发布。

    ' + f'

    工程结论

    完整 {task_count} 题官方结果是本报告的主口径;{clean_task_count} 题事后诊断子集只用于分离已知外部干扰,不改写成绩。

    Moli 的内存占用明显更低,但 HAR 完整性、工具耗时和部分任务兼容性还需修复;Chromium 在本轮 Sol 层多通过 3 题,Astra 层通过数相同,但当前每种配置只有一个正式样本,不能外推成总体优势。

    下一轮验证

    先修复可控的合同、evaluator、站点和共享状态问题,再按同一协议完整重跑 90 题四种配置;历史官方分数在新证据批次出现前保持不变。

    ' ) @@ -2982,7 +2952,7 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat "四种配置先在同一张表和同一任务矩阵中比较;失败结果可直接打开诊断,查看发生位置、程序报错、复核根因、轨迹、画面和日志。", ) analysis += _section( - f"四种配置总览:{len(qualified_tasks)} 题审查合格比较集", + f"四种配置总览:完整 {len(tasks)} 题官方结果", _table( [ "模型", @@ -2993,27 +2963,27 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat "BrowserTool 耗时", "Tokens", ], - _summary_comparison_rows(qualified_summary), + _summary_comparison_rows(summary_by_model), table_id="analysis-core-metrics", classes="core-comparison-table", ) - + f'

    四种完整配置统一比较,每个统计项只标记唯一最优值;相同值不标。已确认受合同、站点、共享状态或参考状态干扰的 {len(excluded_task_ids)} 题不进入本表。这里比较模型 × 浏览器整套配置,不能把差值单独归因于浏览器。

    ' + + f'

    四种完整配置统一比较,每个统计项只标记唯一最优值;相同值不标。本表保留完整 90 题官方 evaluator 结果;其中 {len(excluded_task_ids)} 题已标记外部干扰,但不人工改分或缩减分母。这里比较模型 × 浏览器整套配置,不能把差值单独归因于浏览器。

    ' + '

    审查口径:438/438 次物理执行完成程序完整性核验,75/75 个最终失败完成逐条根因复核;成功结果没有逐条语义盲审,人工复核也没有改写官方 evaluator 分数。

    ' + '
    耗时和 Tokens 如何统计

    Agent 链路累计耗时把取得逻辑结果所发生的执行相加,包含登录、模型推理、调度、页面等待、BrowserTool 与收尾,不包含此前的 Docker 站点重置。BrowserTool 累计耗时是所有 browser 工具调用的墙钟时间之和,包含工具封装、CDP 往返和页面等待。Tokens 同样累计正式结果包含的物理执行。

    ', ) analysis += _section( - f"同题全配置对比:{len(qualified_tasks)} 题", - "

    每行是一道审查合格任务,四列分别对应模型×浏览器配置;可筛出结果有差异、全部通过或全部失败的任务。

    " - + _same_task_comparison(qualified_tasks, qualified_attempts), + f"同题全配置对比:完整 {len(tasks)} 题", + "

    每行是一道预注册任务,四列分别对应模型×浏览器配置;可筛出结果有差异、全部通过或全部失败的任务。

    " + + _same_task_comparison(tasks, attempts), ).replace('
    ', '
    ', 1) analysis += _section( "BrowserTool 动作延迟", "

    每个动作按模型分层比较两个浏览器的单次调用墙钟均值,只标记层内唯一最低值。总耗时和调用数受 Agent 行为量影响,仅用于解释工作量,不参与快慢判定。

    " - + _action_duration_cards(qualified_attempts), + + _action_duration_cards(attempts), ) analysis += _section( f"{result_count} 个结果与失败诊断", - f"

    这里保留预注册 {task_count} 题的全部原始结果,包括退出正式比较的 {len(excluded_task_ids)} 题。默认显示失败;点击“查看诊断”可核对发生位置、程序报错、复核根因、轨迹、画面和日志。

    " + f"

    这里保留预注册 {task_count} 题的全部官方结果;其中 {len(excluded_task_ids)} 题已标记外部干扰。默认显示失败;点击“查看诊断”可核对发生位置、程序报错、复核根因、轨迹、画面和日志。

    " + _attempt_table(attempts, tasks, verified_causes), ) (stage / "analysis/index.html").write_text( diff --git a/harness/browser_use/report_i18n.py b/harness/browser_use/report_i18n.py index 914180fbe..63dc9d8d4 100644 --- a/harness/browser_use/report_i18n.py +++ b/harness/browser_use/report_i18n.py @@ -456,6 +456,23 @@ # blocks containing evaluator JSON and agent output. TEXT.update( { + "90 题 Browser Use 完整结果:Chromium 与 Moli 的成功率、代价与失败根因": "Complete 90-task Browser Use results: Chromium vs. Moli success, cost, and root causes", + "90 题核心指标": "Core metrics for all 90 tasks", + "每个模型×浏览器配置都保留完整 90 题。以上是官方 evaluator 决定的最终逻辑结果;物理执行记录可在单题证据页下钻。": "Every model-browser configuration retains all 90 tasks. These are the final logical outcomes from the official evaluator; physical execution records remain available on each task's evidence page.", + "首页按预注册的 90 题展示四种配置的官方 evaluator 结果。其中 14 题经复核存在验收合同、站点、共享状态或参考状态干扰;这些题仍保留在 90 题官方结果中,另外提供 76 题事后诊断子集。": "The homepage reports official evaluator outcomes for all 90 preregistered tasks across four configurations. Review found acceptance-contract, site, shared-state, or reference-state interference in 14 tasks. They remain in the official 90-task result, while a separate 76-task post-hoc diagnostic subset is also provided.", + "14 题存在外部干扰": "Fourteen tasks contain external interference", + "官方 90 题结果不改分、不缩减分母;另用 76 题事后诊断子集观察排除已知干扰后的差异。": "The official 90-task result is neither rescored nor given a smaller denominator. A separate 76-task post-hoc subset shows differences after removing known interference.", + "90 题官方结果与事后诊断分开看": "Separate the official 90-task result from post-hoc diagnosis", + "首页主结果始终使用完整 90 题官方 evaluator 分母。为了观察排除已知外部干扰后的差异,下表再按整题、四种配置一致排除 14 题,得到 76 题事后诊断子集。": "The homepage always uses all 90 tasks as the denominator for the official evaluator result. To inspect differences after known external interference, the table below removes the same 14 tasks across all four configurations and shows a 76-task post-hoc diagnostic subset.", + "这是事后诊断子集,不是正式分数,也不替代 90 题官方结果。75 个原始失败记录继续保存在结果页和核验记录中。": "This is a post-hoc diagnostic subset, not an official score and not a replacement for the official 90-task result. All 75 original failure records remain available in the result and review pages.", + "完整 90 题的速度与浏览器资源": "Latency and browser resources across all 90 tasks", + "完整 90 题官方结果是本报告的主口径;76 题事后诊断子集只用于分离已知外部干扰,不改写成绩。": "The complete official 90-task result is the report's primary view. The 76-task post-hoc diagnostic subset only separates known external interference and does not rewrite scores.", + "Moli 的内存占用明显更低,但 HAR 完整性、工具耗时和部分任务兼容性还需修复;Chromium 在本轮 Sol 层多通过 3 题,Astra 层通过数相同,但当前每种配置只有一个正式样本,不能外推成总体优势。": "Moli uses substantially less memory, while HAR completeness, tool latency, and some task compatibility still require fixes. Chromium passes three more tasks in the Sol cohort and ties Moli in the Astra cohort, but one formal sample per configuration cannot establish a general advantage.", + "先修复可控的合同、evaluator、站点和共享状态问题,再按同一协议完整重跑 90 题四种配置;历史官方分数在新证据批次出现前保持不变。": "Fix controllable contract, evaluator, site, and shared-state defects first, then rerun all four configurations across the same 90-task protocol. Historical official scores remain unchanged until a new evidence cohort exists.", + "四种配置总览:完整 90 题官方结果": "Four-configuration overview: complete official results for 90 tasks", + "同题全配置对比:完整 90 题": "Same-task comparison across all configurations: complete 90-task set", + "每行是一道预注册任务,四列分别对应模型×浏览器配置;可筛出结果有差异、全部通过或全部失败的任务。": "Each row is one preregistered task, with one column per model-browser configuration. Filters isolate divergent, all-pass, or all-fail outcomes.", + "这里保留预注册 90 题的全部官方结果;其中 14 题已标记外部干扰。默认显示失败;点击“查看诊断”可核对发生位置、程序报错、复核根因、轨迹、画面和日志。": "This inventory retains official results for all 90 preregistered tasks, with 14 tasks marked for external interference. Failures are shown by default; open a diagnosis to inspect the failure point, program error, reviewed cause, trajectory, frames, and logs.", # Current home page and dataset-only inventory. "Chromium 任务通过": "Chromium tasks passed", "Moli 任务通过": "Moli tasks passed", diff --git a/harness/trajectory_replay/webarena_replay.py b/harness/trajectory_replay/webarena_replay.py index 6bd199555..e7e67e839 100644 --- a/harness/trajectory_replay/webarena_replay.py +++ b/harness/trajectory_replay/webarena_replay.py @@ -157,6 +157,16 @@ def select_tasks(seed_manifest: dict[str, Any], task_ids: list[int]) -> list[dic return selected +def select_methods(plan: dict[str, Any], engines: list[str]) -> list[dict[str, Any]]: + """Return only methods registered for this formal or diagnostic engine scope.""" + + selected = [method for method in plan["methods"] if str(method["engine"]) in engines] + selected_engines = [str(method["engine"]) for method in selected] + if selected_engines != engines: + raise ValueError("replay methods do not match the selected engine scope") + return selected + + def _rebind_url( value: str, site_access_host: str, trusted_source_site_hosts: set[str] ) -> str: @@ -793,7 +803,7 @@ def run_replay_suite( # noqa: PLR0913 receipt["method_teardown"] = method_teardown receipt["isolation"]["lock_wait_ms"] = round(lock_wait_ms, 3) _write_receipt(receipt_path, receipt) - for method in plan["methods"]: + for method in select_methods(plan, engines): engine = str(method["engine"]) pending = [task for task in tasks if (engine, int(task["task_id"])) not in completed] if not pending: diff --git a/research/report-changes.json b/research/report-changes.json index cb45558c5..cd2e980a6 100644 --- a/research/report-changes.json +++ b/research/report-changes.json @@ -5,6 +5,24 @@ "en": "This history begins with revisions supported by a complete audit trail. Earlier Artifact Hub versions remain available, but no itemized history is reconstructed where the supporting record is incomplete." }, "browser_use_reviews": [ + { + "id": "review-2026-09-14-complete-90-task-primary-result", + "date": "2026-09-14", + "title": "恢复完整 90 题官方结果主口径", + "status": "已处理", + "scope": "Browser Use 首页、核心指标表、同题矩阵、性能统计和结果分析页。", + "findings": "上一版误把排除 14 个已知外部干扰任务后的 76 题事后诊断子集写成正式主结果,造成报告看起来只有 76 题;首页还重复突出首次执行与重试恢复,掩盖了每题一个最终逻辑结果的正式口径。", + "corrections": "首页和结果分析恢复完整 90 题官方 evaluator 结果;四种配置的核心指标、同题矩阵和性能统计均使用 90 题。76 题仅保留为明确标注的事后诊断子集,不改分、不替代官方结果。首页删除首次/重试拆分模块,物理执行记录只在单题证据页下钻。", + "remaining": "14 个外部干扰任务的历史分数保持不变;只有修复后按冻结协议产生新的完整 90 题证据批次,才能更新官方结果。", + "i18n": {"en": { + "title": "Restore the complete 90-task official result as the primary view", + "status": "Resolved", + "scope": "The Browser Use homepage, core metrics, same-task matrix, performance statistics, and result analysis.", + "findings": "The prior version incorrectly presented the 76-task post-hoc diagnostic subset, which excludes 14 known interference tasks, as the formal primary result. It therefore appeared to contain only 76 tasks. The homepage also overemphasized first attempts and retry recovery instead of one final logical result per task.", + "corrections": "Restored the complete official 90-task evaluator result on the homepage and analysis page. Core metrics, the same-task matrix, and performance statistics now use all 90 tasks across all four configurations. The 76-task view remains only as an explicitly labeled post-hoc diagnostic subset and does not rescore or replace the official result. Removed the first-attempt/retry split from the homepage; physical executions remain available on task evidence pages.", + "remaining": "Historical scores for the 14 interference tasks remain unchanged. Only a new complete 90-task evidence cohort produced after corrections under the frozen protocol can update the official result." + }} + }, { "id": "review-2026-09-11-interpretation-reader-audit", "date": "2026-09-11", diff --git a/scripts/check_browser_use_report.py b/scripts/check_browser_use_report.py index bb7745c21..fc411c11a 100644 --- a/scripts/check_browser_use_report.py +++ b/scripts/check_browser_use_report.py @@ -531,7 +531,7 @@ def check( guide_index = rendered_pages[report_dir / "index.html"] for marker in ( - "首次成功与重试恢复", + "90 题核心指标", "程序判分落点与复核根因", 'class="verdict-strip"', 'class="cause-bars"', @@ -542,6 +542,8 @@ def check( ): if marker not in guide_index: raise ValueError(f"interpretation is missing an evidence-backed reading aid: {marker}") + if "首次成功与重试恢复" in guide_index: + raise ValueError("homepage must present one final logical result, not retry narration") for forbidden in ( "中位数", "失败结果定位", diff --git a/tests/browser_use/test_report.py b/tests/browser_use/test_report.py index 63620f370..f5b30579c 100644 --- a/tests/browser_use/test_report.py +++ b/tests/browser_use/test_report.py @@ -812,7 +812,9 @@ def test_report_renders_measured_runtime_identities(tmp_path: Path, monkeypatch) assert report.is_file() assert (output / "assets/webarena-autonomous-evaluation-flow.png").is_file() home_text = report.read_text(encoding="utf-8") - assert "审查合格比较集" in home_text + assert "90 题 Browser Use 完整结果" in home_text + assert "90 题核心指标" in home_text + assert "首次成功与重试恢复" not in home_text methodology_text = (output / "methodology/index.html").read_text(encoding="utf-8") assert "webarena-autonomous-evaluation-flow.png" in methodology_text assert "ContainerManager.start(reset=True)" in methodology_text diff --git a/tests/trajectory_replay/test_webarena_replay.py b/tests/trajectory_replay/test_webarena_replay.py index 45560dee4..527d826f9 100644 --- a/tests/trajectory_replay/test_webarena_replay.py +++ b/tests/trajectory_replay/test_webarena_replay.py @@ -14,6 +14,7 @@ _rebind_url, _validated_resume, run_replay_suite, + select_methods, select_tasks, ) @@ -29,6 +30,20 @@ def test_select_tasks_requires_clean_canonical_candidates() -> None: assert [task["task_id"] for task in select_tasks(manifest, [1])] == [1] +def test_select_methods_limits_diagnostic_run_to_requested_engine() -> None: + plan = {"methods": [{"engine": "chromium"}, {"engine": "moli"}]} + + assert select_methods(plan, ["chromium"]) == [{"engine": "chromium"}] + assert select_methods(plan, ["chromium", "moli"]) == plan["methods"] + + +def test_select_methods_rejects_profile_order_mismatch() -> None: + plan = {"methods": [{"engine": "moli"}, {"engine": "chromium"}]} + + with pytest.raises(ValueError, match="selected engine scope"): + select_methods(plan, ["chromium", "moli"]) + + def test_rebind_url_changes_only_known_local_site_ports() -> None: assert ( _rebind_url( From 7af6b68b454dbf58d88f7acc9ccb7232c0cb759f Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 14 Sep 2026 16:50:08 +0800 Subject: [PATCH 092/127] Clear persisted filters in task 676 replay --- .../webarena-verified/current-run.json | 2 +- .../webarena-verified/expert-repairs.json | 2 +- tests/trajectory_replay/test_expert_seeds.py | 16 ++++++++++++++++ 3 files changed, 18 insertions(+), 2 deletions(-) diff --git a/evaluation/trajectory-replay/webarena-verified/current-run.json b/evaluation/trajectory-replay/webarena-verified/current-run.json index ab82ff05e..6a311a098 100644 --- a/evaluation/trajectory-replay/webarena-verified/current-run.json +++ b/evaluation/trajectory-replay/webarena-verified/current-run.json @@ -10,7 +10,7 @@ "seed_inventory": { "base_kind": "successful Browser Use executions", "expert_repairs_path": "evaluation/trajectory-replay/webarena-verified/expert-repairs.json", - "expert_repairs_sha256": "3f8ef291ad3ed92ecef3469763a93a1aa1a53967116202c73f09a9816ea39832", + "expert_repairs_sha256": "1ec3c8bb285e41ccd9eb7f8bc970dae78a186f177ea6d3d7d94acfedde925157", "required_ready_tasks": 90, "canonical_trajectories_per_task": 1 }, diff --git a/evaluation/trajectory-replay/webarena-verified/expert-repairs.json b/evaluation/trajectory-replay/webarena-verified/expert-repairs.json index 5fe6a56f1..6d25b4896 100644 --- a/evaluation/trajectory-replay/webarena-verified/expert-repairs.json +++ b/evaluation/trajectory-replay/webarena-verified/expert-repairs.json @@ -556,7 +556,7 @@ }, { "action": "evaluate", - "script": "const select=document.querySelector('select[name=\"status\"]'); if(!select) throw new Error('status filter not found'); select.value='fraud'; select.dispatchEvent(new Event('input',{bubbles:true})); select.dispatchEvent(new Event('change',{bubbles:true})); const button=document.querySelector('button[data-action=\"grid-filter-apply\"]'); if(!button) throw new Error('apply filters button not found'); button.click(); return true;" + "script": "for(const name of ['created_at[from]','created_at[to]']){const input=document.querySelector(`input[name=\"${name}\"]`); if(input){input.value=''; input.dispatchEvent(new Event('input',{bubbles:true})); input.dispatchEvent(new Event('change',{bubbles:true}));}} const select=document.querySelector('select[name=\"status\"]'); if(!select) throw new Error('status filter not found'); select.value='fraud'; select.dispatchEvent(new Event('input',{bubbles:true})); select.dispatchEvent(new Event('change',{bubbles:true})); const button=document.querySelector('button[data-action=\"grid-filter-apply\"]'); if(!button) throw new Error('apply filters button not found'); button.click(); return true;" }, { "action": "wait", diff --git a/tests/trajectory_replay/test_expert_seeds.py b/tests/trajectory_replay/test_expert_seeds.py index 6846a65b2..be2b7c778 100644 --- a/tests/trajectory_replay/test_expert_seeds.py +++ b/tests/trajectory_replay/test_expert_seeds.py @@ -137,3 +137,19 @@ def test_build_binds_exact_input_files(tmp_path: Path) -> None: assert len(manifest["expert_repairs"]["base_manifest_sha256"]) == 64 assert len(manifest["expert_repairs"]["repair_spec_sha256"]) == 64 assert manifest["expert_repairs"]["candidate_count"] == 1 + + +def test_task_676_repair_clears_unrelated_persisted_date_filters() -> None: + repairs = json.loads( + Path("evaluation/trajectory-replay/webarena-verified/expert-repairs.json").read_text( + encoding="utf-8" + ) + ) + repair = next(item for item in repairs["repairs"] if item["task_id"] == 676) + scripts = "\n".join( + call.get("script", "") for call in repair["tool_calls"] if call["action"] == "evaluate" + ) + + assert "created_at[from]" in scripts + assert "created_at[to]" in scripts + assert "value=''" in scripts From dcf686824d6e73e5d3ffdd1787e6d69dad8e29af Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 14 Sep 2026 16:58:11 +0800 Subject: [PATCH 093/127] Separate trajectory qualification from comparison --- .../webarena-verified/README.md | 20 +++-- .../webarena-verified/current-run.json | 6 +- harness/trajectory_replay/webarena_dataset.py | 40 ++------- harness/trajectory_replay/webarena_plan.py | 8 ++ harness/trajectory_replay/webarena_replay.py | 60 ++++++++++++-- scripts/promote_webarena_trajectories.py | 2 - scripts/run_webarena_trajectory_replay.py | 12 ++- .../test_webarena_dataset.py | 82 ++++++++----------- tests/trajectory_replay/test_webarena_plan.py | 1 + .../trajectory_replay/test_webarena_replay.py | 53 ++++++++++++ 10 files changed, 187 insertions(+), 97 deletions(-) diff --git a/evaluation/trajectory-replay/webarena-verified/README.md b/evaluation/trajectory-replay/webarena-verified/README.md index f3bdd0c80..0e4e086b2 100644 --- a/evaluation/trajectory-replay/webarena-verified/README.md +++ b/evaluation/trajectory-replay/webarena-verified/README.md @@ -24,10 +24,12 @@ candidate evidence, not additional tasks. - A Browser Use success is only a candidate. A reviewed expert repair may also provide a candidate when every autonomous run failed or a successful trace contains failed actions. Expert candidates retain their repair basis and repair-spec hash and may not contain ephemeral accessibility refs. -- Promotion requires the same candidate to replay without a model call and pass both execution and - evaluation on every required browser. Where `task-corrections.json` records a proven upstream - contract defect, both browser proofs must bind the same correction ID, catalog hash and corrected - task hash; historical Browser Use scores remain unchanged. +- Before comparison, promotion requires each canonical candidate to replay without a model call and + pass both execution and evaluation on the frozen reference browser. The measured Chromium and + Moli runs consume that already-qualified trajectory set; their failures remain benchmark outcomes + and never remove a task from the comparison. Where `task-corrections.json` records a proven + upstream contract defect, the qualification proof binds its correction ID, catalog hash and + corrected task hash; historical Browser Use scores remain unchanged. - Full HAR, screenshots, traces, resource samples and replay receipts remain outside Git under `BROWSER_EVAL_DATA_HOME`; this directory stores deterministic actions and provenance hashes. - The current self-hosted WebArena deployment has one fixed-port site stack, so a formal suite owns @@ -46,7 +48,9 @@ candidate evidence, not additional tasks. independently satisfy the same promotion gate. Run the frozen suite with `scripts/run_webarena_trajectory_replay.py`. Diagnostic subsets are -explicitly non-promotable. `--diagnostic-engine chromium` runs the complete frozen task set only on -the reference browser so trajectory defects can be corrected before the measured method matrix. -Promote a completed formal receipt with -`scripts/promote_webarena_trajectories.py`; never edit a verdict or verification claim by hand. +explicitly non-promotable. `--qualification-engine chromium` runs the complete frozen task set on +the reference browser and is the only scope that may promote the standard trajectory dataset. +After all 90 qualify, promote that receipt with `scripts/promote_webarena_trajectories.py`, then run +the measured method matrix with `--qualified-dataset` and no scope option. The runner rejects formal +execution when that dataset is missing, incomplete, or differs from the frozen profile, seeds, or +canonical actions. Never edit a verdict or verification claim by hand. diff --git a/evaluation/trajectory-replay/webarena-verified/current-run.json b/evaluation/trajectory-replay/webarena-verified/current-run.json index 6a311a098..67304326a 100644 --- a/evaluation/trajectory-replay/webarena-verified/current-run.json +++ b/evaluation/trajectory-replay/webarena-verified/current-run.json @@ -26,6 +26,10 @@ "label": "Moli 1.1.3" } ], + "qualification": { + "reference_engine": "chromium", + "required_task_count": 90 + }, "runs_per_task": 1, "resources": { "cpus": 2.0, @@ -47,7 +51,7 @@ }, "scoring": { "authority": "official programmatic evaluator or a hash-bound replay correction", - "promotion_gate": "the same canonical trajectory passes execution and evaluation on every registered method", + "promotion_gate": "the canonical trajectory passes execution and official evaluation on the frozen reference browser before any measured browser comparison", "model_calls": 0 } } diff --git a/harness/trajectory_replay/webarena_dataset.py b/harness/trajectory_replay/webarena_dataset.py index b280097b1..2a4bb9719 100644 --- a/harness/trajectory_replay/webarena_dataset.py +++ b/harness/trajectory_replay/webarena_dataset.py @@ -2,7 +2,6 @@ from __future__ import annotations -import copy import hashlib import json from collections import defaultdict @@ -37,7 +36,7 @@ def _official_evaluator_names() -> dict[int, tuple[str, ...]]: } -def _validate_formal_receipt( +def _validate_qualification_receipt( receipt: dict[str, Any], *, receipt_path: Path, @@ -49,8 +48,8 @@ def _validate_formal_receipt( ) -> None: if receipt.get("schema") != RECEIPT_SCHEMA: raise ValueError(f"unsupported replay receipt: {receipt_path}") - if receipt.get("execution_scope") != "formal" or receipt.get("status") != "complete": - raise ValueError(f"replay receipt is not a completed formal suite: {receipt_path}") + if receipt.get("execution_scope") != "qualification" or receipt.get("status") != "complete": + raise ValueError(f"replay receipt is not a completed qualification suite: {receipt_path}") if receipt.get("source_seed_manifest_sha256") != seed_sha256: raise ValueError(f"replay receipt is not bound to the selected seeds: {receipt_path}") if receipt.get("profile_sha256") != profile_sha256: @@ -289,9 +288,8 @@ def build_verified_trajectory_dataset( replay_receipt_paths: list[str | Path], *, profile_path: str | Path, - prior_dataset_path: str | Path | None = None, ) -> dict[str, Any]: - """Return compact trajectories that passed process and official evaluation gates.""" + """Return trajectories qualified before the measured browser comparison.""" seed_path = Path(seed_manifest_path).expanduser().resolve() seed_sha256 = _sha256(seed_path) @@ -305,7 +303,8 @@ def build_verified_trajectory_dataset( seed_task_ids = set(seed_tasks) plan = load_replay_plan(profile_path, seed_path) profile = plan["profile"] - required_engines = tuple(plan["engines"]) + qualification_engine = str(plan["qualification_engine"]) + required_engines = (qualification_engine,) profile_sha256 = plan["profile_sha256"] attempts_by_key: dict[tuple[int, str], list[dict[str, Any]]] = defaultdict(list) @@ -314,7 +313,7 @@ def build_verified_trajectory_dataset( for raw_path in replay_receipt_paths: receipt_path = Path(raw_path).expanduser().resolve() receipt = _read_json(receipt_path) - _validate_formal_receipt( + _validate_qualification_receipt( receipt, receipt_path=receipt_path, seed_sha256=seed_sha256, @@ -434,27 +433,6 @@ def build_verified_trajectory_dataset( } ) - prior_dataset_sha256 = None - if prior_dataset_path is not None: - prior_path = Path(prior_dataset_path).expanduser().resolve() - prior = _read_json(prior_path) - if prior.get("schema") != SCHEMA: - raise ValueError("unsupported prior trajectory dataset") - if ( - prior.get("official_data_checksum") - and seeds["source"].get("official_data_checksum") - and prior["official_data_checksum"] != seeds["source"]["official_data_checksum"] - ): - raise ValueError("prior trajectory dataset uses a different official dataset") - new_task_ids = {item["task_id"] for item in trajectories} - prior_trajectories = copy.deepcopy(prior.get("trajectories") or []) - overlap = sorted(new_task_ids & {item["task_id"] for item in prior_trajectories}) - if overlap: - raise ValueError(f"prior trajectory dataset overlaps new proofs: {overlap}") - trajectories.extend(prior_trajectories) - trajectories.sort(key=lambda item: int(item["task_id"])) - prior_dataset_sha256 = _sha256(prior_path) - return { "schema": SCHEMA, "benchmark": seeds["source"].get("benchmark") or "WebArena-Verified", @@ -470,7 +448,8 @@ def build_verified_trajectory_dataset( "evaluation_input_role": "official evaluator input only; never an Agent prompt", }, "promotion_policy": { - "required_browsers": list(required_engines), + "qualification_browser": qualification_engine, + "measured_browsers_are_not_promotion_gates": True, "process_status": "pass", "evaluator_status": "pass", "model_calls": 0, @@ -479,7 +458,6 @@ def build_verified_trajectory_dataset( "seed_manifest_sha256": seed_sha256, "profile_sha256": profile_sha256, "replay_receipts": receipts, - "prior_dataset_sha256": prior_dataset_sha256, }, "counts": { "verified_tasks": len({item["task_id"] for item in trajectories}), diff --git a/harness/trajectory_replay/webarena_plan.py b/harness/trajectory_replay/webarena_plan.py index 3fef9f98c..7e7dea906 100644 --- a/harness/trajectory_replay/webarena_plan.py +++ b/harness/trajectory_replay/webarena_plan.py @@ -276,6 +276,13 @@ def load_replay_plan( ): raise ValueError("trajectory replay methods must be unique registered engines") + qualification = profile.get("qualification") or {} + qualification_engine = str(qualification.get("reference_engine") or "") + if qualification_engine not in engines: + raise ValueError("trajectory qualification requires one registered reference engine") + if int(qualification.get("required_task_count") or 0) != len(task_ids): + raise ValueError("trajectory qualification must cover the frozen task set") + execution = profile.get("execution") or {} if execution.get("environment_pool_count") != 1: raise ValueError("the fixed-port WebArena stack supports exactly one environment pool") @@ -321,5 +328,6 @@ def load_replay_plan( "task_ids": task_ids, "methods": methods, "engines": engines, + "qualification_engine": qualification_engine, "trusted_source_site_hosts": trusted_source_site_hosts, } diff --git a/harness/trajectory_replay/webarena_replay.py b/harness/trajectory_replay/webarena_replay.py index e7e67e839..a2ec33609 100644 --- a/harness/trajectory_replay/webarena_replay.py +++ b/harness/trajectory_replay/webarena_replay.py @@ -167,6 +167,34 @@ def select_methods(plan: dict[str, Any], engines: list[str]) -> list[dict[str, A return selected +def validate_qualification_dataset(plan: dict[str, Any], path: str | Path) -> str: + """Bind a formal comparison to the complete pre-qualified trajectory set.""" + + dataset_path = Path(path).expanduser().resolve() + dataset = json.loads(dataset_path.read_text(encoding="utf-8")) + if dataset.get("schema") != "lex_webarena_verified_trajectory_dataset/1": + raise ValueError("formal replay requires the current qualified trajectory dataset") + provenance = dataset.get("provenance") or {} + if provenance.get("seed_manifest_sha256") != plan["seed_manifest_sha256"]: + raise ValueError("qualified trajectories differ from the frozen replay seeds") + if provenance.get("profile_sha256") != plan["profile_sha256"]: + raise ValueError("qualified trajectories differ from the frozen replay profile") + policy = dataset.get("promotion_policy") or {} + if policy.get("qualification_browser") != plan["qualification_engine"]: + raise ValueError("qualified trajectories use a different reference browser") + trajectories = dataset.get("trajectories") or [] + by_task = {int(item["task_id"]): item for item in trajectories} + if len(by_task) != len(trajectories) or set(by_task) != set(plan["task_ids"]): + raise ValueError("qualified trajectories do not cover the frozen task set") + seed_tasks = { + int(item["task_id"]): item for item in plan["seed_manifest"].get("tasks") or [] + } + for task_id, trajectory in by_task.items(): + if trajectory.get("tool_calls") != _candidate(seed_tasks[task_id]).get("tool_calls"): + raise ValueError(f"qualified trajectory differs from task {task_id} canonical input") + return _sha256(dataset_path) + + def _rebind_url( value: str, site_access_host: str, trusted_source_site_hosts: set[str] ) -> str: @@ -598,6 +626,7 @@ def _validated_resume( engines: list[str], scope: str, runtime: dict[str, Any], + qualification_dataset_sha256: str | None = None, ) -> tuple[ str, list[dict[str, Any]], @@ -613,6 +642,7 @@ def _validated_resume( "execution_scope": scope, "profile_sha256": plan["profile_sha256"], "source_seed_manifest_sha256": plan["seed_manifest_sha256"], + "qualification_dataset_sha256": qualification_dataset_sha256, "task_ids": task_ids, "engines": engines, "runtime": runtime, @@ -706,24 +736,40 @@ def run_replay_suite( # noqa: PLR0913 cowagent_repo: str | Path, diagnostic_task_ids: list[int] | None = None, diagnostic_engines: list[str] | None = None, + qualification_engine: str | None = None, + qualified_dataset_path: str | Path | None = None, ) -> dict[str, Any]: plan = load_replay_plan(profile_path, seed_manifest_path) seeds = plan["seed_manifest"] task_ids = list(plan["task_ids"] if diagnostic_task_ids is None else diagnostic_task_ids) tasks = select_tasks(seeds, task_ids) engines = list(plan["engines"]) - if diagnostic_engines is not None: + if qualification_engine is not None: + if diagnostic_task_ids is not None or diagnostic_engines is not None: + raise ValueError("qualification and diagnostic scopes cannot be combined") + if qualification_engine != plan["qualification_engine"]: + raise ValueError("qualification engine differs from the frozen reference engine") + engines = [qualification_engine] + scope = "qualification" + elif diagnostic_engines is not None: unknown_engines = sorted(set(diagnostic_engines) - set(engines)) if unknown_engines: raise ValueError(f"diagnostic engines are not registered: {unknown_engines}") engines = [engine for engine in engines if engine in set(diagnostic_engines)] if not engines: raise ValueError("diagnostic engine selection is empty") - scope = ( - "formal" - if diagnostic_task_ids is None and diagnostic_engines is None - else "diagnostic" - ) + scope = "diagnostic" + else: + scope = "formal" if diagnostic_task_ids is None else "diagnostic" + qualification_dataset_sha256 = None + if scope == "formal": + if qualified_dataset_path is None: + raise ValueError("formal replay requires a complete qualified trajectory dataset") + qualification_dataset_sha256 = validate_qualification_dataset( + plan, qualified_dataset_path + ) + elif qualified_dataset_path is not None: + raise ValueError("a qualified trajectory dataset is only accepted for formal replay") resources = plan["profile"].get("resources") or {} cpus = float(resources["cpus"]) memory = str(resources["memory"]) @@ -770,6 +816,7 @@ def run_replay_suite( # noqa: PLR0913 engines=engines, scope=scope, runtime=runtime, + qualification_dataset_sha256=qualification_dataset_sha256, ) completed = {(str(item["engine"]), int(item["task_id"])) for item in attempts} lock_path = _physical_execution_lock_path() @@ -782,6 +829,7 @@ def run_replay_suite( # noqa: PLR0913 "profile_sha256": plan["profile_sha256"], "source_seed_manifest": plan["seed_manifest_path"], "source_seed_manifest_sha256": plan["seed_manifest_sha256"], + "qualification_dataset_sha256": qualification_dataset_sha256, "task_ids": task_ids, "engines": engines, "runtime": runtime, diff --git a/scripts/promote_webarena_trajectories.py b/scripts/promote_webarena_trajectories.py index 948d39330..396ec1ecf 100644 --- a/scripts/promote_webarena_trajectories.py +++ b/scripts/promote_webarena_trajectories.py @@ -16,7 +16,6 @@ def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser() parser.add_argument("--seeds", required=True) parser.add_argument("--receipt", action="append", required=True) - parser.add_argument("--prior-dataset") parser.add_argument("--output", required=True) return parser.parse_args() @@ -27,7 +26,6 @@ def main() -> None: args.seeds, args.receipt, profile_path=CURRENT_PROFILE, - prior_dataset_path=args.prior_dataset, ) output = Path(args.output) output.parent.mkdir(parents=True, exist_ok=True) diff --git a/scripts/run_webarena_trajectory_replay.py b/scripts/run_webarena_trajectory_replay.py index b019592c9..1dccf98be 100644 --- a/scripts/run_webarena_trajectory_replay.py +++ b/scripts/run_webarena_trajectory_replay.py @@ -24,7 +24,15 @@ def parse_args() -> argparse.Namespace: parser.add_argument( "--diagnostic-engine", action="append", - help="run only a registered browser as an explicitly non-promotable qualification scan", + help="run only selected registered browsers as an explicitly non-promotable diagnostic", + ) + parser.add_argument( + "--qualification-engine", + help="qualify the complete frozen trajectory set on the registered reference browser", + ) + parser.add_argument( + "--qualified-dataset", + help="complete qualified trajectory dataset required by a formal comparison", ) return parser.parse_args() @@ -38,6 +46,8 @@ def main() -> None: cowagent_repo=args.cowagent_repo, diagnostic_task_ids=args.diagnostic_task, diagnostic_engines=args.diagnostic_engine, + qualification_engine=args.qualification_engine, + qualified_dataset_path=args.qualified_dataset, ) diff --git a/tests/trajectory_replay/test_webarena_dataset.py b/tests/trajectory_replay/test_webarena_dataset.py index e9e9a083c..4f92693cd 100644 --- a/tests/trajectory_replay/test_webarena_dataset.py +++ b/tests/trajectory_replay/test_webarena_dataset.py @@ -163,6 +163,10 @@ def _profile(tmp_path: Path, task_ids: list[int]) -> tuple[Path, str, str]: {"id": "chromium", "engine": "chromium"}, {"id": "moli", "engine": "moli"}, ], + "qualification": { + "reference_engine": "chromium", + "required_task_count": len(task_ids), + }, "runs_per_task": 1, "resources": {"cpus": 2.0, "memory": "2g", "sample_interval_ms": 100}, "execution": { @@ -186,7 +190,7 @@ def _receipt( task_ids: list[int], attempts: list[dict[str, object]], run_id: str = "run", - execution_scope: str = "formal", + execution_scope: str = "qualification", ) -> dict[str, object]: attempts = json.loads(json.dumps(attempts)) seed_payload = _materialize_autonomous_sources(seeds) @@ -278,18 +282,15 @@ def _receipt( "profile_sha256": _sha256(profile), "source_seed_manifest_sha256": _sha256(seeds), "task_ids": task_ids, - "engines": ["chromium", "moli"], + "engines": ["chromium"], "runtime": { "browser_eval_commit": "browser-eval-commit", "cowagent_commit": "cowagent-commit", "resources": {"cpus": 2.0, "memory": "2g", "sample_interval_ms": 100}, - "browser_images": { - "chromium": {"id": "chromium-image"}, - "moli": {"id": "moli-image"}, - }, + "browser_images": {"chromium": {"id": "chromium-image"}}, "site_images": {"gitlab": {"id": "gitlab-image"}}, }, - "method_teardown": {"chromium": True, "moli": True}, + "method_teardown": {"chromium": True}, "sessions": sessions, "physical_executions": physical_executions, "isolation": { @@ -304,7 +305,7 @@ def _receipt( } -def test_promotion_requires_same_candidate_to_pass_every_browser(tmp_path: Path) -> None: +def test_promotion_requires_candidate_to_pass_reference_qualification(tmp_path: Path) -> None: profile, selection_sha256, repair_sha256 = _profile(tmp_path, [44]) seeds = _write( tmp_path / "seeds.json", @@ -355,7 +356,7 @@ def test_promotion_requires_same_candidate_to_pass_every_browser(tmp_path: Path) "evaluator_status": "pass", "completed_action_count": 1, } - for engine in ("chromium", "moli") + for engine in ("chromium",) ], ), ) @@ -364,6 +365,8 @@ def test_promotion_requires_same_candidate_to_pass_every_browser(tmp_path: Path) assert result["counts"] == {"verified_tasks": 1, "verified_trajectories": 1} assert result["trajectories"][0]["verification"]["model_calls"] == 0 + assert result["trajectories"][0]["verification"]["browsers"] == ["chromium"] + assert result["promotion_policy"]["measured_browsers_are_not_promotion_gates"] is True assert result["trajectories"][0]["verification"]["contract"] == { "kind": "official_task_contract" } @@ -403,31 +406,30 @@ def test_promotion_rejects_incomplete_method_task_matrix(tmp_path: Path) -> None ], }, ) - receipt = _write( - tmp_path / "receipt.json", - _receipt( - seeds, - profile, - task_ids=[1], - attempts=[ - { - "task_id": 1, - "source_candidate_id": "candidate", - "engine": "chromium", - "process_status": "pass", - "evaluator_status": "pass", - "completed_action_count": 1, - } - ], - ), + receipt_payload = _receipt( + seeds, + profile, + task_ids=[1], + attempts=[ + { + "task_id": 1, + "source_candidate_id": "candidate", + "engine": "chromium", + "process_status": "pass", + "evaluator_status": "pass", + "completed_action_count": 1, + } + ], ) + receipt_payload["attempts"] = [] + receipt = _write(tmp_path / "receipt.json", receipt_payload) try: build_verified_trajectory_dataset(seeds, [receipt], profile_path=profile) except ValueError as error: assert "complete method-task matrix" in str(error) else: - raise AssertionError("partial formal receipt must not be promotable") + raise AssertionError("partial qualification receipt must not be promotable") def test_promotion_supports_hash_bound_expert_repair(tmp_path: Path) -> None: @@ -481,7 +483,7 @@ def test_promotion_supports_hash_bound_expert_repair(tmp_path: Path) -> None: "contract_correction_source_sha256": "catalog-sha", "corrected_task_sha256": "task-sha", } - for engine in ("chromium", "moli") + for engine in ("chromium",) ], ), ) @@ -498,22 +500,6 @@ def test_promotion_supports_hash_bound_expert_repair(tmp_path: Path) -> None: "corrected_task_sha256": "task-sha", } - prior = _write( - tmp_path / "prior.json", - { - "schema": "lex_webarena_verified_trajectory_dataset/1", - "official_data_checksum": None, - "trajectories": [{"id": "webarena-verified-44-trajectory-1", "task_id": 44}], - }, - ) - incremented = build_verified_trajectory_dataset( - seeds, [receipt], profile_path=profile, prior_dataset_path=prior - ) - assert [item["task_id"] for item in incremented["trajectories"]] == [44, 339] - assert incremented["counts"] == {"verified_tasks": 2, "verified_trajectories": 2} - assert incremented["provenance"]["prior_dataset_sha256"] == _sha256(prior) - - def test_promotion_rejects_diagnostic_or_nonisolated_receipts(tmp_path: Path) -> None: profile, selection_sha256, repair_sha256 = _profile(tmp_path, [1]) seeds = _write( @@ -553,7 +539,7 @@ def test_promotion_rejects_diagnostic_or_nonisolated_receipts(tmp_path: Path) -> try: build_verified_trajectory_dataset(seeds, [diagnostic_path], profile_path=profile) except ValueError as error: - assert "completed formal suite" in str(error) + assert "completed qualification suite" in str(error) else: raise AssertionError("diagnostic receipt must not be promotable") @@ -617,7 +603,7 @@ def test_promotion_rejects_receipt_claim_when_evaluation_evidence_failed( "evaluator_status": "pass", "completed_action_count": 1, } - for engine in ("chromium", "moli") + for engine in ("chromium",) ], ) evaluation = Path(receipt_payload["attempts"][0]["evidence"]["evaluation"]["path"]) @@ -676,7 +662,7 @@ def test_promotion_rejects_trace_that_differs_from_canonical_actions(tmp_path: P "evaluator_status": "pass", "completed_action_count": 1, } - for engine in ("chromium", "moli") + for engine in ("chromium",) ], ) attempt = receipt_payload["attempts"][0] @@ -744,7 +730,7 @@ def test_promotion_rejects_missing_official_evaluator(tmp_path: Path) -> None: "evaluator_status": "pass", "completed_action_count": 1, } - for engine in ("chromium", "moli") + for engine in ("chromium",) ], ) attempt = receipt_payload["attempts"][0] diff --git a/tests/trajectory_replay/test_webarena_plan.py b/tests/trajectory_replay/test_webarena_plan.py index e18eb417f..3b94769d9 100644 --- a/tests/trajectory_replay/test_webarena_plan.py +++ b/tests/trajectory_replay/test_webarena_plan.py @@ -59,6 +59,7 @@ def _profile(tmp_path: Path) -> tuple[Path, Path]: {"id": "chromium", "engine": "chromium"}, {"id": "moli", "engine": "moli"}, ], + "qualification": {"reference_engine": "chromium", "required_task_count": 2}, "execution": { "environment_pool_count": 1, "method_parallelism": 1, diff --git a/tests/trajectory_replay/test_webarena_replay.py b/tests/trajectory_replay/test_webarena_replay.py index 527d826f9..d4f5213f2 100644 --- a/tests/trajectory_replay/test_webarena_replay.py +++ b/tests/trajectory_replay/test_webarena_replay.py @@ -16,6 +16,7 @@ run_replay_suite, select_methods, select_tasks, + validate_qualification_dataset, ) @@ -44,6 +45,58 @@ def test_select_methods_rejects_profile_order_mismatch() -> None: select_methods(plan, ["chromium", "moli"]) +def test_qualification_scope_is_separate_from_diagnostics() -> None: + source = inspect.getsource(run_replay_suite) + + assert 'scope = "qualification"' in source + assert "qualification and diagnostic scopes cannot be combined" in source + assert "qualification engine differs from the frozen reference engine" in source + + +def test_formal_dataset_binding_requires_all_canonical_trajectories(tmp_path: Path) -> None: + plan = { + "seed_manifest_sha256": "seed-hash", + "profile_sha256": "profile-hash", + "qualification_engine": "chromium", + "task_ids": [1], + "seed_manifest": { + "tasks": [ + { + "task_id": 1, + "canonical_candidate_id": "candidate-1", + "candidates": [ + {"id": "candidate-1", "tool_calls": [{"action": "snapshot"}]} + ], + } + ] + }, + } + dataset = tmp_path / "trajectories.json" + dataset.write_text( + json.dumps( + { + "schema": "lex_webarena_verified_trajectory_dataset/1", + "promotion_policy": {"qualification_browser": "chromium"}, + "provenance": { + "seed_manifest_sha256": "seed-hash", + "profile_sha256": "profile-hash", + }, + "trajectories": [ + {"task_id": 1, "tool_calls": [{"action": "snapshot"}]} + ], + } + ), + encoding="utf-8", + ) + + assert len(validate_qualification_dataset(plan, dataset)) == 64 + payload = json.loads(dataset.read_text(encoding="utf-8")) + payload["trajectories"][0]["tool_calls"] = [{"action": "wait", "seconds": 1}] + dataset.write_text(json.dumps(payload), encoding="utf-8") + with pytest.raises(ValueError, match="canonical input"): + validate_qualification_dataset(plan, dataset) + + def test_rebind_url_changes_only_known_local_site_ports() -> None: assert ( _rebind_url( From 6b196ff3b301bd8b49b2516216c9a128c595a65f Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 14 Sep 2026 17:23:43 +0800 Subject: [PATCH 094/127] Use final logical results throughout report --- evaluation/browser-use/current-report.json | 2 +- harness/browser_use/report.py | 72 ++++------------------ harness/browser_use/report_i18n.py | 11 +++- research/report-changes.json | 4 +- tests/browser_use/test_report.py | 54 +++++----------- 5 files changed, 40 insertions(+), 103 deletions(-) diff --git a/evaluation/browser-use/current-report.json b/evaluation/browser-use/current-report.json index bf54bb78a..47411de37 100644 --- a/evaluation/browser-use/current-report.json +++ b/evaluation/browser-use/current-report.json @@ -39,7 +39,7 @@ "base_url": "https://artifact-hub.app.lexmount.com", "site_slug": "7l77EeXPRfml", "share_url": "https://artifact-hub.app.lexmount.com/v/Ghn1BR6PCy-njsCMqCe2LRIJ-eyJEJoa", - "expected_version": "ver_68a8dddd4c0b4cbd86d82db4b54f7d6d", + "expected_version": "ver_9f731293d5b749dc9538c94284f04ee9", "state": "verified" } } diff --git a/harness/browser_use/report.py b/harness/browser_use/report.py index ae11b7fb0..4e7a6e18a 100644 --- a/harness/browser_use/report.py +++ b/harness/browser_use/report.py @@ -1949,38 +1949,10 @@ def _execution_evidence_html(result_id: str, executions: list[dict[str, Any]]) - ) -def _first_final_rows(attempts: list[dict[str, Any]]) -> list[list[Any]]: - rows: list[list[Any]] = [] - for model in MODEL_LABELS: - for engine in ENGINE_LABELS: - logical = [ - row for row in attempts if row["model"] == model and row["engine"] == engine - ] - first = sum( - float((row.get("_source_executions") or [row])[0].get("score") or 0) == 1 - for row in logical - ) - final = sum(float(row.get("score") or 0) == 1 for row in logical) - retried = sum(len(row.get("_source_executions") or [row]) > 1 for row in logical) - rows.append( - [ - MODEL_LABELS[model], - ENGINE_LABELS[engine], - f"{first}/{len(logical)}", - retried, - f"+{final - first}" if final > first else "0", - f"{final}/{len(logical)}", - ] - ) - return rows - - -def _common_success_resource_rows(attempts: list[dict[str, Any]]) -> tuple[int, list[list[Any]]]: +def _common_final_success_resource_rows( + attempts: list[dict[str, Any]], +) -> tuple[int, list[list[Any]]]: task_ids = {int(row["task_id"]) for row in attempts} - - def first_execution(row: dict[str, Any]) -> dict[str, Any]: - return (row.get("_source_executions") or [row])[0] - common = { task_id for task_id in task_ids @@ -1989,7 +1961,7 @@ def first_execution(row: dict[str, Any]) -> dict[str, Any]: row["model"] == model and row["engine"] == engine and int(row["task_id"]) == task_id - and float(first_execution(row).get("score") or 0) == 1 + and float(row.get("score") or 0) == 1 for row in attempts ) for model in MODEL_LABELS @@ -2005,8 +1977,8 @@ def median(values: list[float]) -> float | None: for engine in ENGINE_LABELS: cohort = [ { - **first_execution(row), - **_execution_tool_metrics(first_execution(row)), + **row, + **_execution_tool_metrics(row), } for row in attempts if row["model"] == model @@ -2068,23 +2040,6 @@ def scope(code: str) -> str: ] -def _score_flow_figure(attempts: list[dict[str, Any]]) -> str: - rows = _first_final_rows(attempts) - cards = [] - for model, engine, first, retried, recovered, final in rows: - first_count, total = (int(value) for value in str(first).split("/")) - recovered_count = int(str(recovered).lstrip("+")) - first_width = first_count / total * 100 if total else 0 - recovered_width = recovered_count / total * 100 if total else 0 - cards.append( - '
    ' - + _e(f"{model} · {engine}") - + f'{_e(final)}
    ' - + f'

    首次 {_e(first)} → 失败后重试 {_e(retried)} 题 → 恢复 {_e(recovered)} 题

    ' - ) - return '
    ' + "".join(cards) + "
    " - - def _failure_scope_figure(verified_causes: dict[str, dict[str, Any]]) -> str: rows = _failure_scope_counts(verified_causes) maximum = max((count for _, count, _ in rows), default=1) @@ -2125,7 +2080,7 @@ def _automatic_failure_figure(counts: Counter[str]) -> str: def _resource_summary_cards(common_resources: tuple[int, list[list[Any]]]) -> str: task_count, rows = common_resources if not task_count: - return '

    当前没有四种配置首次都成功的任务,资源差异暂不比较。

    ' + return '

    当前没有四种配置最终都成功的任务,资源差异暂不比较。

    ' def values(engine: str, column: int) -> list[float]: label = ENGINE_LABELS[engine] @@ -2136,7 +2091,7 @@ def values(engine: str, column: int) -> list[float]: chromium_cpu, moli_cpu = values("chromium", 6), values("moli", 6) return ( '

    Moli 的内存区间更低

    ' - f'{task_count} 题首次共同成功样本中,Moli 峰值内存 P50 为 {min(moli_memory):.1f}–{max(moli_memory):.1f} MiB;' + f'{task_count} 题最终共同成功样本中,Moli 峰值内存 P50 为 {min(moli_memory):.1f}–{max(moli_memory):.1f} MiB;' f'Chromium 为 {min(chromium_memory):.1f}–{max(chromium_memory):.1f} MiB。

    ' '

    Chromium 的工具耗时与 CPU 更低

    ' f'BrowserTool P50:Chromium {min(chromium_tool):.1f}–{max(chromium_tool):.1f} 秒,Moli {min(moli_tool):.1f}–{max(moli_tool):.1f} 秒;' @@ -2201,7 +2156,7 @@ def _article_body( ) else: cause_section = '

    本轮没有失败结果,无需生成失败根因记录。

    ' - common_count, resource_rows = _common_success_resource_rows(attempts) + common_count, resource_rows = _common_final_success_resource_rows(attempts) clean = _primary_summary(eligible_attempts) clean_rows = [ [ @@ -2249,9 +2204,9 @@ def _article_body( f'

    完整 {task_count} 题的速度与浏览器资源

    均值、P50、P95 都在四种完整配置中统一比较。CPU、内存和网络只计浏览器容器,不含 CowAgent 或模型服务;模型会改变动作数量和页面停留时间,因此这里比较整套配置,不能单独归因于浏览器内核。

    ' + _resource_comparison_table(summary) + '

    每项只标记四种配置中数值唯一最低者。CPU 时间与网络总量累计逻辑结果使用的全部执行;内存来自 Browser Use 执行阶段的浏览器容器采样。

    ' - '

    共同成功任务的性能与资源

    下表限定为首次执行共同成功的 ' + '

    共同成功任务的性能与资源

    下表限定为四种配置最终都通过的 ' + str(common_count) - + ' 题,每个配置只取第一次执行并报告 P50。这既排除了重试累计,也避免把“很快失败”错当成速度优势。

    ' + + ' 题,并以每题最终逻辑结果包含的全部物理执行计算 P50;这样不会把快速失败误当成性能优势。

    ' + _table(["模型", "浏览器", "Agent 链路 P50", "BrowserTool P50", "Tokens P50", "峰值内存 P50", "CPU 时间 P50"], resource_rows, table_id="home-resource-table", classes="core-comparison-table") + _resource_summary_cards((common_count, resource_rows)) + '

    Agent 链路还包含模型推理和页面等待,因此不能解读为纯内核性能。

    ' @@ -3444,7 +3399,6 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat claims = { "schema": "lex_browser_use_interpretation/1", "claims": [ - {"claim": "审查合格比较集的首次执行、重试与最终成绩", "basis": _first_final_rows(qualified_attempts)}, {"claim": "审查合格比较集的模型内浏览器配对结果", "basis": _paired_summary(qualified_attempts)}, {"claim": "失败根因以当前证据绑定的逐结果核验为准", "basis": verified_causes}, { @@ -3477,11 +3431,11 @@ def build_report(*, evidence: Path | Iterable[Path], output: Path, selection_pat }, }, { - "claim": "四配置首次共同成功任务的性能与资源 P50", + "claim": "四配置最终共同成功任务的性能与资源 P50", "basis": dict( zip( ("task_count", "rows"), - _common_success_resource_rows(qualified_attempts), + _common_final_success_resource_rows(qualified_attempts), strict=True, ) ), diff --git a/harness/browser_use/report_i18n.py b/harness/browser_use/report_i18n.py index 63dc9d8d4..1ed292f62 100644 --- a/harness/browser_use/report_i18n.py +++ b/harness/browser_use/report_i18n.py @@ -1564,7 +1564,8 @@ def _register_report_change_translations() -> None: "前者确认“证据包与 evaluator 记录一致”;后者检查 evaluator 语义,并识别出 Task 699 等问题。已通过结果没有再做独立语义盲审,其判定依据仍是官方 evaluator。": "The first confirms that evidence bundles agree with evaluator records. The second checks evaluator semantics and identified issues such as Task 699. Passed results did not receive a separate blinded semantic review and still rely on the official evaluator.", "查看核验记录 →": "View review records →", "性能与资源:只在四种配置首次都成功的任务上看": "Performance and resources on tasks all four configurations passed on the first attempt", - "共同成功任务的性能与资源": "Performance and resources on common first-attempt successes", + "共同成功任务的性能与资源": "Performance and resources on common final successes", + "当前没有四种配置最终都成功的任务,资源差异暂不比较。": "No task was finally passed by all four configurations, so resource differences are unavailable.", "首次共同成功任务的资源差异": "Resource differences on common first-attempt successes", "当前没有四种配置首次都成功的任务,资源差异暂不比较。": "No task was passed on the first attempt by all four configurations, so resource differences are unavailable.", "Agent 链路还包含模型推理和页面等待,因此不能解读为纯内核性能。": "The agent path also includes model inference and page waits, so it is not a pure browser-core measurement.", @@ -1753,10 +1754,18 @@ def translate_text(value: str) -> str: r"下表限定为首次执行共同成功的 (\d+) 题,每个配置只取第一次执行并报告 P50。这既排除了重试累计,也避免把“很快失败”错当成速度优势。", lambda m: f"The table uses the {m.group(1)} tasks that all four configurations passed on the first attempt. It reports P50 from the first execution only, excluding retry accumulation and the false speed advantage of fast failures.", ), + ( + r"下表限定为四种配置最终都通过的 (\d+) 题,并以每题最终逻辑结果包含的全部物理执行计算 P50;这样不会把快速失败误当成性能优势。", + lambda m: f"The table uses the {m.group(1)} tasks finally passed by all four configurations. P50 includes every physical execution belonging to each final logical result, so fast failures are not mistaken for a performance advantage.", + ), ( r"(\d+) 题首次共同成功样本中,Moli 峰值内存 P50 为 ([0-9.]+)–([0-9.]+) MiB;Chromium 为 ([0-9.]+)–([0-9.]+) MiB。", lambda m: f"Across {m.group(1)} common first-attempt successes, Moli's P50 peak memory spans {m.group(2)}-{m.group(3)} MiB, versus {m.group(4)}-{m.group(5)} MiB for Chromium.", ), + ( + r"(\d+) 题最终共同成功样本中,Moli 峰值内存 P50 为 ([0-9.]+)–([0-9.]+) MiB;Chromium 为 ([0-9.]+)–([0-9.]+) MiB。", + lambda m: f"Across {m.group(1)} common final successes, Moli's P50 peak memory spans {m.group(2)}-{m.group(3)} MiB, versus {m.group(4)}-{m.group(5)} MiB for Chromium.", + ), ( r"BrowserTool P50:Chromium ([0-9.]+)–([0-9.]+) 秒,Moli ([0-9.]+)–([0-9.]+) 秒;CPU 时间 P50:Chromium ([0-9.]+)–([0-9.]+) 秒,Moli ([0-9.]+)–([0-9.]+) 秒。", lambda m: f"BrowserTool P50: Chromium {m.group(1)}-{m.group(2)} seconds, Moli {m.group(3)}-{m.group(4)} seconds. CPU-time P50: Chromium {m.group(5)}-{m.group(6)} seconds, Moli {m.group(7)}-{m.group(8)} seconds.", diff --git a/research/report-changes.json b/research/report-changes.json index cd2e980a6..89fd67fbe 100644 --- a/research/report-changes.json +++ b/research/report-changes.json @@ -12,14 +12,14 @@ "status": "已处理", "scope": "Browser Use 首页、核心指标表、同题矩阵、性能统计和结果分析页。", "findings": "上一版误把排除 14 个已知外部干扰任务后的 76 题事后诊断子集写成正式主结果,造成报告看起来只有 76 题;首页还重复突出首次执行与重试恢复,掩盖了每题一个最终逻辑结果的正式口径。", - "corrections": "首页和结果分析恢复完整 90 题官方 evaluator 结果;四种配置的核心指标、同题矩阵和性能统计均使用 90 题。76 题仅保留为明确标注的事后诊断子集,不改分、不替代官方结果。首页删除首次/重试拆分模块,物理执行记录只在单题证据页下钻。", + "corrections": "首页和结果分析恢复完整 90 题官方 evaluator 结果;四种配置的核心指标、同题矩阵和性能统计均使用 90 题。76 题仅保留为明确标注的事后诊断子集,不改分、不替代官方结果。首页删除首次/重试拆分模块;共同成功性能统计按最终逻辑结果及其全部物理执行计算,物理执行明细只在单题证据页下钻。", "remaining": "14 个外部干扰任务的历史分数保持不变;只有修复后按冻结协议产生新的完整 90 题证据批次,才能更新官方结果。", "i18n": {"en": { "title": "Restore the complete 90-task official result as the primary view", "status": "Resolved", "scope": "The Browser Use homepage, core metrics, same-task matrix, performance statistics, and result analysis.", "findings": "The prior version incorrectly presented the 76-task post-hoc diagnostic subset, which excludes 14 known interference tasks, as the formal primary result. It therefore appeared to contain only 76 tasks. The homepage also overemphasized first attempts and retry recovery instead of one final logical result per task.", - "corrections": "Restored the complete official 90-task evaluator result on the homepage and analysis page. Core metrics, the same-task matrix, and performance statistics now use all 90 tasks across all four configurations. The 76-task view remains only as an explicitly labeled post-hoc diagnostic subset and does not rescore or replace the official result. Removed the first-attempt/retry split from the homepage; physical executions remain available on task evidence pages.", + "corrections": "Restored the complete official 90-task evaluator result on the homepage and analysis page. Core metrics, the same-task matrix, and performance statistics now use all 90 tasks across all four configurations. The 76-task view remains only as an explicitly labeled post-hoc diagnostic subset and does not rescore or replace the official result. Removed the first-attempt/retry split from the homepage; common-success performance now uses each final logical result and all of its physical executions, while physical-execution details remain on task evidence pages.", "remaining": "Historical scores for the 14 interference tasks remain unchanged. Only a new complete 90-task evidence cohort produced after corrections under the frozen protocol can update the official result." }} }, diff --git a/tests/browser_use/test_report.py b/tests/browser_use/test_report.py index f5b30579c..031238a27 100644 --- a/tests/browser_use/test_report.py +++ b/tests/browser_use/test_report.py @@ -10,10 +10,9 @@ _attempt_table, _automatic_failure_type, _best, - _common_success_resource_rows, + _common_final_success_resource_rows, _failure_diagnosis, _failure_scope_counts, - _first_final_rows, _load_evidence_bundles, _logical_results, _paired_summary, @@ -45,38 +44,6 @@ def test_best_marks_only_one_visibly_distinct_winner() -> None: ) -def test_interpretation_keeps_first_attempt_and_retry_recovery_separate( - monkeypatch, -) -> None: - monkeypatch.setattr(report_module, "MODEL_LABELS", {"model": "Model"}) - monkeypatch.setattr(report_module, "ENGINE_LABELS", {"chromium": "Chromium"}) - rows = [ - { - "model": "model", - "engine": "chromium", - "task_id": 1, - "score": 1, - "_source_executions": [{"score": 1}], - }, - { - "model": "model", - "engine": "chromium", - "task_id": 2, - "score": 1, - "_source_executions": [{"score": 0}, {"score": 1}], - }, - { - "model": "model", - "engine": "chromium", - "task_id": 3, - "score": 0, - "_source_executions": [{"score": 0}, {"score": 0}], - }, - ] - - assert _first_final_rows(rows) == [["Model", "Chromium", "1/3", 2, "+1", "2/3"]] - - def test_failure_scope_summary_does_not_merge_browser_evidence_into_agent() -> None: causes = { "a": {"subcategory": {"code": "agent_retrieval"}}, @@ -640,7 +607,7 @@ def test_report_publishes_one_final_logical_result_and_aggregates_cost( assert not list(output.rglob("*.svg")) -def test_common_success_resources_use_only_shared_first_attempts( +def test_common_success_resources_use_final_logical_results( tmp_path: Path, monkeypatch ) -> None: monkeypatch.setattr(report_module, "MODEL_LABELS", {"a": "A", "b": "B"}) @@ -677,13 +644,20 @@ def execution(task_id: int, model: str, engine: str, attempt: int, score: int) - if task_id == 2 and first_score == 0: sources.append(execution(task_id, model, engine, 2, 1)) final_score = 1 - logical.append({**sources[-1], "score": final_score, "_source_executions": sources}) + logical.append( + { + **sources[-1], + "score": final_score, + "duration_ms": sum(item["duration_ms"] for item in sources), + "_source_executions": sources, + } + ) - count, rows = _common_success_resource_rows(logical) + count, rows = _common_final_success_resource_rows(logical) - assert count == 1 - assert {row[2] for row in rows} == {"1.0 秒"} - assert {row[5] for row in rows} == {"1.0 MiB"} + assert count == 2 + assert {row[2] for row in rows} == {"1.5 秒", "2.5 秒"} + assert {row[5] for row in rows} == {"1.5 MiB"} def test_summary_winners_are_scoped_to_all_configurations(monkeypatch) -> None: From d38a56f095b7b7d628cef028e311de85b0a4640e Mon Sep 17 00:00:00 2001 From: kevin Date: Mon, 14 Sep 2026 18:15:43 +0800 Subject: [PATCH 095/127] Correct complete Browser Use publication --- evaluation/browser-use/current-report.json | 2 +- harness/browser_use/report.py | 113 ++++++++++++-------- harness/browser_use/report_i18n.py | 30 ++++++ research/report-changes.json | 57 ++++++++++ scripts/check_browser_use_report.py | 21 ++-- scripts/publish_browser_use_artifact_hub.py | 30 +++++- tests/browser_use/test_publish.py | 18 ++++ tests/browser_use/test_report.py | 10 +- 8 files changed, 219 insertions(+), 62 deletions(-) diff --git a/evaluation/browser-use/current-report.json b/evaluation/browser-use/current-report.json index 47411de37..294992029 100644 --- a/evaluation/browser-use/current-report.json +++ b/evaluation/browser-use/current-report.json @@ -39,7 +39,7 @@ "base_url": "https://artifact-hub.app.lexmount.com", "site_slug": "7l77EeXPRfml", "share_url": "https://artifact-hub.app.lexmount.com/v/Ghn1BR6PCy-njsCMqCe2LRIJ-eyJEJoa", - "expected_version": "ver_9f731293d5b749dc9538c94284f04ee9", + "expected_version": "ver_ce4dd70fde2e444ab18d24bba3ee0e93", "state": "verified" } } diff --git a/harness/browser_use/report.py b/harness/browser_use/report.py index 4e7a6e18a..abed9bbe4 100644 --- a/harness/browser_use/report.py +++ b/harness/browser_use/report.py @@ -1056,17 +1056,11 @@ def _action_duration_cards(attempts: list[dict[str, Any]]) -> str: if _row_model(row) == model and row["engine"] == engine ) metrics[key] = (total, count, total / count if count else None) - average_winners = { - model: _best( - { - engine: metrics[f"{model}|{engine}"][2] - for engine in ENGINE_LABELS - }, - higher=False, - display=lambda value: round(value / 1000, 3), - ) - for model in MODEL_LABELS - } + average_winners = _best( + {key: values[2] for key, values in metrics.items()}, + higher=False, + display=lambda value: round(value / 1000, 3), + ) maximum = max((values[2] or 0 for values in metrics.values()), default=0) browser_rows = [] for model in MODEL_LABELS: @@ -1076,15 +1070,15 @@ def _action_duration_cards(attempts: list[dict[str, Any]]) -> str: width = 0 if average is None or maximum <= 0 else max(2.0, average / maximum * 100) average_text = "—" if average is None else f"{average / 1000:.3f} 秒" browser_rows.append( - f'
    ' - f"{_e(MODEL_LABELS[model])} · {_e(ENGINE_LABELS[engine])}{_winner(average_text, won=engine in average_winners[model], label='同模型浏览器中单次均值最低')}" + f'
    ' + f"{_e(MODEL_LABELS[model])} · {_e(ENGINE_LABELS[engine])}{_winner(average_text, won=key in average_winners, label='全部配置中单次均值最低')}" f'
    ' f'
    总耗时 {total / 1000:.1f} 秒 · {count} 次调用
    ' ) cards.append( '
    ' + _e(action) - + "按模型分层 · 单次调用墙钟时间
    " + + "全部配置 · 单次调用墙钟时间" + "".join(browser_rows) + "
    " ) @@ -1219,6 +1213,55 @@ def _resource_value(value: float | None, unit: str) -> str: raise ValueError(f"unknown resource unit: {unit}") +def _resource_detail_rows(resource: dict[str, Any]) -> list[list[str]]: + fields = ( + ("cpu_ms", "浏览器 CPU 时间", "ms"), + ("cpu_average_utilization_pct", "平均单核 CPU 占用", "pct"), + ("memory_mean_bytes", "时间加权平均内存", "mib"), + ("memory_peak_bytes", "峰值内存", "mib"), + ("memory_retained_bytes", "结束时新增常驻内存", "mib"), + ("network_rx_bytes", "网络接收量", "mib_net"), + ("network_tx_bytes", "网络发送量", "mib_net"), + ) + return [ + [label, _resource_value(float(resource[key]), unit)] + for key, label, unit in fields + if resource.get(key) is not None + ] + + +def _evidence_check_rows(checks: dict[str, Any]) -> list[list[str]]: + labels = { + "network_har_exists": "浏览器网络记录存在", + "agent_trace_exists": "浏览器执行轨迹存在", + "response_exists": "Agent 最终回答存在", + "evaluation_exists": "官方判分明细存在", + "events_exists": "Agent 事件流存在", + "environment_prepare_exists": "环境准备记录存在", + "evaluation_json": "判分明细可读取", + "response_json": "最终回答可读取", + "response_matches_record": "最终回答与运行记录一致", + "score_matches_evaluator": "报告分数与官方判分一致", + "status_matches_evaluator": "报告状态与官方判分一致", + "official_evaluator_version_present": "官方 evaluator 版本已记录", + "official_evaluator_checksum_matches": "官方 evaluator 文件校验一致", + "official_data_checksum_matches": "官方题集文件校验一致", + "model_request_attempts_accounted": "模型请求次数已完整计入", + } + return [ + [labels.get(key, key.replace("_", " ")), "通过" if value else "未通过"] + for key, value in checks.items() + ] + + +def _publication_label(value: Any) -> str: + return { + "private_only_session_sensitive": "仅保留哈希与字节数(含登录态)", + "sanitized_projection": "已脱敏公开副本", + "public": "公开原文件", + }.get(str(value), "按证据清单处理") + + def _resource_comparison_table(summary: dict[str, dict[str, dict[str, Any]]]) -> str: metrics = ( ("cpu_ms", "CPU 时间", "ms"), @@ -1393,7 +1436,7 @@ def _evidence_viewer_page() -> str: body += ( '
    ' '

    执行证据 JSON

    ' - '
    ' + '
    ' '

    正在读取证据…

    ' '
    '
             "
    \n\n\n\n

    gimmiethat.space

    \n\n", + "selector": "textarea[aria-label=\"Editor content;Press Alt+F1 for Accessibility Options.\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "fill", + "text": "Change browser tab title to GIVE ME SPACE", + "selector": "textarea[name=\"commit_message\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "get_text", + "selector": "textarea[aria-label^=\"Editor content\"]" + }, + { + "action": "click", + "selector": "button:visible:has-text(\"Commit changes\")", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "wait", + "timeout": 1000 + }, + { + "action": "snapshot" + } + ], + "evaluation_input": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "authoritative_successful_browser_use_execution", + "candidate_id": "gpt-5.6-sol__moli__task-441", + "model": "gpt-5.6-sol", + "browser": "moli", + "execution": 1, + "events_sha256": "c03872ea11c034863ab87fdefd40bf201e3e8d0af785c40f1b2bf10f77bde025", + "response_sha256": "db765f7e78b6c552e25494cde987b92c12e4d11d6990604b328f604c66bbf9c7", + "evaluation_sha256": "bf3b67b9f93f3fc74d3b374aaeccfbd3672653e766673d3641562e66da0bb2f7" + }, + "verification": { + "browsers": [ + "chromium" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "official_task_contract" + }, + "model_calls": 0, + "receipt_run_ids": [ + "ab8cbb4dd809" + ] + } + }, + { + "id": "webarena-verified-448-trajectory-1", + "task_id": 448, + "task": { + "intent": "set the homepage URL on my GitLab profile to https://egg.tart.com", + "intent_template_id": 331, + "revision": 2, + "sites": [ + "gitlab" + ], + "start_urls": [ + "__GITLAB__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://192.168.5.2:8023" + }, + { + "action": "click", + "selector": "a[href=\"/byteblaze\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "click", + "selector": "a[href=\"/-/profile\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "fill", + "text": "https://egg.tart.com", + "selector": "input[name=\"user[website_url]\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "click", + "selector": "button:visible:has-text(\"Update profile settings\")", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + } + ], + "evaluation_input": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "authoritative_successful_browser_use_execution", + "candidate_id": "gpt-5.6-sol__chromium__task-448", + "model": "gpt-5.6-sol", + "browser": "chromium", + "execution": 2, + "events_sha256": "45563c6211f6257a24bff31c8d103aa8437273cef0e67ff30f836f7a0c3b8d3a", + "response_sha256": "db765f7e78b6c552e25494cde987b92c12e4d11d6990604b328f604c66bbf9c7", + "evaluation_sha256": "274eb1881e12fe901e14791abaab1586bd2a446935d7cc06da1093a2dd6584e2" + }, + "verification": { + "browsers": [ + "chromium" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "corrected_replay_contract", + "correction_id": "wv-448-match-profile-save-before-recency-20260915", + "correction_source_sha256": "a1390db7fae99adeb3e3d7bb55159e09eee2bf31f063f501bdd4ee902772e510", + "corrected_task_sha256": "0a5aae653c1beca1bb30d2f73289b1c6f0ed6623d67da799d6fd3967ddbb0c43" + }, + "model_calls": 0, + "receipt_run_ids": [ + "2904cf117286" + ] + } + }, + { + "id": "webarena-verified-464-trajectory-1", + "task_id": 464, + "task": { + "intent": "Update the product description of Antonia Racer Tank to \"{count} customer(s) love it!\" where count is the number of reviews with 4 stars or above, or \"don't miss out on this amazing product\" when there are no such reviews.", + "intent_template_id": 251, + "revision": 2, + "sites": [ + "shopping_admin" + ], + "start_urls": [ + "__SHOPPING_ADMIN__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:7780/admin/catalog/product/edit/id/1796/" + }, + { + "action": "wait", + "seconds": 1 + }, + { + "action": "evaluate", + "script": "const title=document.querySelector('[data-index=\"content\"] > .fieldset-wrapper-title'); if(!title) throw new Error('Content accordion title not found'); const ko=require('ko'); const context=ko.contextFor(title); const viewModel=context&&context.$data; if(!viewModel||typeof viewModel.toggleOpened!=='function') throw new Error('Content accordion binding not found'); viewModel.toggleOpened(); return typeof viewModel.opened==='function'?viewModel.opened():true;" + }, + { + "action": "wait", + "seconds": 3 + }, + { + "action": "evaluate", + "script": "const fields=[...document.querySelectorAll('textarea')].filter(el=>el.name==='short_description'||el.name==='product[short_description]'); if(fields.length!==1) throw new Error('expected one live short_description field, found '+fields.length); const field=fields[0]; field.value='

    1 customer(s) love it!

    '; field.dispatchEvent(new Event('input',{bubbles:true})); field.dispatchEvent(new Event('change',{bubbles:true})); return {name:field.name,value:field.value};" + }, + { + "action": "evaluate", + "script": "const form=require('uiRegistry').get('product_form.product_form'); const source=form?.source; if(!source?.client||typeof source.client._save!=='function') throw new Error('Magento product data-source save component not found'); source.set('data.product.short_description','

    1 customer(s) love it!

    '); setTimeout(()=>source.client._save(source.get('data'),{redirect:false,ajaxSave:false,attributes:{id:form.namespace}}),500); return {component:source.name,action:'save'};" + }, + { + "action": "wait", + "seconds": 3 + } + ], + "evaluation_input": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "expert_repair_spec", + "candidate_id": "expert__task-464__short-description", + "repair_spec_sha256": "ea34996acd3f137da912dcc386858c2ab51b86514cacc23502d1448224bbb830", + "details": { + "task_revision": 2, + "evidence": "Official task contract and seeded product-review records for product 1796" + }, + "repair_basis": { + "root_cause": "The autonomous runs edited the long Page Builder description instead of the product short_description field required by the evaluator.", + "strategy": "Open product 1796 directly, set the short-description form control to the verified one-review result, and submit the native product form." + } + }, + "verification": { + "browsers": [ + "chromium" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "official_task_contract" + }, + "model_calls": 0, + "receipt_run_ids": [ + "ab8cbb4dd809" + ] + } + }, + { + "id": "webarena-verified-465-trajectory-1", + "task_id": 465, + "task": { + "intent": "Add Tide PODS Spring Meadow Scent HE Turbo Laundry Detergent Pacs, 81 Count to my wish list", + "intent_template_id": 186, + "revision": 2, + "sites": [ + "shopping" + ], + "start_urls": [ + "__SHOPPING__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://192.168.5.2:7770" + }, + { + "action": "fill", + "text": "Tide PODS Spring Meadow Scent HE Turbo Laundry Detergent Pacs, 81 Count", + "selector": "input[name=\"q\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "press", + "key": "Enter" + }, + { + "action": "snapshot" + }, + { + "action": "click", + "selector": ":nth-match(a[href=\"#\"]:visible, 3)", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "wait", + "timeout": 2000 + }, + { + "action": "snapshot" + } + ], + "evaluation_input": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "authoritative_successful_browser_use_execution", + "candidate_id": "gpt-5.6-sol__chromium__task-465", + "model": "gpt-5.6-sol", + "browser": "chromium", + "execution": 1, + "events_sha256": "dfb9b2bd5a3ac26af7db9a3f8c98381a46473e71cf79524648a24954670c35f6", + "response_sha256": "db765f7e78b6c552e25494cde987b92c12e4d11d6990604b328f604c66bbf9c7", + "evaluation_sha256": "7444b8a871d291aa153435f2516a462da41459a4cd6e67ed35730c871f1cee52" + }, + "verification": { + "browsers": [ + "chromium" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "official_task_contract" + }, + "model_calls": 0, + "receipt_run_ids": [ + "ab8cbb4dd809" + ] + } + }, + { + "id": "webarena-verified-470-trajectory-1", + "task_id": 470, + "task": { + "intent": "Cancel order 302", + "intent_template_id": 257, + "revision": 2, + "sites": [ + "shopping_admin" + ], + "start_urls": [ + "__SHOPPING_ADMIN__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:7780/admin/sales/order/view/order_id/302/" + }, + { + "action": "evaluate", + "script": "const button=document.querySelector('#order-view-cancel-button'); if(!button) throw new Error('cancel button not found'); button.click(); return true;" + }, + { + "action": "wait", + "seconds": 1 + }, + { + "action": "evaluate", + "script": "const button=document.querySelector('.modal-popup._show .action-primary.action-accept'); if(!button) throw new Error('confirmation button not found'); button.click(); return true;" + }, + { + "action": "wait", + "seconds": 3 + } + ], + "evaluation_input": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "expert_repair_spec", + "candidate_id": "expert__task-470__cancel-order-302", + "repair_spec_sha256": "ea34996acd3f137da912dcc386858c2ab51b86514cacc23502d1448224bbb830", + "details": { + "task_revision": 2, + "evidence": "Chromium canary passed the official evaluator after Magento dependency readiness was enforced" + }, + "repair_basis": { + "root_cause": "Earlier cancellations raced Magento search readiness; Moli additionally timed out while checking actionability of the confirmation button.", + "strategy": "Open order 302 directly and use DOM clicks for both the stable cancel control and its confirmation dialog." + } + }, + "verification": { + "browsers": [ + "chromium" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "official_task_contract" + }, + "model_calls": 0, + "receipt_run_ids": [ + "ab8cbb4dd809" + ] + } + }, + { + "id": "webarena-verified-475-trajectory-1", + "task_id": 475, + "task": { + "intent": "Set up a new, empty repository with the name chatgpt_plugin", + "intent_template_id": 292, + "revision": 2, + "sites": [ + "gitlab" + ], + "start_urls": [ + "__GITLAB__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:8023/projects/new#blank_project" + }, + { + "action": "wait", + "seconds": 1 + }, + { + "action": "evaluate", + "script": "const set=(selector,value)=>{const el=document.querySelector(selector);if(!el)throw new Error(selector+' not found');el.value=value;el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));}; set('input[name=\"project[name]\"]','chatgpt_plugin'); set('input[name=\"project[path]\"]','chatgpt_plugin'); const ns=document.querySelector('select[name=\"project[namespace_id]\"]'); if(ns){ns.value='2505';ns.dispatchEvent(new Event('change',{bubbles:true}));} const readme=document.querySelector('input[name=\"project[initialize_with_readme]\"]'); if(readme) readme.checked=false; return true;" + }, + { + "action": "evaluate", + "script": "const form=document.querySelector('form[action$=\"/projects\"]'); if(!form) throw new Error('project form not found'); if(form.requestSubmit) form.requestSubmit(); else form.submit(); return true;" + }, + { + "action": "wait", + "seconds": 3 + } + ], + "evaluation_input": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "expert_repair_spec", + "candidate_id": "expert__task-475__empty-project", + "repair_spec_sha256": "ea34996acd3f137da912dcc386858c2ab51b86514cacc23502d1448224bbb830", + "details": { + "task_revision": 2, + "contract_correction_id": "wv-475-unchecked-checkbox-is-false-20260911", + "evidence": "Official task contract plus browser-standard unchecked-checkbox serialization" + }, + "repair_basis": { + "root_cause": "The upstream evaluator required an explicit zero for an unchecked checkbox even though browsers omit unchecked controls from form submissions.", + "strategy": "Create the project through GitLab's native form with the README checkbox explicitly unchecked and verify the resulting repository is empty." + } + }, + "verification": { + "browsers": [ + "chromium" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "corrected_replay_contract", + "correction_id": "wv-475-unchecked-checkbox-is-false-20260911", + "correction_source_sha256": "ac41b463b33f8364bf67d5c76eb76e93004fa85f25a43c4a22a5e2f01a45e102", + "corrected_task_sha256": "7dc04654e3adf67db0da4cf20bf6147f446078a9481ae8249910a231560579a1" + }, + "model_calls": 0, + "receipt_run_ids": [ + "2904cf117286" + ] + } + }, + { + "id": "webarena-verified-506-trajectory-1", + "task_id": 506, + "task": { + "intent": "Buy the highest rated product from the meat substitute category within a budget between 100 and 200. Discard any items in your cart if it is not empty.", + "intent_template_id": 172, + "revision": 2, + "sites": [ + "shopping" + ], + "start_urls": [ + "__SHOPPING__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:7770/checkout/cart/" + }, + { + "action": "wait", + "seconds": 2 + }, + { + "action": "evaluate", + "script": "(async()=>{const links=[...document.querySelectorAll('a.action-delete, a[title=\"Remove item\"]')]; const formKey=document.querySelector('input[name=\"form_key\"]')?.value; if(links.length&&!formKey) throw new Error('Magento form_key missing'); for(const link of links){const raw=link.getAttribute('data-post'); if(!raw) throw new Error('cart removal contract missing'); const request=JSON.parse(raw); const response=await fetch(request.action,{method:'POST',credentials:'same-origin',headers:{'Content-Type':'application/x-www-form-urlencoded; charset=UTF-8','X-Requested-With':'XMLHttpRequest'},body:new URLSearchParams({...request.data,form_key:formKey})}); if(!response.ok) throw new Error(`cart removal failed: ${response.status}`);} return {removed:links.length};})()" + }, + { + "action": "wait", + "seconds": 2 + }, + { + "action": "navigate", + "url": "http://host.docker.internal:7770/beyond-meat-beef-beefy-crumble-5-pound-2-per-case.html" + }, + { + "action": "click", + "selector": "button:visible:has-text(\"Add to Cart\")", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "wait", + "seconds": 3 + }, + { + "action": "navigate", + "url": "http://host.docker.internal:7770/checkout/cart/" + }, + { + "action": "wait", + "seconds": 2 + }, + { + "action": "evaluate", + "script": "const names=[...document.querySelectorAll('.cart.item .product-item-name, .cart.item strong.product-item-name')].map(e=>e.textContent.trim()); if(names.length!==1 || !names[0].includes('Beyond Meat Beef Beefy Crumble')) throw new Error(`cart isolation failed: ${JSON.stringify(names)}`); return names;" + }, + { + "action": "click", + "selector": "button:visible:has-text(\"Proceed to Checkout\")", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "wait", + "seconds": 4 + }, + { + "action": "click", + "selector": "input[name=\"ko_unique_1\"]", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "click", + "selector": "button:visible:has-text(\"Next\")", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "wait", + "seconds": 4 + }, + { + "action": "click", + "selector": ".payment-method._active button.action.primary.checkout:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "wait", + "seconds": 20 + }, + { + "action": "get_text", + "selector": "main" + } + ], + "evaluation_input": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "expert_repair_spec", + "candidate_id": "expert__task-506__meat-substitute-checkout", + "repair_spec_sha256": "ea34996acd3f137da912dcc386858c2ab51b86514cacc23502d1448224bbb830", + "details": { + "kind": "reviewed_task_contract_and_existing_browser_evidence", + "task_revision": 2 + }, + "repair_basis": { + "root_cause": "The shopping reset left three unrelated products in the account cart, so checkout contained four items and violated the evaluator contract.", + "strategy": "Clear every server-backed cart item through its own Magento removal contract before adding and purchasing the required product." + } + }, + "verification": { + "browsers": [ + "chromium" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "official_task_contract" + }, + "model_calls": 0, + "receipt_run_ids": [ + "d6e1474684e2" + ] + } + }, + { + "id": "webarena-verified-510-trajectory-1", + "task_id": 510, + "task": { + "intent": "Buy the best rating product from \"Home Audio Speaker\" category with at least 5 reviews and the product is least expensive. Choose any available variant.", + "intent_template_id": 216, + "revision": 2, + "sites": [ + "shopping" + ], + "start_urls": [ + "__SHOPPING__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:7770/checkout/cart/" + }, + { + "action": "wait", + "seconds": 3 + }, + { + "action": "evaluate", + "script": "(() => { const quantities=[...document.querySelectorAll('input[name^=\"cart[\"][name$=\"[qty]\"]')]; if(!quantities.length) return 'cart already empty'; for(const field of quantities) field.value='0'; const form=quantities[0].form; if(!form || !/checkout\\/cart\\/updatePost\\/?$/.test(form.action)) throw new Error('cart update form missing'); form.submit(); return `submitted removal of ${quantities.length} cart item(s)`; })()" + }, + { + "action": "wait", + "seconds": 4 + }, + { + "action": "navigate", + "url": "http://host.docker.internal:7770/atlantic-technology-fs-7-0-glb-7-channel-surround-bar-gloss-black-discontinued-by-manufacturer.html" + }, + { + "action": "wait", + "seconds": 3 + }, + { + "action": "click", + "selector": "#product-addtocart-button" + }, + { + "action": "wait", + "seconds": 2 + }, + { + "action": "navigate", + "url": "http://host.docker.internal:7770/checkout/cart/" + }, + { + "action": "wait", + "seconds": 3 + }, + { + "action": "evaluate", + "script": "(() => { const rows=[...document.querySelectorAll('#shopping-cart-table tbody.cart.item')]; if(rows.length!==1) throw new Error(`expected one cart item, found ${rows.length}`); const name=(rows[0].querySelector('.product-item-name')?.textContent||rows[0].textContent||'').replace(/\\s+/g,' ').trim(); if(!name.includes('Atlantic Technology FS-7.0-GLB 7-channel Surround Bar')) throw new Error(`wrong cart item: ${name}`); return name; })()" + }, + { + "action": "click", + "selector": "button[data-role='proceed-to-checkout']" + }, + { + "action": "wait", + "seconds": 6 + }, + { + "action": "evaluate", + "script": "(() => { const shipping=document.querySelector('input[type=radio][name=ko_unique_1]') || document.querySelector('input[type=radio][value=flatrate_flatrate]'); if(shipping && !shipping.checked) shipping.click(); const next=document.querySelector('button[data-role=opc-continue]'); if(!next) throw new Error('shipping continue button missing'); next.click(); return true; })()" + }, + { + "action": "wait", + "seconds": 6 + }, + { + "action": "evaluate", + "script": "(() => { const radio=document.querySelector('input[type=radio][value=checkmo]'); if(!radio) throw new Error('checkmo payment method missing'); if(!radio.checked) radio.click(); return true; })()" + }, + { + "action": "wait", + "seconds": 3 + }, + { + "action": "evaluate", + "script": "(() => { const place=[...document.querySelectorAll('button')].find(e=>e.textContent.trim()==='Place Order' && !e.disabled); if(!place) throw new Error('place order button missing'); place.click(); return true; })()" + }, + { + "action": "wait", + "seconds": 8 + }, + { + "action": "get_text", + "selector": ".checkout-success" + } + ], + "evaluation_input": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "expert_repair_spec", + "candidate_id": "expert-repair-c-510", + "repair_spec_sha256": "ea34996acd3f137da912dcc386858c2ab51b86514cacc23502d1448224bbb830", + "details": { + "kind": "reviewed_task_contract_and_existing_browser_evidence", + "task_revision": 2 + }, + "repair_basis": { + "root_cause": "The autonomous attempts checked out a different product, and the authenticated fixture starts with unrelated items in its persistent cart; the evaluator requires a one-item cart containing product 75640.", + "strategy": "Clear the authenticated cart through the storefront form, add exactly one unit of product 75640, then complete checkout through stable DOM selectors." + } + }, + "verification": { + "browsers": [ + "chromium" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "corrected_replay_contract", + "correction_id": "wv-510-freeze-target-and-use-cart-dom-20260912", + "correction_source_sha256": "5a141413e71ad077c19515596b7b4a904fac427bdaa43de656a8f978aab7839e", + "corrected_task_sha256": "5fb4e1b3f5d734001981c6382a37e3a4ddfeda523f8da198e76bdc764108e7ad" + }, + "model_calls": 0, + "receipt_run_ids": [ + "2904cf117286" + ] + } + }, + { + "id": "webarena-verified-528-trajectory-1", + "task_id": 528, + "task": { + "intent": "Fill out the contact us form with this refund message for the phone screen protector bought March 2023: \"I need a refund. Order: . Amount: . Reason: It broke after three days of use.\" Leave the filled form ready for review and do not submit it.", + "intent_template_id": 154, + "revision": 2, + "sites": [ + "shopping" + ], + "start_urls": [ + "__SHOPPING__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:7770/contact" + }, + { + "action": "evaluate", + "script": "const values={\"input[name='name']\":'Emma Lopez',\"input[name='email']\":'emma.lopez@gmail.com',\"textarea[name='comment']\":'I need a refund. Order: 000000180. Amount: $12.99. Reason: It broke after three days of use.'}; for(const [selector,value] of Object.entries(values)){const el=document.querySelector(selector);if(!el)throw new Error(selector+' not found');el.value=value;el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));} return Object.fromEntries(Object.keys(values).map(selector=>[selector,document.querySelector(selector).value]));" + } + ], + "evaluation_input": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "expert_repair_spec", + "candidate_id": "expert__task-528__refund-form-dom", + "repair_spec_sha256": "ea34996acd3f137da912dcc386858c2ab51b86514cacc23502d1448224bbb830", + "details": { + "task_revision": 2, + "contract_correction_id": "wv-528-dom-state-without-submit-20260911", + "evidence": "Official task contract, seeded March 2023 order, and corrected final-DOM acceptance contract" + }, + "repair_basis": { + "root_cause": "The upstream contract simultaneously required an artificial POST and prohibited the real contact-form POST, contradicting the instruction to leave the form unsubmitted.", + "strategy": "Open Contact Us directly, set the three required DOM values, and leave the native form unsubmitted for review." + } + }, + "verification": { + "browsers": [ + "chromium" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "corrected_replay_contract", + "correction_id": "wv-528-dom-state-without-submit-20260911", + "correction_source_sha256": "c9fc0e69b795fa4636926c2de68e349187ee45a0c6d7e5747c4bba46037de6f2", + "corrected_task_sha256": "6d3f1296cfc7a0288a66e3278e5ec285a3eb4ca614b37f2dc0a98124de8dd7e7" + }, + "model_calls": 0, + "receipt_run_ids": [ + "2904cf117286" + ] + } + }, + { + "id": "webarena-verified-547-trajectory-1", + "task_id": 547, + "task": { + "intent": "Add a new color option brown to the size S of Phoebe Zipper Sweatshirt", + "intent_template_id": 252, + "revision": 2, + "sites": [ + "shopping_admin" + ], + "start_urls": [ + "__SHOPPING_ADMIN__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:7780/admin/catalog/product/edit/id/1130/" + }, + { + "action": "wait", + "seconds": 4 + }, + { + "action": "evaluate", + "script": "(() => { const button=[...document.querySelectorAll('button')].find(e=>e.textContent.trim()==='Edit Configurations'); if(!button) throw new Error('Edit Configurations button missing'); button.click(); return true; })()" + }, + { + "action": "wait", + "seconds": 2 + }, + { + "action": "evaluate", + "script": "(() => { const dialog=[...document.querySelectorAll('aside[role=dialog]')].find(e=>e.offsetParent!==null); const next=[...(dialog||document).querySelectorAll('button')].find(e=>e.textContent.trim()==='Next' && !e.disabled); if(!next) throw new Error('configuration Next button missing'); next.click(); return true; })()" + }, + { + "action": "wait", + "seconds": 2 + }, + { + "action": "evaluate", + "script": "(() => { const boxes=[...document.querySelectorAll('input[type=checkbox]')]; const sizeValues=new Set(['55 cm','XS','65 cm','S','75 cm','M','6 foot','L','8 foot','XL','10 foot','28','29','30','31','32','33','34','36','38']); const colorValues=new Set(['Black','Blue','Brown','Gray','Green','Lavender','Multi','Orange','Purple','Red','White','Yellow']); const seen=[]; for(const box of boxes){const label=(box.labels?.[0]?.textContent||'').trim(); if(!sizeValues.has(label)&&!colorValues.has(label)) continue; const should=label==='S'||label==='Brown'; if(box.checked!==should) box.click(); if(should) seen.push(label);} if(!seen.includes('S')||!seen.includes('Brown')) throw new Error(`missing Size/Color options: ${seen.join(',')}`); const next=[...document.querySelectorAll('button')].filter(e=>e.textContent.trim()==='Next'&&!e.disabled).at(-1); if(!next) throw new Error('Attribute Values Next button missing'); next.click(); return seen; })()" + }, + { + "action": "wait", + "seconds": 2 + }, + { + "action": "evaluate", + "script": "(() => { const price=document.querySelector('#apply-single-price-radio'); const quantity=document.querySelector('#apply-single-inventory-radio'); if(!price||!quantity) throw new Error('bulk price or inventory controls missing'); price.click(); quantity.click(); return true; })()" + }, + { + "action": "wait", + "seconds": 1 + }, + { + "action": "evaluate", + "script": "(() => { const set=(name,value)=>{const field=document.querySelector(`input[name=${name}]`); if(!field) throw new Error(`missing ${name}`); field.value=value; field.dispatchEvent(new Event('input',{bubbles:true})); field.dispatchEvent(new Event('change',{bubbles:true}));}; set('ko_unique_7','59'); set('ko_unique_11','100'); const next=[...document.querySelectorAll('button')].filter(e=>e.textContent.trim()==='Next'&&!e.disabled).at(-1); if(!next) throw new Error('Bulk Images and Price Next button missing'); next.click(); return true; })()" + }, + { + "action": "wait", + "seconds": 2 + }, + { + "action": "evaluate", + "script": "(() => { const button=[...document.querySelectorAll('button')].filter(e=>e.textContent.trim()==='Generate Products'&&!e.disabled).at(-1); if(!button) throw new Error('Generate Products button missing'); button.click(); return true; })()" + }, + { + "action": "wait", + "seconds": 5 + }, + { + "action": "evaluate", + "script": "(() => { const button=document.querySelector('#save-button')||[...document.querySelectorAll('button')].find(e=>e.offsetParent!==null&&e.textContent.trim()==='Save'); if(!button) throw new Error('parent Save button missing'); button.click(); return true; })()" + }, + { + "action": "wait", + "seconds": 8 + }, + { + "action": "get_text", + "selector": "main" + } + ], + "evaluation_input": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "expert_repair_spec", + "candidate_id": "expert-repair-c-547", + "repair_spec_sha256": "ea34996acd3f137da912dcc386858c2ab51b86514cacc23502d1448224bbb830", + "details": { + "kind": "reviewed_task_contract_and_existing_browser_evidence", + "task_revision": 2 + }, + "repair_basis": { + "root_cause": "Brown already exists as global color option 51; prior attempts created or edited a child but did not save the parent configurable-product matrix.", + "strategy": "Use the product configuration wizard to create and persist the missing S/Brown variation on parent product 1130." + } + }, + "verification": { + "browsers": [ + "chromium" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "corrected_replay_contract", + "correction_id": "wv-547-existing-brown-option-20260912", + "correction_source_sha256": "4506c41226bbe787123484e251f89116cbaa9770dd77b2949aedcaf5cf00433a", + "corrected_task_sha256": "492d978fb60cce180734859e2547b79ad385300ec02bfd30be57c89274856fe0" + }, + "model_calls": 0, + "receipt_run_ids": [ + "2904cf117286" + ] + } + }, + { + "id": "webarena-verified-571-trajectory-1", + "task_id": 571, + "task": { + "intent": "I recently moved, my address is 231 Willow Way, Suite 100, Chicago, IL, 60601, update my information on OneStopShopping accordingly", + "intent_template_id": 165, + "revision": 2, + "sites": [ + "shopping" + ], + "start_urls": [ + "__SHOPPING__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://192.168.5.2:7770" + }, + { + "action": "click", + "selector": "a[href=\"http://host.docker.internal:7770/customer/account/\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "click", + "selector": ":nth-match(a[href=\"http://host.docker.internal:7770/customer/address/edit/id/26/\"]:visible, 1)", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "fill", + "text": "231 Willow Way", + "selector": "input[name=\"street[0]\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "fill", + "text": "Suite 100", + "selector": "input[name=\"street[1]\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "select", + "value": "23", + "selector": "select[name=\"region_id\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "fill", + "text": "Chicago", + "selector": "input[name=\"city\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "fill", + "text": "60601", + "selector": "input[name=\"postcode\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "click", + "selector": "button:visible:has-text(\"Save Address\")", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + } + ], + "evaluation_input": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "authoritative_successful_browser_use_execution", + "candidate_id": "gpt-5.6-sol__chromium__task-571", + "model": "gpt-5.6-sol", + "browser": "chromium", + "execution": 1, + "events_sha256": "5d5af474be266a7f6f5fa383f91f25caae22e13e9ffb7f7392cec2d83d8d40ab", + "response_sha256": "db765f7e78b6c552e25494cde987b92c12e4d11d6990604b328f604c66bbf9c7", + "evaluation_sha256": "41e9331f4e3b80a990fb11832f215ee5834e7cbd7f336083d186a1cfa9f3b9b0" + }, + "verification": { + "browsers": [ + "chromium" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "official_task_contract" + }, + "model_calls": 0, + "receipt_run_ids": [ + "ab8cbb4dd809" + ] + } + }, + { + "id": "webarena-verified-576-trajectory-1", + "task_id": 576, + "task": { + "intent": "Add the following users to repo a11y-webring.club as developer: \"abisubramanya27\" and \"lahwaacz\"", + "intent_template_id": 351, + "revision": 2, + "sites": [ + "gitlab" + ], + "start_urls": [ + "__GITLAB__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:8023/byteblaze/a11y-webring.club/-/project_members" + }, + { + "action": "click", + "selector": "button:visible:has-text(\"Invite members\")", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "wait", + "seconds": 1 + }, + { + "action": "evaluate", + "script": "const heading=[...document.querySelectorAll('h4')].find(e=>e.textContent.trim()==='Invite members'); const root=heading?.closest('[role=dialog],.modal,.gl-modal')||heading?.parentElement?.parentElement; const input=[...(root?.querySelectorAll('input[type=text]')||[])].find(e=>e.placeholder!=='YYYY-MM-DD'); if(!input) throw new Error('member lookup input not found'); const setter=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set; setter.call(input,'abisubramanya27'); input.dispatchEvent(new Event('input',{bubbles:true})); return true;" + }, + { + "action": "wait", + "seconds": 2 + }, + { + "action": "evaluate", + "script": "const button=[...document.querySelectorAll('button[role=menuitem],button')].find(e=>e.offsetParent!==null&&e.textContent.includes('abisubramanya27')); if(!button) throw new Error('abisubramanya27 suggestion not found'); button.click(); return true;" + }, + { + "action": "evaluate", + "script": "const heading=[...document.querySelectorAll('h4')].find(e=>e.textContent.trim()==='Invite members'); const root=heading?.closest('[role=dialog],.modal,.gl-modal')||heading?.parentElement?.parentElement; const input=[...(root?.querySelectorAll('input[type=text]')||[])].find(e=>e.placeholder!=='YYYY-MM-DD'); if(!input) throw new Error('member lookup input not found'); const setter=Object.getOwnPropertyDescriptor(HTMLInputElement.prototype,'value').set; setter.call(input,'lahwaacz'); input.dispatchEvent(new Event('input',{bubbles:true})); return true;" + }, + { + "action": "wait", + "seconds": 2 + }, + { + "action": "evaluate", + "script": "const button=[...document.querySelectorAll('button[role=menuitem],button')].find(e=>e.offsetParent!==null&&e.textContent.includes('lahwaacz')); if(!button) throw new Error('lahwaacz suggestion not found'); button.click(); return true;" + }, + { + "action": "select", + "selector": ".modal select:visible", + "value": "30", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "click", + "selector": ":nth-match(button:visible:has-text(\"Invite\"), 3)", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "wait", + "seconds": 4 + }, + { + "action": "get_text", + "selector": "main" + } + ], + "evaluation_input": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "expert_repair_spec", + "candidate_id": "expert__task-576__invite-two-developers", + "repair_spec_sha256": "ea34996acd3f137da912dcc386858c2ab51b86514cacc23502d1448224bbb830", + "details": { + "kind": "reviewed_task_contract_and_existing_browser_evidence", + "task_revision": 2 + }, + "repair_basis": { + "root_cause": "The selected trajectory addressed the third visible text input, which was the expiration-date field after reset, then waited for a user menu that could never appear.", + "strategy": "Scope member lookup to the Invite members dialog, select each named user by text, set Developer, and submit once." + } + }, + "verification": { + "browsers": [ + "chromium" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "official_task_contract" + }, + "model_calls": 0, + "receipt_run_ids": [ + "6bd92e821d69" + ] + } + }, + { + "id": "webarena-verified-580-trajectory-1", + "task_id": 580, + "task": { + "intent": "Create a new forum with name and title \"sci_fi\", description \"A wild place for sci-fi enthusiasts\", and sidebar \"**New**, Classic, *Movies*, Post my novel, Random\"", + "intent_template_id": 7, + "revision": 2, + "sites": [ + "reddit" + ], + "start_urls": [ + "__REDDIT__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://192.168.5.2:9999" + }, + { + "action": "click", + "selector": "a[href=\"/forums\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "click", + "selector": "a[href=\"/create_forum\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "fill", + "text": "sci_fi", + "selector": "input[name=\"forum[name]\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "fill", + "text": "sci_fi", + "selector": "input[name=\"forum[title]\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "fill", + "text": "A wild place for sci-fi enthusiasts", + "selector": "textarea[name=\"forum[description]\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "fill", + "text": "**New**, Classic, *Movies*, Post my novel, Random", + "selector": "textarea[name=\"forum[sidebar]\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "click", + "selector": "button:visible:has-text(\"Create forum\")", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + } + ], + "evaluation_input": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "authoritative_successful_browser_use_execution", + "candidate_id": "gpt-5.6-sol__chromium__task-580", + "model": "gpt-5.6-sol", + "browser": "chromium", + "execution": 1, + "events_sha256": "222e9f7f4f52d31156694dee79d0f0e252ee5d86f07345ce7dc5a34144625145", + "response_sha256": "db765f7e78b6c552e25494cde987b92c12e4d11d6990604b328f604c66bbf9c7", + "evaluation_sha256": "7a80c8e435ab4e173e967882a7c6c892815a33d09cf8fab229b7147c0a0c7c0f" + }, + "verification": { + "browsers": [ + "chromium" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "official_task_contract" + }, + "model_calls": 0, + "receipt_run_ids": [ + "ab8cbb4dd809" + ] + } + }, + { + "id": "webarena-verified-585-trajectory-1", + "task_id": 585, + "task": { + "intent": "Rate my recently purchased floor lamp with 5 stars using my nickname Emma Lopez, with the summary \"Good purchase\" and review \"I like it\"", + "intent_template_id": 194, + "revision": 2, + "sites": [ + "shopping" + ], + "start_urls": [ + "__SHOPPING__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:7770/quoizel-tf9404m-grove-park-tiffany-multi-color-floor-lamp.html" + }, + { + "action": "wait", + "seconds": 2 + }, + { + "action": "click", + "selector": "a[href=\"#reviews\"]", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "wait", + "seconds": 2 + }, + { + "action": "click", + "selector": "input[name=\"ratings[4]\"][value=\"20\"]", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "fill", + "selector": "input[name=\"nickname\"]", + "text": "Emma Lopez", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "fill", + "selector": "input[name=\"title\"]", + "text": "Good purchase", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "fill", + "selector": "textarea[name=\"detail\"]", + "text": "I like it", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "click", + "selector": "button:visible:has-text(\"Submit Review\")", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "wait", + "seconds": 3 + }, + { + "action": "get_text", + "selector": "main" + } + ], + "evaluation_input": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "expert_repair_spec", + "candidate_id": "expert__task-585__five-star-review", + "repair_spec_sha256": "ea34996acd3f137da912dcc386858c2ab51b86514cacc23502d1448224bbb830", + "details": { + "kind": "reviewed_task_contract_and_existing_browser_evidence", + "task_revision": 2 + }, + "repair_basis": { + "root_cause": "The selected trajectory addressed the fifth visible rating input before the Reviews panel had rendered, making both timing and position part of the selector.", + "strategy": "Open the contract product, wait for the Reviews panel, and select the five-star value by its form value." + } + }, + "verification": { + "browsers": [ + "chromium" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "official_task_contract" + }, + "model_calls": 0, + "receipt_run_ids": [ + "13f17cee19ab" + ] + } + }, + { + "id": "webarena-verified-590-trajectory-1", + "task_id": 590, + "task": { + "intent": "Create a milestone in the current repo with title \"product launch\" for the upcoming event of product launch starting on January 16, 2023 and ending on January 30, 2023", + "intent_template_id": 339, + "revision": 2, + "sites": [ + "gitlab" + ], + "start_urls": [ + "__GITLAB__/primer/design" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://192.168.5.2:8023/primer/design" + }, + { + "action": "click", + "selector": "a[href=\"/primer/design/-/issues\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "navigate", + "url": "http://192.168.5.2:8023/primer/design/-/milestones" + }, + { + "action": "click", + "selector": "a[href=\"/primer/design/-/milestones/new\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "fill", + "text": "product launch", + "selector": "input[name=\"milestone[title]\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "fill", + "text": "2023-01-16", + "selector": "input[name=\"milestone[start_date]\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "fill", + "text": "2023-01-30", + "selector": "input[name=\"milestone[due_date]\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "click", + "selector": "button:visible:has-text(\"Create milestone\")", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + } + ], + "evaluation_input": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "authoritative_successful_browser_use_execution", + "candidate_id": "gpt-5.6-sol__chromium__task-590", + "model": "gpt-5.6-sol", + "browser": "chromium", + "execution": 1, + "events_sha256": "1ee105deb614907c0fc4ec4ce74c4a94a9cef43fa49537fec1460cea5aca4bae", + "response_sha256": "db765f7e78b6c552e25494cde987b92c12e4d11d6990604b328f604c66bbf9c7", + "evaluation_sha256": "2dd47c26e79713a107c31acd1a8336e1a159d69042938c210a323df9f905a5d6" + }, + "verification": { + "browsers": [ + "chromium" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "official_task_contract" + }, + "model_calls": 0, + "receipt_run_ids": [ + "ab8cbb4dd809" + ] + } + }, + { + "id": "webarena-verified-595-trajectory-1", + "task_id": 595, + "task": { + "intent": "Subscribe to the \"space\" forum from the page of the hottest post in that forum.", + "intent_template_id": 4, + "revision": 2, + "sites": [ + "reddit" + ], + "start_urls": [ + "__REDDIT__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://192.168.5.2:9999" + }, + { + "action": "click", + "selector": "a[href=\"/forums\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "click", + "selector": "a[href=\"/f/space\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "wait", + "timeout": 1000 + }, + { + "action": "snapshot" + }, + { + "action": "click", + "selector": "a[href=\"/f/space/69581/the-moon-saturn-and-jupiter-through-my-4-telescope-and\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "wait", + "timeout": 800 + }, + { + "action": "snapshot" + }, + { + "action": "wait", + "timeout": 1000 + }, + { + "action": "snapshot" + }, + { + "action": "evaluate", + "script": "(() => { const buttons=[...document.querySelectorAll('button')]; const b=buttons.find(x=>x.textContent.trim().startsWith('Subscribe')); if(!b) return {found:false}; b.scrollIntoView({block:'center'}); b.click(); return {found:true,text:b.textContent.trim()}; })()" + }, + { + "action": "wait", + "timeout": 800 + }, + { + "action": "evaluate", + "script": "(() => ({url:location.href, controls:[...document.querySelectorAll('button')].filter(x=>/subscribe/i.test(x.textContent)).map(x=>({text:x.textContent.replace(/\\s+/g,' ').trim(),pressed:x.getAttribute('aria-pressed'),disabled:x.disabled,class:x.className})).slice(-5)}))()" + } + ], + "evaluation_input": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "authoritative_successful_browser_use_execution", + "candidate_id": "gpt-5.6-sol__chromium__task-595", + "model": "gpt-5.6-sol", + "browser": "chromium", + "execution": 1, + "events_sha256": "e20e1093edbd1635fd2e2040539ffb9722805a18d0d4b4a73e3e79da2e1d54c2", + "response_sha256": "db765f7e78b6c552e25494cde987b92c12e4d11d6990604b328f604c66bbf9c7", + "evaluation_sha256": "c3b50a98df48306bd852e4ec59b7c4bc925093d512d625d2fe7a2ca16f7a2aca" + }, + "verification": { + "browsers": [ + "chromium" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "official_task_contract" + }, + "model_calls": 0, + "receipt_run_ids": [ + "ab8cbb4dd809" + ] + } + }, + { + "id": "webarena-verified-600-trajectory-1", + "task_id": 600, + "task": { + "intent": "Post my question with the title \"what is the recommended console to buy these days?\", in a forum where I'm likely to get an answer", + "intent_template_id": 3765, + "revision": 2, + "sites": [ + "reddit" + ], + "start_urls": [ + "__REDDIT__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:9999/submit/consoles" + }, + { + "action": "fill", + "selector": "textarea[name=\"submission[title]\"]", + "text": "what is the recommended console to buy these days?", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "click", + "selector": "button:visible:has-text(\"Create submission\")", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "wait", + "seconds": 3 + }, + { + "action": "get_text", + "selector": "main" + } + ], + "evaluation_input": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "expert_repair_spec", + "candidate_id": "expert__task-600__fresh-console-question", + "repair_spec_sha256": "ea34996acd3f137da912dcc386858c2ab51b86514cacc23502d1448224bbb830", + "details": { + "kind": "reviewed_task_contract_and_existing_browser_evidence", + "task_revision": 2 + }, + "repair_basis": { + "root_cause": "The selected trajectory navigated away while a gaming submission-form click was still completing, before it reached the intended consoles form.", + "strategy": "Load the consoles submission form directly and submit the exact title immediately." + } + }, + "verification": { + "browsers": [ + "chromium" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "official_task_contract" + }, + "model_calls": 0, + "receipt_run_ids": [ + "13f17cee19ab" + ] + } + }, + { + "id": "webarena-verified-605-trajectory-1", + "task_id": 605, + "task": { + "intent": "Find a forum focused on topics related to gaming consoles, and post my question, \"what is the recommended console to buy these days\" there", + "intent_template_id": 5, + "revision": 2, + "sites": [ + "reddit" + ], + "start_urls": [ + "__REDDIT__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://host.docker.internal:9999/submit/consoles" + }, + { + "action": "fill", + "selector": "textarea[name=\"submission[title]\"]", + "text": "what is the recommended console to buy these days", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "click", + "selector": "button:visible:has-text(\"Create submission\")", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "wait", + "seconds": 3 + }, + { + "action": "get_text", + "selector": "main" + } + ], + "evaluation_input": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "expert_repair_spec", + "candidate_id": "expert__task-605__fresh-console-submission", + "repair_spec_sha256": "ea34996acd3f137da912dcc386858c2ab51b86514cacc23502d1448224bbb830", + "details": { + "kind": "reviewed_task_contract_and_existing_browser_evidence", + "task_revision": 2 + }, + "repair_basis": { + "root_cause": "The replay submitted the correct title and forum but the site returned the form with HTTP 200 instead of the successful 302 redirect; same-task historical evidence identifies an invalid CSRF token.", + "strategy": "Load a fresh forum-bound submission form and submit immediately, with no stale intermediate page or duplicate-click retry." + } + }, + "verification": { + "browsers": [ + "chromium" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "official_task_contract" + }, + "model_calls": 0, + "receipt_run_ids": [ + "13f17cee19ab" + ] + } + }, + { + "id": "webarena-verified-610-trajectory-1", + "task_id": 610, + "task": { + "intent": "Post a review of my recent reading, titled \"To Kill a Mockingbird by Harper Lee\" in f/books and then comment \"good book!\" on the created post", + "intent_template_id": 9, + "revision": 2, + "sites": [ + "reddit" + ], + "start_urls": [ + "__REDDIT__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://192.168.5.2:9999" + }, + { + "action": "click", + "selector": "a[href=\"/submit\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "wait", + "timeout": 1000 + }, + { + "action": "snapshot" + }, + { + "action": "fill", + "text": "To Kill a Mockingbird by Harper Lee", + "selector": "textarea[name=\"submission[title]\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "select", + "value": "10037", + "selector": "select[name=\"submission[forum]\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "click", + "selector": "button:visible:has-text(\"Create submission\")", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "wait", + "timeout": 1000 + }, + { + "action": "snapshot" + }, + { + "action": "wait", + "timeout": 1500 + }, + { + "action": "snapshot" + }, + { + "action": "fill", + "text": "good book!", + "selector": "textarea[name=\"reply_to_submission_2[comment]\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "click", + "selector": "button:visible:has-text(\"Post\")", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "wait", + "timeout": 1200 + }, + { + "action": "snapshot" + } + ], + "evaluation_input": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "authoritative_successful_browser_use_execution", + "candidate_id": "gpt-5.6-sol__moli__task-610", + "model": "gpt-5.6-sol", + "browser": "moli", + "execution": 1, + "events_sha256": "50e3008fd411dc269b2ecbc3e51cc86745607e3b90b22e34585316d6060ad750", + "response_sha256": "db765f7e78b6c552e25494cde987b92c12e4d11d6990604b328f604c66bbf9c7", + "evaluation_sha256": "5344844f668e97c98d7a37f0bb4d4bba1f9d556fe63f5dad3c57053785239c94" + }, + "verification": { + "browsers": [ + "chromium" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "official_task_contract" + }, + "model_calls": 0, + "receipt_run_ids": [ + "ab8cbb4dd809" + ] + } + }, + { + "id": "webarena-verified-615-trajectory-1", + "task_id": 615, + "task": { + "intent": "Re-post the image of Bald Eagle from this forum to connecticut forum using the image URL and title \"from /f/pics\"", + "intent_template_id": 11, + "revision": 2, + "sites": [ + "reddit" + ], + "start_urls": [ + "__REDDIT__/f/pics" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://192.168.5.2:9999/f/pics" + }, + { + "action": "click", + "selector": "a[href=\"/f/pics/110715/amazing-shot-of-a-blue-jay-pestering-a-bald-eagle\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "wait", + "timeout": 1000 + }, + { + "action": "snapshot" + }, + { + "action": "evaluate", + "script": "() => document.querySelector('main article header h1 a').href" + }, + { + "action": "navigate", + "url": "http://192.168.5.2:9999/submit/connecticut" + }, + { + "action": "fill", + "text": "http://192.168.5.2:9999/submission_images/b02113033af32feae9ff147dbbe3764039368d67d193885bd04e65c2e6beea9c.jpg", + "selector": "input[name=\"submission[url]\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "fill", + "text": "from /f/pics", + "selector": "textarea[name=\"submission[title]\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "click", + "selector": "button:visible:has-text(\"Create submission\")", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "wait", + "timeout": 1000 + }, + { + "action": "snapshot" + } + ], + "evaluation_input": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "authoritative_successful_browser_use_execution", + "candidate_id": "gpt-5.6-sol__chromium__task-615", + "model": "gpt-5.6-sol", + "browser": "chromium", + "execution": 1, + "events_sha256": "e2f0d470e6df98f3b360d52e9563929ecc5250a328c07389c01602941752c1ca", + "response_sha256": "db765f7e78b6c552e25494cde987b92c12e4d11d6990604b328f604c66bbf9c7", + "evaluation_sha256": "0070d0cdcd78359932c360ac08d67e1712b110bac231537a15f589c2775fe430" + }, + "verification": { + "browsers": [ + "chromium" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "official_task_contract" + }, + "model_calls": 0, + "receipt_run_ids": [ + "ab8cbb4dd809" + ] + } + }, + { + "id": "webarena-verified-620-trajectory-1", + "task_id": 620, + "task": { + "intent": "Ask for advice in a forum for relations. Title it \"deal with long-distance relationships\" and in post details ask \"Please help\"", + "intent_template_id": 12, + "revision": 2, + "sites": [ + "reddit" + ], + "start_urls": [ + "__REDDIT__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://192.168.5.2:9999" + }, + { + "action": "click", + "selector": "a[href=\"/forums\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "click", + "selector": "a[href=\"/f/relationship_advice\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "click", + "selector": "a[href=\"/submit/relationship_advice\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "fill", + "text": "deal with long-distance relationships", + "selector": "textarea[name=\"submission[title]\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "fill", + "text": "Please help", + "selector": "textarea[name=\"submission[body]\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "click", + "selector": "button:visible:has-text(\"Create submission\")", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "wait", + "timeout": 1000 + }, + { + "action": "snapshot" + } + ], + "evaluation_input": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "authoritative_successful_browser_use_execution", + "candidate_id": "gpt-5.6-sol__chromium__task-620", + "model": "gpt-5.6-sol", + "browser": "chromium", + "execution": 1, + "events_sha256": "204ddc080fb36e360fa05fb3d0ae3893d0a5d39e996be28427c0bf930b9ead71", + "response_sha256": "db765f7e78b6c552e25494cde987b92c12e4d11d6990604b328f604c66bbf9c7", + "evaluation_sha256": "277471d3dc29f6bf937e6b8cc3b6c3bc9dacfb3bdcb0c8eb34478efbf1e11625" + }, + "verification": { + "browsers": [ + "chromium" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "official_task_contract" + }, + "model_calls": 0, + "receipt_run_ids": [ + "ab8cbb4dd809" + ] + } + }, + { + "id": "webarena-verified-625-trajectory-1", + "task_id": 625, + "task": { + "intent": "Create a discussion post titled \"the effectiveness of deep learning\" in a relevant forum and ask users for their opinions with the simple prompt, \"your opinion\"", + "intent_template_id": 13, + "revision": 2, + "sites": [ + "reddit" + ], + "start_urls": [ + "__REDDIT__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://192.168.5.2:9999" + }, + { + "action": "click", + "selector": "a[href=\"/forums\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "click", + "selector": "a[href=\"/submit\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "snapshot" + }, + { + "action": "fill", + "text": "the effectiveness of deep learning", + "selector": "textarea[name=\"submission[title]\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "fill", + "text": "your opinion", + "selector": "textarea[name=\"submission[body]\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "select", + "value": "10043", + "selector": "select[name=\"submission[forum]\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "click", + "selector": "button:visible:has-text(\"Create submission\")", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "get_text", + "selector": "main" + } + ], + "evaluation_input": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "authoritative_successful_browser_use_execution", + "candidate_id": "gpt-6-astra__chromium__task-625", + "model": "gpt-6-astra", + "browser": "chromium", + "execution": 1, + "events_sha256": "c08cd4306a8fc224457b2459d5fdb9f9778077d9ed063d6f490da28bd67fd8d1", + "response_sha256": "db765f7e78b6c552e25494cde987b92c12e4d11d6990604b328f604c66bbf9c7", + "evaluation_sha256": "2d19a291c0e283b3b5b217792cb5c88438a86d30f8a34a0db9259a4eaff3e417" + }, + "verification": { + "browsers": [ + "chromium" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "official_task_contract" + }, + "model_calls": 0, + "receipt_run_ids": [ + "ab8cbb4dd809" + ] + } + }, + { + "id": "webarena-verified-630-trajectory-1", + "task_id": 630, + "task": { + "intent": "Create a post in f/headphones. Title it \"noise-cancelling headphones recommendations?\" and in post details ask \"Hey, looking for good noise-cancelling headphones under $200, any suggestions?\"", + "intent_template_id": 15, + "revision": 2, + "sites": [ + "reddit" + ], + "start_urls": [ + "__REDDIT__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://192.168.5.2:9999" + }, + { + "action": "click", + "selector": "a[href=\"/submit\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "fill", + "text": "noise-cancelling headphones recommendations?", + "selector": "textarea[name=\"submission[title]\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "fill", + "text": "Hey, looking for good noise-cancelling headphones under $200, any suggestions?", + "selector": "textarea[name=\"submission[body]\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "select", + "value": "10050", + "selector": "select[name=\"submission[forum]\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "click", + "selector": "button:visible:has-text(\"Create submission\")", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + } + ], + "evaluation_input": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "authoritative_successful_browser_use_execution", + "candidate_id": "gpt-5.6-sol__chromium__task-630", + "model": "gpt-5.6-sol", + "browser": "chromium", + "execution": 1, + "events_sha256": "5e0ccee840b3ebef3cc79ffa0ec1c38adf35d1937b1e8ab184b129369d377006", + "response_sha256": "db765f7e78b6c552e25494cde987b92c12e4d11d6990604b328f604c66bbf9c7", + "evaluation_sha256": "87bae2194fce345b062084a9a4967f3b15b6462e8201eec4344502abe1056499" + }, + "verification": { + "browsers": [ + "chromium" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "official_task_contract" + }, + "model_calls": 0, + "receipt_run_ids": [ + "ab8cbb4dd809" + ] + } + }, + { + "id": "webarena-verified-635-trajectory-1", + "task_id": 635, + "task": { + "intent": "Create a post in the most appropriate forum. Title it \"noise-cancelling headphones recommendations\" and in post details ask \"I need recommendations for noise-cancelling headphones within a budget of $200 please\"", + "intent_template_id": 6100, + "revision": 2, + "sites": [ + "reddit" + ], + "start_urls": [ + "__REDDIT__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://192.168.5.2:9999" + }, + { + "action": "click", + "selector": "a[href=\"/forums\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "fill", + "text": "headphones", + "selector": "input[name=\"q\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "press", + "key": "Enter" + }, + { + "action": "snapshot" + }, + { + "action": "wait", + "timeout": 1000 + }, + { + "action": "snapshot" + }, + { + "action": "click", + "selector": "a[href=\"/submit\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "fill", + "text": "noise-cancelling headphones recommendations", + "selector": "textarea[name=\"submission[title]\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "fill", + "text": "I need recommendations for noise-cancelling headphones within a budget of $200 please", + "selector": "textarea[name=\"submission[body]\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "select", + "value": "10050", + "selector": "select[name=\"submission[forum]\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "click", + "selector": "button:visible:has-text(\"Create submission\")", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + } + ], + "evaluation_input": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "authoritative_successful_browser_use_execution", + "candidate_id": "gpt-5.6-sol__chromium__task-635", + "model": "gpt-5.6-sol", + "browser": "chromium", + "execution": 1, + "events_sha256": "48637fa9879333955635b0fb06c772f4651dcb2cdbb0478f4cc3d3767872c802", + "response_sha256": "db765f7e78b6c552e25494cde987b92c12e4d11d6990604b328f604c66bbf9c7", + "evaluation_sha256": "5d8aa27f8653fcc6967b375ad325281a67db4aaeea5db831d9a996b3fbcb881d" + }, + "verification": { + "browsers": [ + "chromium" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "official_task_contract" + }, + "model_calls": 0, + "receipt_run_ids": [ + "ab8cbb4dd809" + ] + } + }, + { + "id": "webarena-verified-640-trajectory-1", + "task_id": 640, + "task": { + "intent": "Post a notice in f/books titled \"book reading Meet up!\". Set post details to \"virtual meetup for book reading on March 15th\"", + "intent_template_id": 16, + "revision": 2, + "sites": [ + "reddit" + ], + "start_urls": [ + "__REDDIT__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://192.168.5.2:9999" + }, + { + "action": "click", + "selector": "a[href=\"/forums\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "click", + "selector": "a[href=\"/f/books\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "click", + "selector": "a[href=\"/submit/books\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "fill", + "text": "book reading Meet up!", + "selector": "textarea[name=\"submission[title]\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "fill", + "text": "virtual meetup for book reading on March 15th", + "selector": "textarea[name=\"submission[body]\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "click", + "selector": "button:visible:has-text(\"Create submission\")", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "wait", + "timeout": 1000 + }, + { + "action": "get_text", + "selector": "main" + } + ], + "evaluation_input": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "authoritative_successful_browser_use_execution", + "candidate_id": "gpt-5.6-sol__chromium__task-640", + "model": "gpt-5.6-sol", + "browser": "chromium", + "execution": 1, + "events_sha256": "1f257cfc9f15b416694d7a34273efbc0c35e86ee64c44a5af1593a13dfe5cde7", + "response_sha256": "db765f7e78b6c552e25494cde987b92c12e4d11d6990604b328f604c66bbf9c7", + "evaluation_sha256": "8117560e00cdf1162583df6b99216f498a97adf7a72362833c4f8ec4ebf91cd3" + }, + "verification": { + "browsers": [ + "chromium" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "official_task_contract" + }, + "model_calls": 0, + "receipt_run_ids": [ + "ab8cbb4dd809" + ] + } + }, + { + "id": "webarena-verified-645-trajectory-1", + "task_id": 645, + "task": { + "intent": "Post in books forum with title \"What could machine learning help the books field?\"", + "intent_template_id": 19, + "revision": 2, + "sites": [ + "reddit" + ], + "start_urls": [ + "__REDDIT__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://192.168.5.2:9999" + }, + { + "action": "click", + "selector": "a[href=\"/forums\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "click", + "selector": "a[href=\"/f/books\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "click", + "selector": "a[href=\"/submit/books\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "fill", + "text": "What could machine learning help the books field?", + "selector": "textarea[name=\"submission[title]\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "click", + "selector": "button:visible:has-text(\"Create submission\")", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "wait", + "timeout": 1000 + }, + { + "action": "get_text", + "selector": "body" + } + ], + "evaluation_input": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "authoritative_successful_browser_use_execution", + "candidate_id": "gpt-5.6-sol__chromium__task-645", + "model": "gpt-5.6-sol", + "browser": "chromium", + "execution": 1, + "events_sha256": "a28a348cdd16c83168e329e3c5f530a3cdb125886d9ac4b657c94fea9c305537", + "response_sha256": "db765f7e78b6c552e25494cde987b92c12e4d11d6990604b328f604c66bbf9c7", + "evaluation_sha256": "b81ccdba6f6f003d977438ed3ed8cbdcee271db6189007988e5170fdfe6ad8f0" + }, + "verification": { + "browsers": [ + "chromium" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "official_task_contract" + }, + "model_calls": 0, + "receipt_run_ids": [ + "ab8cbb4dd809" + ] + } + }, + { + "id": "webarena-verified-658-trajectory-1", + "task_id": 658, + "task": { + "intent": "Create an issue in a11yproject repo with title \"401 bad gateway\". Assign the issue to Roshanjossey (you can invite users to the project if needed). Set due date to be the end of 2030", + "intent_template_id": 327, + "revision": 2, + "sites": [ + "gitlab" + ], + "start_urls": [ + "__GITLAB__" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://192.168.5.2:8023" + }, + { + "action": "click", + "selector": "a[href=\"/a11yproject/a11yproject.com/-/issues\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "click", + "selector": "a[href=\"/a11yproject/a11yproject.com/-/issues/new\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "fill", + "text": "401 bad gateway", + "selector": "input[name=\"issue[title]\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "fill", + "text": "2030-12-31", + "selector": "input[name=\"issue[due_date]\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "click", + "selector": "button:visible:has-text(\"Unassigned\")", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "click", + "selector": ":nth-match(a[href=\"#\"]:visible, 4)", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "click", + "selector": "button:visible:has-text(\"Create issue\")", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + } + ], + "evaluation_input": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "authoritative_successful_browser_use_execution", + "candidate_id": "gpt-5.6-sol__chromium__task-658", + "model": "gpt-5.6-sol", + "browser": "chromium", + "execution": 1, + "events_sha256": "fff5eb90cdaf06e9dd9dededcb99a11747d1e02a50d7c9c79f116b90fa74396e", + "response_sha256": "db765f7e78b6c552e25494cde987b92c12e4d11d6990604b328f604c66bbf9c7", + "evaluation_sha256": "da32ff219fba62d6b783eaae0aa173fe6fe027233aa883f24bfc42603ab0963e" + }, + "verification": { + "browsers": [ + "chromium" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "official_task_contract" + }, + "model_calls": 0, + "receipt_run_ids": [ + "ab8cbb4dd809" + ] + } + }, + { + "id": "webarena-verified-666-trajectory-1", + "task_id": 666, + "task": { + "intent": "Submit a merge request for dialog-component in the current repository to be merged into dialog branch, assign Primer as the reviewer", + "intent_template_id": 335, + "revision": 2, + "sites": [ + "gitlab" + ], + "start_urls": [ + "__GITLAB__/primer/design" + ] + }, + "tool_calls": [ + { + "action": "navigate", + "url": "http://192.168.5.2:8023/primer/design" + }, + { + "action": "click", + "selector": "a[href=\"/primer/design/-/merge_requests\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "click", + "selector": "a[href=\"/primer/design/-/merge_requests/new\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "click", + "selector": "button:visible:has-text(\"Select source branch\")", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "click", + "selector": ":nth-match(a[href=\"#\"]:visible, 11)", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "click", + "selector": "button:visible:has-text(\"main\")", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "click", + "selector": ":nth-match(a[href=\"#\"]:visible, 10)", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "click", + "selector": "button:visible:has-text(\"Compare branches and continue\")", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "click", + "selector": ":nth-match(button:visible:has-text(\"Unassigned\"), 2)", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "click", + "selector": ":nth-match(a[href=\"#\"]:visible, 5)", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "click", + "selector": "button:visible:has-text(\"Create merge request\")", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "wait", + "timeout": 1000 + }, + { + "action": "get_text", + "selector": "body" + } + ], + "evaluation_input": { + "task_type": "MUTATE", + "status": "SUCCESS", + "retrieved_data": null, + "error_details": null + }, + "source": { + "kind": "authoritative_successful_browser_use_execution", + "candidate_id": "gpt-5.6-sol__chromium__task-666", + "model": "gpt-5.6-sol", + "browser": "chromium", + "execution": 1, + "events_sha256": "b6ffaebf0c4c1eddb26c26f11e11ab9a6622c73d67ce052dee7949b0e76e124e", + "response_sha256": "db765f7e78b6c552e25494cde987b92c12e4d11d6990604b328f604c66bbf9c7", + "evaluation_sha256": "4f2b0895d07fa0d02ba721cf98d7cac5fd783ffa1921fb3bf2267074d1ef3620" + }, + "verification": { + "browsers": [ + "chromium" + ], + "process_status": "pass", + "evaluator_status": "pass", + "contract": { + "kind": "official_task_contract" + }, + "model_calls": 0, + "receipt_run_ids": [ + "ab8cbb4dd809" + ] + } + }, + { + "id": "webarena-verified-676-trajectory-1", + "task_id": 676, + "task": { + "intent": "Go to the list of orders that are suspected of being fraudulent", + "intent_template_id": 253, + "revision": 2, + "sites": [ + "shopping_admin" + ], + "start_urls": [ + "__SHOPPING_ADMIN__" + ] + }, + "tool_calls": [ { - "action": "wait", - "seconds": 1 + "action": "navigate", + "url": "http://host.docker.internal:7780/admin/sales/order/" }, { "action": "evaluate", - "script": "const title=document.querySelector('[data-index=\"content\"] > .fieldset-wrapper-title'); if(!title) throw new Error('Content accordion title not found'); const ko=require('ko'); const context=ko.contextFor(title); const viewModel=context&&context.$data; if(!viewModel||typeof viewModel.toggleOpened!=='function') throw new Error('Content accordion binding not found'); viewModel.toggleOpened(); return typeof viewModel.opened==='function'?viewModel.opened():true;" + "script": "const button=document.querySelector('button[data-action=\"grid-filter-expand\"]'); if(!button) throw new Error('filters button not found'); button.click(); return true;" }, { "action": "wait", - "seconds": 3 - }, - { - "action": "evaluate", - "script": "const fields=[...document.querySelectorAll('textarea')].filter(el=>el.name==='short_description'||el.name==='product[short_description]'); if(fields.length!==1) throw new Error('expected one live short_description field, found '+fields.length); const field=fields[0]; field.value='

    1 customer(s) love it!

    '; field.dispatchEvent(new Event('input',{bubbles:true})); field.dispatchEvent(new Event('change',{bubbles:true})); return {name:field.name,value:field.value};" + "seconds": 1 }, { "action": "evaluate", - "script": "const form=require('uiRegistry').get('product_form.product_form'); const source=form?.source; if(!source?.client||typeof source.client._save!=='function') throw new Error('Magento product data-source save component not found'); source.set('data.product.short_description','

    1 customer(s) love it!

    '); setTimeout(()=>source.client._save(source.get('data'),{redirect:false,ajaxSave:false,attributes:{id:form.namespace}}),500); return {component:source.name,action:'save'};" + "script": "for(const name of ['created_at[from]','created_at[to]']){const input=document.querySelector(`input[name=\"${name}\"]`); if(input){input.value=''; input.dispatchEvent(new Event('input',{bubbles:true})); input.dispatchEvent(new Event('change',{bubbles:true}));}} const select=document.querySelector('select[name=\"status\"]'); if(!select) throw new Error('status filter not found'); select.value='fraud'; select.dispatchEvent(new Event('input',{bubbles:true})); select.dispatchEvent(new Event('change',{bubbles:true})); const button=document.querySelector('button[data-action=\"grid-filter-apply\"]'); if(!button) throw new Error('apply filters button not found'); button.click(); return true;" }, { "action": "wait", - "seconds": 3 + "seconds": 1 } ], "evaluation_input": { - "task_type": "MUTATE", + "task_type": "NAVIGATE", "status": "SUCCESS", "retrieved_data": null, "error_details": null }, "source": { "kind": "expert_repair_spec", - "candidate_id": "expert__task-464__short-description", - "repair_spec_sha256": "3f8ef291ad3ed92ecef3469763a93a1aa1a53967116202c73f09a9816ea39832", + "candidate_id": "expert__task-676__fraud-orders", + "repair_spec_sha256": "ea34996acd3f137da912dcc386858c2ab51b86514cacc23502d1448224bbb830", "details": { "task_revision": 2, - "evidence": "Official task contract and seeded product-review records for product 1796" + "contract_correction_id": "wv-676-match-filter-xhr-20260911", + "evidence": "Historical successful Moli filter interaction and corrected XHR-selection contract" }, "repair_basis": { - "root_cause": "The autonomous runs edited the long Page Builder description instead of the product short_description field required by the evaluator.", - "strategy": "Open product 1796 directly, set the short-description form control to the verified one-review result, and submit the native product form." + "root_cause": "The upstream evaluator selected the last document navigation instead of the matching Magento grid XHR and also required a browser-specific Referer.", + "strategy": "Open the orders grid directly and drive its stable filter controls to emit the fraud-status MUI request." } }, "verification": { "browsers": [ - "chromium", - "moli" + "chromium" ], "process_status": "pass", "evaluator_status": "pass", "contract": { - "kind": "official_task_contract" + "kind": "corrected_replay_contract", + "correction_id": "wv-676-match-filter-xhr-20260911", + "correction_source_sha256": "9a5f437d646f8812f3e2d2b31834318de9aca4844e39ff03393a24209e6169e2", + "corrected_task_sha256": "45cc633ad4673ed043af49d4d27989e1cf70c7ab96b4080f26f6ffa9c6cf887e" }, "model_calls": 0, "receipt_run_ids": [ - "2c8348e892f3" + "2904cf117286" ] } }, { - "id": "webarena-verified-470-trajectory-1", - "task_id": 470, + "id": "webarena-verified-694-trajectory-1", + "task_id": 694, "task": { - "intent": "Cancel order 302", - "intent_template_id": 257, + "intent": "Add a simple product named \"Energy-Bulk Women Shirt\" with 50 in stock, available in size S and color blue, priced at $60 using the appropriate attribute set.", + "intent_template_id": 256, "revision": 2, "sites": [ "shopping_admin" @@ -530,11 +7003,15 @@ "tool_calls": [ { "action": "navigate", - "url": "http://host.docker.internal:7780/admin/sales/order/view/order_id/302/" + "url": "http://host.docker.internal:7780/admin/catalog/product/new/set/9/type/simple/" + }, + { + "action": "wait", + "seconds": 4 }, { "action": "evaluate", - "script": "const button=document.querySelector('#order-view-cancel-button'); if(!button) throw new Error('cancel button not found'); button.click(); return true;" + "script": "(() => { const values={'product[name]':'Energy-Bulk Women Shirt','product[sku]':'energy-bulk-women-shirt','product[price]':'60','product[quantity_and_stock_status][qty]':'50','product[quantity_and_stock_status][is_in_stock]':'1','product[weight]':'1','product[size]':'167','product[color]':'50'}; for(const [name,value] of Object.entries(values)){const el=document.querySelector(`[name=\"${name}\"]`); if(!el) throw new Error(`missing field ${name}`); el.value=value; el.dispatchEvent(new Event('input',{bubbles:true})); el.dispatchEvent(new Event('change',{bubbles:true}));} const status=document.querySelector('input[name=\"product[status]\"]'); if(status && !status.checked) status.click(); return Object.fromEntries(Object.keys(values).map(name=>[name,document.querySelector(`[name=\"${name}\"]`).value])); })()" }, { "action": "wait", @@ -542,11 +7019,15 @@ }, { "action": "evaluate", - "script": "const button=document.querySelector('.modal-popup._show .action-primary.action-accept'); if(!button) throw new Error('confirmation button not found'); button.click(); return true;" + "script": "(() => { const button=document.querySelector('#save-button'); if(!button) throw new Error('save button missing'); button.click(); return true; })()" }, { "action": "wait", - "seconds": 3 + "seconds": 8 + }, + { + "action": "get_text", + "selector": "main" } ], "evaluation_input": { @@ -557,21 +7038,20 @@ }, "source": { "kind": "expert_repair_spec", - "candidate_id": "expert__task-470__cancel-order-302", - "repair_spec_sha256": "3f8ef291ad3ed92ecef3469763a93a1aa1a53967116202c73f09a9816ea39832", + "candidate_id": "expert-repair-c-694", + "repair_spec_sha256": "ea34996acd3f137da912dcc386858c2ab51b86514cacc23502d1448224bbb830", "details": { - "task_revision": 2, - "evidence": "Chromium canary passed the official evaluator after Magento dependency readiness was enforced" + "kind": "reviewed_task_contract_and_existing_browser_evidence", + "task_revision": 2 }, "repair_basis": { - "root_cause": "Earlier cancellations raced Magento search readiness; Moli additionally timed out while checking actionability of the confirmation button.", - "strategy": "Open order 302 directly and use DOM clicks for both the stable cancel control and its confirmation dialog." + "root_cause": "Earlier saves raced Magento dependency readiness; Moli also timed out while waiting for actionability on fields that were already present.", + "strategy": "Open the Top attribute-set simple-product form directly, set every evaluator-bearing field through DOM events, and invoke the save button through evaluate." } }, "verification": { "browsers": [ - "chromium", - "moli" + "chromium" ], "process_status": "pass", "evaluator_status": "pass", @@ -580,28 +7060,36 @@ }, "model_calls": 0, "receipt_run_ids": [ - "2c8348e892f3" + "ab8cbb4dd809" ] } }, { - "id": "webarena-verified-475-trajectory-1", - "task_id": 475, + "id": "webarena-verified-699-trajectory-1", + "task_id": 699, "task": { - "intent": "Set up a new, empty repository with the name chatgpt_plugin", - "intent_template_id": 292, + "intent": "Create a new marketing price rule called \"spring sale\" for all registered customers that offers a 20 percent discount site-wide", + "intent_template_id": 258, "revision": 2, "sites": [ - "gitlab" + "shopping_admin" ], "start_urls": [ - "__GITLAB__" + "__SHOPPING_ADMIN__" ] }, "tool_calls": [ { "action": "navigate", - "url": "http://host.docker.internal:8023/projects/new#blank_project" + "url": "http://host.docker.internal:7780/admin/sales_rule/promo_quote/new/" + }, + { + "action": "wait", + "seconds": 4 + }, + { + "action": "evaluate", + "script": "(() => { const setValue=(selector,value)=>{const el=document.querySelector(selector);if(!el)throw new Error(`missing ${selector}`);el.value=value;el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));return el;}; setValue('input[name=name]','spring sale'); const website=document.querySelector('select[name=\"website_ids\"]'); if(!website) throw new Error('missing website_ids'); [...website.options].forEach(o=>o.selected=o.value==='1'); website.dispatchEvent(new Event('change',{bubbles:true})); const groups=document.querySelector('select[name=\"customer_group_ids\"]'); if(!groups) throw new Error('missing customer_group_ids'); [...groups.options].forEach(o=>o.selected=['1','2','3'].includes(o.value)); groups.dispatchEvent(new Event('change',{bubbles:true})); const active=document.querySelector('input[name=is_active]'); if(active && !active.checked) active.click(); return {website:[...website.selectedOptions].map(o=>o.value),groups:[...groups.selectedOptions].map(o=>o.value)}; })()" }, { "action": "wait", @@ -609,15 +7097,31 @@ }, { "action": "evaluate", - "script": "const set=(selector,value)=>{const el=document.querySelector(selector);if(!el)throw new Error(selector+' not found');el.value=value;el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));}; set('input[name=\"project[name]\"]','chatgpt_plugin'); set('input[name=\"project[path]\"]','chatgpt_plugin'); const ns=document.querySelector('select[name=\"project[namespace_id]\"]'); if(ns){ns.value='2505';ns.dispatchEvent(new Event('change',{bubbles:true}));} const readme=document.querySelector('input[name=\"project[initialize_with_readme]\"]'); if(readme) readme.checked=false; return true;" + "script": "(() => { const panel=document.querySelector('[data-index=\"actions\"]'); const title=panel?.querySelector('.admin__collapsible-title') || [...document.querySelectorAll('strong')].find(e=>e.textContent.trim()==='Actions'); if(!title) throw new Error('Actions accordion missing'); title.click(); return true; })()" + }, + { + "action": "wait", + "seconds": 2 }, { "action": "evaluate", - "script": "const form=document.querySelector('form[action$=\"/projects\"]'); if(!form) throw new Error('project form not found'); if(form.requestSubmit) form.requestSubmit(); else form.submit(); return true;" + "script": "(() => { const action=document.querySelector('select[name=simple_action]'); const discount=document.querySelector('input[name=discount_amount]'); if(!action || !discount) throw new Error(`Actions fields missing: simple_action=${!!action}, discount_amount=${!!discount}`); action.value='by_percent'; action.dispatchEvent(new Event('change',{bubbles:true})); discount.value='20'; discount.dispatchEvent(new Event('input',{bubbles:true})); discount.dispatchEvent(new Event('change',{bubbles:true})); return {action:action.value,discount:discount.value}; })()" }, { "action": "wait", - "seconds": 3 + "seconds": 1 + }, + { + "action": "evaluate", + "script": "(() => { const button=document.querySelector('#save_and_apply') || [...document.querySelectorAll('button')].find(e=>e.textContent.trim()==='Save'); if(!button) throw new Error('save button missing'); button.click(); return true; })()" + }, + { + "action": "wait", + "seconds": 8 + }, + { + "action": "get_text", + "selector": "main" } ], "evaluation_input": { @@ -628,131 +7132,105 @@ }, "source": { "kind": "expert_repair_spec", - "candidate_id": "expert__task-475__empty-project", - "repair_spec_sha256": "3f8ef291ad3ed92ecef3469763a93a1aa1a53967116202c73f09a9816ea39832", + "candidate_id": "expert-repair-c-699", + "repair_spec_sha256": "ea34996acd3f137da912dcc386858c2ab51b86514cacc23502d1448224bbb830", "details": { - "task_revision": 2, - "contract_correction_id": "wv-475-unchecked-checkbox-is-false-20260911", - "evidence": "Official task contract plus browser-standard unchecked-checkbox serialization" + "kind": "reviewed_task_contract_and_existing_browser_evidence", + "task_revision": 2 }, "repair_basis": { - "root_cause": "The upstream evaluator required an explicit zero for an unchecked checkbox even though browsers omit unchecked controls from form submissions.", - "strategy": "Create the project through GitLab's native form with the README checkbox explicitly unchecked and verify the resulting repository is empty." + "root_cause": "Prior attempts created a Catalog Price Rule; the task and evaluator require a Cart Price Rule, and all registered groups are IDs 1, 2, and 3.", + "strategy": "Open the Cart Price Rule form directly, select website 1 and registered groups 1/2/3, choose percent discount, and save." } }, "verification": { "browsers": [ - "chromium", - "moli" + "chromium" ], "process_status": "pass", "evaluator_status": "pass", "contract": { "kind": "corrected_replay_contract", - "correction_id": "wv-475-unchecked-checkbox-is-false-20260911", - "correction_source_sha256": "8a5e2150c0ed86d63042b6a01f24d825a3ae728a8db5ee218a23b4fd61e0aa39", - "corrected_task_sha256": "7dc04654e3adf67db0da4cf20bf6147f446078a9481ae8249910a231560579a1" + "correction_id": "wv-699-browser-form-array-fields-20260912", + "correction_source_sha256": "4af179b0fbd508d3aae239bf4315021bdcf8989460cae7b764df3cf3192fa0f6", + "corrected_task_sha256": "5f4c022c31dc69bcd038b3b38c6d4bcc92b1562049c5ef8a6ec22b4acac1894e" }, "model_calls": 0, "receipt_run_ids": [ - "2c8348e892f3" + "2904cf117286" ] } }, { - "id": "webarena-verified-510-trajectory-1", - "task_id": 510, + "id": "webarena-verified-714-trajectory-1", + "task_id": 714, "task": { - "intent": "Buy the best rating product from \"Home Audio Speaker\" category with at least 5 reviews and the product is least expensive. Choose any available variant.", - "intent_template_id": 216, + "intent": "Thumbs down the top 1 post ever in gadgets.", + "intent_template_id": 24, "revision": 2, "sites": [ - "shopping" + "reddit" ], "start_urls": [ - "__SHOPPING__" + "__REDDIT__" ] }, "tool_calls": [ { "action": "navigate", - "url": "http://host.docker.internal:7770/checkout/cart/" - }, - { - "action": "wait", - "seconds": 3 - }, - { - "action": "evaluate", - "script": "(() => { const quantities=[...document.querySelectorAll('input[name^=\"cart[\"][name$=\"[qty]\"]')]; if(!quantities.length) return 'cart already empty'; for(const field of quantities) field.value='0'; const form=quantities[0].form; if(!form || !/checkout\\/cart\\/updatePost\\/?$/.test(form.action)) throw new Error('cart update form missing'); form.submit(); return `submitted removal of ${quantities.length} cart item(s)`; })()" - }, - { - "action": "wait", - "seconds": 4 - }, - { - "action": "navigate", - "url": "http://host.docker.internal:7770/atlantic-technology-fs-7-0-glb-7-channel-surround-bar-gloss-black-discontinued-by-manufacturer.html" - }, - { - "action": "wait", - "seconds": 3 + "url": "http://192.168.5.2:9999" }, { "action": "click", - "selector": "#product-addtocart-button" + "selector": "a[href=\"/forums\"]:visible", + "replay_mode": "snapshot_ref_semantics" }, { - "action": "wait", - "seconds": 2 + "action": "snapshot" }, { "action": "navigate", - "url": "http://host.docker.internal:7770/checkout/cart/" + "url": "http://192.168.5.2:9999/f/gadgets" }, { - "action": "wait", - "seconds": 3 + "action": "click", + "selector": "button[aria-label=\"Sort by: Hot\"]:visible", + "replay_mode": "snapshot_ref_semantics" }, { - "action": "evaluate", - "script": "(() => { const rows=[...document.querySelectorAll('#shopping-cart-table tbody.cart.item')]; if(rows.length!==1) throw new Error(`expected one cart item, found ${rows.length}`); const name=(rows[0].querySelector('.product-item-name')?.textContent||rows[0].textContent||'').replace(/\\s+/g,' ').trim(); if(!name.includes('Atlantic Technology FS-7.0-GLB 7-channel Surround Bar')) throw new Error(`wrong cart item: ${name}`); return name; })()" + "action": "snapshot" }, { "action": "click", - "selector": "button[data-role='proceed-to-checkout']" - }, - { - "action": "wait", - "seconds": 6 + "selector": "a[href=\"/f/gadgets/top?t=day\"]:visible", + "replay_mode": "snapshot_ref_semantics" }, { - "action": "evaluate", - "script": "(() => { const shipping=document.querySelector('input[type=radio][name=ko_unique_1]') || document.querySelector('input[type=radio][value=flatrate_flatrate]'); if(shipping && !shipping.checked) shipping.click(); const next=document.querySelector('button[data-role=opc-continue]'); if(!next) throw new Error('shipping continue button missing'); next.click(); return true; })()" + "action": "snapshot" }, { - "action": "wait", - "seconds": 6 + "action": "click", + "selector": "button[aria-label=\"From: Past 24 hours\"]:visible", + "replay_mode": "snapshot_ref_semantics" }, { - "action": "evaluate", - "script": "(() => { const radio=document.querySelector('input[type=radio][value=checkmo]'); if(!radio) throw new Error('checkmo payment method missing'); if(!radio.checked) radio.click(); return true; })()" + "action": "snapshot" }, { - "action": "wait", - "seconds": 3 + "action": "click", + "selector": "a[href=\"/f/gadgets/top?t=all\"]:visible", + "replay_mode": "snapshot_ref_semantics" }, { - "action": "evaluate", - "script": "(() => { const place=[...document.querySelectorAll('button')].find(e=>e.textContent.trim()==='Place Order' && !e.disabled); if(!place) throw new Error('place order button missing'); place.click(); return true; })()" + "action": "snapshot" }, { - "action": "wait", - "seconds": 8 + "action": "click", + "selector": ":nth-match(button:visible, 5)", + "replay_mode": "snapshot_ref_semantics" }, { - "action": "get_text", - "selector": ".checkout-success" + "action": "snapshot" } ], "evaluation_input": { @@ -762,59 +7240,93 @@ "error_details": null }, "source": { - "kind": "expert_repair_spec", - "candidate_id": "expert-repair-c-510", - "repair_spec_sha256": "3f8ef291ad3ed92ecef3469763a93a1aa1a53967116202c73f09a9816ea39832", - "details": { - "kind": "reviewed_task_contract_and_existing_browser_evidence", - "task_revision": 2 - }, - "repair_basis": { - "root_cause": "The autonomous attempts checked out a different product, and the authenticated fixture starts with unrelated items in its persistent cart; the evaluator requires a one-item cart containing product 75640.", - "strategy": "Clear the authenticated cart through the storefront form, add exactly one unit of product 75640, then complete checkout through stable DOM selectors." - } + "kind": "authoritative_successful_browser_use_execution", + "candidate_id": "gpt-5.6-sol__chromium__task-714", + "model": "gpt-5.6-sol", + "browser": "chromium", + "execution": 1, + "events_sha256": "904372b39f29c0ba4f5f178b5ee8691df1efa46400012bf03fc40a7ec9a4eee7", + "response_sha256": "db765f7e78b6c552e25494cde987b92c12e4d11d6990604b328f604c66bbf9c7", + "evaluation_sha256": "38e8171b734f3e203d096bc0ddb5511b27ff7ba1ec722a11d00f6935916a804f" }, "verification": { "browsers": [ - "chromium", - "moli" + "chromium" ], "process_status": "pass", "evaluator_status": "pass", "contract": { - "kind": "corrected_replay_contract", - "correction_id": "wv-510-freeze-target-and-use-cart-dom-20260912", - "correction_source_sha256": "8a5e2150c0ed86d63042b6a01f24d825a3ae728a8db5ee218a23b4fd61e0aa39", - "corrected_task_sha256": "5fb4e1b3f5d734001981c6382a37e3a4ddfeda523f8da198e76bdc764108e7ad" + "kind": "official_task_contract" }, "model_calls": 0, "receipt_run_ids": [ - "2c8348e892f3" + "ab8cbb4dd809" ] } }, { - "id": "webarena-verified-528-trajectory-1", - "task_id": 528, + "id": "webarena-verified-719-trajectory-1", + "task_id": 719, "task": { - "intent": "Fill out the contact us form with this refund message for the phone screen protector bought March 2023: \"I need a refund. Order: . Amount: . Reason: It broke after three days of use.\" Leave the filled form ready for review and do not submit it.", - "intent_template_id": 154, + "intent": "Like all submissions created by ThetaGang_wsb in forum wallstreetbets", + "intent_template_id": 25, "revision": 2, "sites": [ - "shopping" + "reddit" ], "start_urls": [ - "__SHOPPING__" + "__REDDIT__" ] }, "tool_calls": [ { "action": "navigate", - "url": "http://host.docker.internal:7770/contact" + "url": "http://192.168.5.2:9999" }, { - "action": "evaluate", - "script": "const values={\"input[name='name']\":'Emma Lopez',\"input[name='email']\":'emma.lopez@gmail.com',\"textarea[name='comment']\":'I need a refund. Order: 000000180. Amount: $12.99. Reason: It broke after three days of use.'}; for(const [selector,value] of Object.entries(values)){const el=document.querySelector(selector);if(!el)throw new Error(selector+' not found');el.value=value;el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));} return Object.fromEntries(Object.keys(values).map(selector=>[selector,document.querySelector(selector).value]));" + "action": "fill", + "text": "ThetaGang_wsb", + "selector": "input[name=\"q\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "press", + "key": "Enter" + }, + { + "action": "snapshot" + }, + { + "action": "wait", + "timeout": 1000 + }, + { + "action": "snapshot" + }, + { + "action": "navigate", + "url": "http://192.168.5.2:9999/user/ThetaGang_wsb" + }, + { + "action": "click", + "selector": "a[href=\"/user/ThetaGang_wsb/submissions\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "click", + "selector": ":nth-match(button:visible, 2)", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "click", + "selector": ":nth-match(button:visible, 4)", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" } ], "evaluation_input": { @@ -823,81 +7335,49 @@ "retrieved_data": null, "error_details": null }, - "source": { - "kind": "expert_repair_spec", - "candidate_id": "expert__task-528__refund-form-dom", - "repair_spec_sha256": "3f8ef291ad3ed92ecef3469763a93a1aa1a53967116202c73f09a9816ea39832", - "details": { - "task_revision": 2, - "contract_correction_id": "wv-528-dom-state-without-submit-20260911", - "evidence": "Official task contract, seeded March 2023 order, and corrected final-DOM acceptance contract" - }, - "repair_basis": { - "root_cause": "The upstream contract simultaneously required an artificial POST and prohibited the real contact-form POST, contradicting the instruction to leave the form unsubmitted.", - "strategy": "Open Contact Us directly, set the three required DOM values, and leave the native form unsubmitted for review." - } - }, + "source": { + "kind": "authoritative_successful_browser_use_execution", + "candidate_id": "gpt-5.6-sol__chromium__task-719", + "model": "gpt-5.6-sol", + "browser": "chromium", + "execution": 1, + "events_sha256": "4b1c2c8a46353ef6bf8c7249a45376ad6576f248c558c93f22f08b4db9ede35c", + "response_sha256": "db765f7e78b6c552e25494cde987b92c12e4d11d6990604b328f604c66bbf9c7", + "evaluation_sha256": "039901b5db2c20877a8e3a08ca69515d3046e962afbc160c52d97656de8d36c2" + }, "verification": { "browsers": [ - "chromium", - "moli" + "chromium" ], "process_status": "pass", "evaluator_status": "pass", "contract": { - "kind": "corrected_replay_contract", - "correction_id": "wv-528-dom-state-without-submit-20260911", - "correction_source_sha256": "8a5e2150c0ed86d63042b6a01f24d825a3ae728a8db5ee218a23b4fd61e0aa39", - "corrected_task_sha256": "6d3f1296cfc7a0288a66e3278e5ec285a3eb4ca614b37f2dc0a98124de8dd7e7" + "kind": "official_task_contract" }, "model_calls": 0, "receipt_run_ids": [ - "2c8348e892f3" + "ab8cbb4dd809" ] } }, { - "id": "webarena-verified-547-trajectory-1", - "task_id": 547, + "id": "webarena-verified-725-trajectory-1", + "task_id": 725, "task": { - "intent": "Add a new color option brown to the size S of Phoebe Zipper Sweatshirt", - "intent_template_id": 252, + "intent": "DisLike all submissions created by RickyDontLoseThat in forum massachusetts", + "intent_template_id": 1510, "revision": 2, "sites": [ - "shopping_admin" + "reddit" ], "start_urls": [ - "__SHOPPING_ADMIN__" + "__REDDIT__" ] }, "tool_calls": [ { "action": "navigate", - "url": "http://host.docker.internal:7780/admin/catalog/product/edit/id/1130/" - }, - { - "action": "wait", - "seconds": 4 - }, - { - "action": "evaluate", - "script": "(() => { const button=[...document.querySelectorAll('button')].find(e=>e.textContent.trim()==='Edit Configurations'); if(!button) throw new Error('Edit Configurations button missing'); button.click(); return true; })()" - }, - { - "action": "wait", - "seconds": 2 - }, - { - "action": "evaluate", - "script": "(() => { const dialog=[...document.querySelectorAll('aside[role=dialog]')].find(e=>e.offsetParent!==null); const next=[...(dialog||document).querySelectorAll('button')].find(e=>e.textContent.trim()==='Next' && !e.disabled); if(!next) throw new Error('configuration Next button missing'); next.click(); return true; })()" - }, - { - "action": "wait", - "seconds": 2 - }, - { - "action": "evaluate", - "script": "(() => { const boxes=[...document.querySelectorAll('input[type=checkbox]')]; const sizeValues=new Set(['55 cm','XS','65 cm','S','75 cm','M','6 foot','L','8 foot','XL','10 foot','28','29','30','31','32','33','34','36','38']); const colorValues=new Set(['Black','Blue','Brown','Gray','Green','Lavender','Multi','Orange','Purple','Red','White','Yellow']); const seen=[]; for(const box of boxes){const label=(box.labels?.[0]?.textContent||'').trim(); if(!sizeValues.has(label)&&!colorValues.has(label)) continue; const should=label==='S'||label==='Brown'; if(box.checked!==should) box.click(); if(should) seen.push(label);} if(!seen.includes('S')||!seen.includes('Brown')) throw new Error(`missing Size/Color options: ${seen.join(',')}`); const next=[...document.querySelectorAll('button')].filter(e=>e.textContent.trim()==='Next'&&!e.disabled).at(-1); if(!next) throw new Error('Attribute Values Next button missing'); next.click(); return seen; })()" + "url": "http://host.docker.internal:9999/user/RickyDontLoseThat/submissions" }, { "action": "wait", @@ -905,36 +7385,12 @@ }, { "action": "evaluate", - "script": "(() => { const price=document.querySelector('#apply-single-price-radio'); const quantity=document.querySelector('#apply-single-inventory-radio'); if(!price||!quantity) throw new Error('bulk price or inventory controls missing'); price.click(); quantity.click(); return true; })()" - }, - { - "action": "wait", - "seconds": 1 - }, - { - "action": "evaluate", - "script": "(() => { const set=(name,value)=>{const field=document.querySelector(`input[name=${name}]`); if(!field) throw new Error(`missing ${name}`); field.value=value; field.dispatchEvent(new Event('input',{bubbles:true})); field.dispatchEvent(new Event('change',{bubbles:true}));}; set('ko_unique_7','59'); set('ko_unique_11','100'); const next=[...document.querySelectorAll('button')].filter(e=>e.textContent.trim()==='Next'&&!e.disabled).at(-1); if(!next) throw new Error('Bulk Images and Price Next button missing'); next.click(); return true; })()" + "script": "const article=[...document.querySelectorAll('main article')].find(e=>e.querySelector('a[href=\"/f/massachusetts\"]')); const button=article?.querySelector('form button:has(img[alt=\"down\"])'); if(!button) throw new Error('massachusetts submission downvote control not found'); button.click(); return {post:article.querySelector('h1,h2')?.textContent.trim()};" }, { "action": "wait", "seconds": 2 }, - { - "action": "evaluate", - "script": "(() => { const button=[...document.querySelectorAll('button')].filter(e=>e.textContent.trim()==='Generate Products'&&!e.disabled).at(-1); if(!button) throw new Error('Generate Products button missing'); button.click(); return true; })()" - }, - { - "action": "wait", - "seconds": 5 - }, - { - "action": "evaluate", - "script": "(() => { const button=document.querySelector('#save-button')||[...document.querySelectorAll('button')].find(e=>e.offsetParent!==null&&e.textContent.trim()==='Save'); if(!button) throw new Error('parent Save button missing'); button.click(); return true; })()" - }, - { - "action": "wait", - "seconds": 8 - }, { "action": "get_text", "selector": "main" @@ -948,117 +7404,131 @@ }, "source": { "kind": "expert_repair_spec", - "candidate_id": "expert-repair-c-547", - "repair_spec_sha256": "3f8ef291ad3ed92ecef3469763a93a1aa1a53967116202c73f09a9816ea39832", + "candidate_id": "expert__task-725__massachusetts-submissions-downvote", + "repair_spec_sha256": "ea34996acd3f137da912dcc386858c2ab51b86514cacc23502d1448224bbb830", "details": { "kind": "reviewed_task_contract_and_existing_browser_evidence", "task_revision": 2 }, "repair_basis": { - "root_cause": "Brown already exists as global color option 51; prior attempts created or edited a child but did not save the parent configurable-product matrix.", - "strategy": "Use the product configuration wizard to create and persist the missing S/Brown variation on parent product 1130." + "root_cause": "The selected trajectory started a forum navigation while the preceding forums click was still navigating and later addressed the downvote button by page position.", + "strategy": "Open the author's submissions directly, bind the target article to its massachusetts forum link, and click that article's downvote control." } }, "verification": { "browsers": [ - "chromium", - "moli" + "chromium" ], "process_status": "pass", "evaluator_status": "pass", "contract": { - "kind": "corrected_replay_contract", - "correction_id": "wv-547-existing-brown-option-20260912", - "correction_source_sha256": "8a5e2150c0ed86d63042b6a01f24d825a3ae728a8db5ee218a23b4fd61e0aa39", - "corrected_task_sha256": "492d978fb60cce180734859e2547b79ad385300ec02bfd30be57c89274856fe0" + "kind": "official_task_contract" }, "model_calls": 0, "receipt_run_ids": [ - "2c8348e892f3" + "13f17cee19ab" ] } }, { - "id": "webarena-verified-676-trajectory-1", - "task_id": 676, + "id": "webarena-verified-731-trajectory-1", + "task_id": 731, "task": { - "intent": "Go to the list of orders that are suspected of being fraudulent", - "intent_template_id": 253, + "intent": "Edit my post on Nvidia RTX 4090 by adding a line to the body that says \"EDIT: This news aged well\"", + "intent_template_id": 27, "revision": 2, "sites": [ - "shopping_admin" + "reddit" ], "start_urls": [ - "__SHOPPING_ADMIN__" + "__REDDIT__" ] }, "tool_calls": [ { "action": "navigate", - "url": "http://host.docker.internal:7780/admin/sales/order/" + "url": "http://192.168.5.2:9999" }, { - "action": "evaluate", - "script": "const button=document.querySelector('button[data-action=\"grid-filter-expand\"]'); if(!button) throw new Error('filters button not found'); button.click(); return true;" + "action": "fill", + "text": "Nvidia RTX 4090", + "selector": "input[name=\"q\"]:visible", + "replay_mode": "snapshot_ref_semantics" }, { - "action": "wait", - "seconds": 1 + "action": "press", + "key": "Enter" }, { - "action": "evaluate", - "script": "const select=document.querySelector('select[name=\"status\"]'); if(!select) throw new Error('status filter not found'); select.value='fraud'; select.dispatchEvent(new Event('input',{bubbles:true})); select.dispatchEvent(new Event('change',{bubbles:true})); const button=document.querySelector('button[data-action=\"grid-filter-apply\"]'); if(!button) throw new Error('apply filters button not found'); button.click(); return true;" + "action": "snapshot" }, { "action": "wait", - "seconds": 1 + "timeout": 1000 + }, + { + "action": "snapshot" + }, + { + "action": "click", + "selector": "a[href=\"/f/MachineLearning/1/-/edit\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "fill", + "text": "Crazy device for ML!\n\nEDIT: This news aged well", + "selector": "textarea[name=\"submission[body]\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "click", + "selector": "button:visible:has-text(\"Edit submission\")", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" } ], "evaluation_input": { - "task_type": "NAVIGATE", + "task_type": "MUTATE", "status": "SUCCESS", "retrieved_data": null, "error_details": null }, "source": { - "kind": "expert_repair_spec", - "candidate_id": "expert__task-676__fraud-orders", - "repair_spec_sha256": "3f8ef291ad3ed92ecef3469763a93a1aa1a53967116202c73f09a9816ea39832", - "details": { - "task_revision": 2, - "contract_correction_id": "wv-676-match-filter-xhr-20260911", - "evidence": "Historical successful Moli filter interaction and corrected XHR-selection contract" - }, - "repair_basis": { - "root_cause": "The upstream evaluator selected the last document navigation instead of the matching Magento grid XHR and also required a browser-specific Referer.", - "strategy": "Open the orders grid directly and drive its stable filter controls to emit the fraud-status MUI request." - } + "kind": "authoritative_successful_browser_use_execution", + "candidate_id": "gpt-5.6-sol__chromium__task-731", + "model": "gpt-5.6-sol", + "browser": "chromium", + "execution": 1, + "events_sha256": "aaa017895be2ca14837144f58e9ff75cc508e49553687fa7e6ec50ca45ef3f74", + "response_sha256": "db765f7e78b6c552e25494cde987b92c12e4d11d6990604b328f604c66bbf9c7", + "evaluation_sha256": "23d569a765d8246d28287610ae6ae6b30e562ed02c744a32157a9709deaffc17" }, "verification": { "browsers": [ - "chromium", - "moli" + "chromium" ], "process_status": "pass", "evaluator_status": "pass", "contract": { - "kind": "corrected_replay_contract", - "correction_id": "wv-676-match-filter-xhr-20260911", - "correction_source_sha256": "8a5e2150c0ed86d63042b6a01f24d825a3ae728a8db5ee218a23b4fd61e0aa39", - "corrected_task_sha256": "45cc633ad4673ed043af49d4d27989e1cf70c7ab96b4080f26f6ffa9c6cf887e" + "kind": "official_task_contract" }, "model_calls": 0, "receipt_run_ids": [ - "2c8348e892f3" + "ab8cbb4dd809" ] } }, { - "id": "webarena-verified-694-trajectory-1", - "task_id": 694, + "id": "webarena-verified-771-trajectory-1", + "task_id": 771, "task": { - "intent": "Add a simple product named \"Energy-Bulk Women Shirt\" with 50 in stock, available in size S and color blue, priced at $60 using the appropriate attribute set.", - "intent_template_id": 256, + "intent": "Approve reviews with four stars or higher to display in our store.", + "intent_template_id": 243, "revision": 2, "sites": [ "shopping_admin" @@ -1070,31 +7540,93 @@ "tool_calls": [ { "action": "navigate", - "url": "http://host.docker.internal:7780/admin/catalog/product/new/set/9/type/simple/" + "url": "http://192.168.5.2:7780/admin" }, { - "action": "wait", - "seconds": 4 + "action": "click", + "selector": ":nth-match(a[href=\"#\"]:visible, 4)", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "click", + "selector": "a[href=\"http://host.docker.internal:7780/admin/review/product/pending/\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" + }, + { + "action": "click", + "selector": "a[href=\"http://host.docker.internal:7780/admin/review/product/edit/id/353/\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "snapshot" }, { "action": "evaluate", - "script": "(() => { const values={'product[name]':'Energy-Bulk Women Shirt','product[sku]':'energy-bulk-women-shirt','product[price]':'60','product[quantity_and_stock_status][qty]':'50','product[quantity_and_stock_status][is_in_stock]':'1','product[weight]':'1','product[size]':'167','product[color]':'50'}; for(const [name,value] of Object.entries(values)){const el=document.querySelector(`[name=\"${name}\"]`); if(!el) throw new Error(`missing field ${name}`); el.value=value; el.dispatchEvent(new Event('input',{bubbles:true})); el.dispatchEvent(new Event('change',{bubbles:true}));} const status=document.querySelector('input[name=\"product[status]\"]'); if(status && !status.checked) status.click(); return Object.fromEntries(Object.keys(values).map(name=>[name,document.querySelector(`[name=\"${name}\"]`).value])); })()" + "script": "(() => { const f=document.querySelector('form'); return {checked:[...document.querySelectorAll('input[type=radio]')].map(x=>({name:x.name,value:x.value,checked:x.checked,outer:x.outerHTML})), ratings:[...document.querySelectorAll('[class*=rating]')].map(x=>({tag:x.tagName,cls:x.className,text:x.innerText,html:x.innerHTML.slice(0,500)}))}; })()" }, { - "action": "wait", - "seconds": 1 + "action": "navigate", + "url": "http://host.docker.internal:7780/admin/review/product/edit/id/352/" }, { "action": "evaluate", - "script": "(() => { const button=document.querySelector('#save-button'); if(!button) throw new Error('save button missing'); button.click(); return true; })()" + "script": "(()=>({rating:[...document.querySelectorAll('input[name^=\"ratings\"]')].find(x=>x.checked)?.id,status:document.querySelector('[name=status_id]')?.value}))()" }, { - "action": "wait", - "seconds": 8 + "action": "select", + "value": "1", + "selector": "select[name=\"status_id\"]:visible", + "replay_mode": "snapshot_ref_semantics" }, { - "action": "get_text", - "selector": "main" + "action": "click", + "selector": "button:visible:has-text(\"Save Review\")", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "navigate", + "url": "http://host.docker.internal:7780/admin/review/product/edit/id/351/" + }, + { + "action": "evaluate", + "script": "(()=>[...document.querySelectorAll('input[name^=\"ratings\"]')].find(x=>x.checked)?.id)()" + }, + { + "action": "navigate", + "url": "http://host.docker.internal:7780/admin/review/product/edit/id/349/" + }, + { + "action": "evaluate", + "script": "(()=>[...document.querySelectorAll('input[name^=\"ratings\"]')].find(x=>x.checked)?.id)()" + }, + { + "action": "navigate", + "url": "http://host.docker.internal:7780/admin/review/product/edit/id/347/" + }, + { + "action": "evaluate", + "script": "(()=>[...document.querySelectorAll('input[name^=\"ratings\"]')].find(x=>x.checked)?.id)()" + }, + { + "action": "select", + "value": "1", + "selector": "select[name=\"status_id\"]:visible", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "click", + "selector": "button:visible:has-text(\"Save Review\")", + "replay_mode": "snapshot_ref_semantics" + }, + { + "action": "navigate", + "url": "http://host.docker.internal:7780/admin/review/product/pending/" } ], "evaluation_input": { @@ -1104,22 +7636,18 @@ "error_details": null }, "source": { - "kind": "expert_repair_spec", - "candidate_id": "expert-repair-c-694", - "repair_spec_sha256": "3f8ef291ad3ed92ecef3469763a93a1aa1a53967116202c73f09a9816ea39832", - "details": { - "kind": "reviewed_task_contract_and_existing_browser_evidence", - "task_revision": 2 - }, - "repair_basis": { - "root_cause": "Earlier saves raced Magento dependency readiness; Moli also timed out while waiting for actionability on fields that were already present.", - "strategy": "Open the Top attribute-set simple-product form directly, set every evaluator-bearing field through DOM events, and invoke the save button through evaluate." - } + "kind": "authoritative_successful_browser_use_execution", + "candidate_id": "gpt-5.6-sol__moli__task-771", + "model": "gpt-5.6-sol", + "browser": "moli", + "execution": 1, + "events_sha256": "37c14caa1a034341e11a34a7bd7fccc51c509ca55a82732394f0185580719f86", + "response_sha256": "db765f7e78b6c552e25494cde987b92c12e4d11d6990604b328f604c66bbf9c7", + "evaluation_sha256": "f7d73fb103ab4d8bb64e63f13c76c053cca33fd78aa84f087e8baaf669b78818" }, "verification": { "browsers": [ - "chromium", - "moli" + "chromium" ], "process_status": "pass", "evaluator_status": "pass", @@ -1128,105 +7656,92 @@ }, "model_calls": 0, "receipt_run_ids": [ - "2c8348e892f3" + "ab8cbb4dd809" ] } }, { - "id": "webarena-verified-699-trajectory-1", - "task_id": 699, + "id": "webarena-verified-784-trajectory-1", + "task_id": 784, "task": { - "intent": "Create a new marketing price rule called \"spring sale\" for all registered customers that offers a 20 percent discount site-wide", - "intent_template_id": 258, + "intent": "Get the email address of the contributor who has the most commits to branch master in the current repository", + "intent_template_id": 316, "revision": 2, "sites": [ - "shopping_admin" + "gitlab" ], "start_urls": [ - "__SHOPPING_ADMIN__" + "__GITLAB__/CellularPrivacy/Android-IMSI-Catcher-Detector" ] }, "tool_calls": [ { "action": "navigate", - "url": "http://host.docker.internal:7780/admin/sales_rule/promo_quote/new/" + "url": "http://192.168.5.2:8023/CellularPrivacy/Android-IMSI-Catcher-Detector" }, { - "action": "wait", - "seconds": 4 + "action": "navigate", + "url": "http://192.168.5.2:8023/CellularPrivacy/Android-IMSI-Catcher-Detector/-/graphs/master/charts" }, { "action": "evaluate", - "script": "(() => { const setValue=(selector,value)=>{const el=document.querySelector(selector);if(!el)throw new Error(`missing ${selector}`);el.value=value;el.dispatchEvent(new Event('input',{bubbles:true}));el.dispatchEvent(new Event('change',{bubbles:true}));return el;}; setValue('input[name=name]','spring sale'); const website=document.querySelector('select[name=\"website_ids\"]'); if(!website) throw new Error('missing website_ids'); [...website.options].forEach(o=>o.selected=o.value==='1'); website.dispatchEvent(new Event('change',{bubbles:true})); const groups=document.querySelector('select[name=\"customer_group_ids\"]'); if(!groups) throw new Error('missing customer_group_ids'); [...groups.options].forEach(o=>o.selected=['1','2','3'].includes(o.value)); groups.dispatchEvent(new Event('change',{bubbles:true})); const active=document.querySelector('input[name=is_active]'); if(active && !active.checked) active.click(); return {website:[...website.selectedOptions].map(o=>o.value),groups:[...groups.selectedOptions].map(o=>o.value)}; })()" - }, - { - "action": "wait", - "seconds": 1 + "script": "JSON.stringify(performance.getEntriesByType('resource').map(x=>x.name).filter(x=>x.includes('graphs')||x.includes('chart')||x.includes('commit')))" }, { "action": "evaluate", - "script": "(() => { const panel=document.querySelector('[data-index=\"actions\"]'); const title=panel?.querySelector('.admin__collapsible-title') || [...document.querySelectorAll('strong')].find(e=>e.textContent.trim()==='Actions'); if(!title) throw new Error('Actions accordion missing'); title.click(); return true; })()" - }, - { - "action": "wait", - "seconds": 2 + "script": "document.documentElement.innerHTML.includes('commit_data') ? document.documentElement.innerHTML.slice(document.documentElement.innerHTML.indexOf('commit_data')-500,document.documentElement.innerHTML.indexOf('commit_data')+1000) : document.documentElement.innerHTML.match(/data-[^=]+='[^']{100,}'/g)?.slice(0,10)" }, { - "action": "evaluate", - "script": "(() => { const action=document.querySelector('select[name=simple_action]'); const discount=document.querySelector('input[name=discount_amount]'); if(!action || !discount) throw new Error(`Actions fields missing: simple_action=${!!action}, discount_amount=${!!discount}`); action.value='by_percent'; action.dispatchEvent(new Event('change',{bubbles:true})); discount.value='20'; discount.dispatchEvent(new Event('input',{bubbles:true})); discount.dispatchEvent(new Event('change',{bubbles:true})); return {action:action.value,discount:discount.value}; })()" + "action": "navigate", + "url": "http://192.168.5.2:8023/api/v4/projects/61/repository/contributors?ref=master&per_page=100" }, { - "action": "wait", - "seconds": 1 + "action": "get_text", + "selector": "pre" }, { - "action": "evaluate", - "script": "(() => { const button=document.querySelector('#save_and_apply') || [...document.querySelectorAll('button')].find(e=>e.textContent.trim()==='Save'); if(!button) throw new Error('save button missing'); button.click(); return true; })()" + "action": "navigate", + "url": "http://192.168.5.2:8023/api/v4/projects/61/repository/contributors?ref=master&per_page=100&page=2" }, { - "action": "wait", - "seconds": 8 + "action": "get_text", + "selector": "pre" }, { - "action": "get_text", - "selector": "main" + "action": "navigate", + "url": "http://192.168.5.2:8023/api/v4/projects/61/repository/contributors?ref=master&per_page=100&page=3" } ], "evaluation_input": { - "task_type": "MUTATE", + "task_type": "RETRIEVE", "status": "SUCCESS", - "retrieved_data": null, + "retrieved_data": [ + "SecUpwN@users.noreply.github.com" + ], "error_details": null }, "source": { - "kind": "expert_repair_spec", - "candidate_id": "expert-repair-c-699", - "repair_spec_sha256": "3f8ef291ad3ed92ecef3469763a93a1aa1a53967116202c73f09a9816ea39832", - "details": { - "kind": "reviewed_task_contract_and_existing_browser_evidence", - "task_revision": 2 - }, - "repair_basis": { - "root_cause": "Prior attempts created a Catalog Price Rule; the task and evaluator require a Cart Price Rule, and all registered groups are IDs 1, 2, and 3.", - "strategy": "Open the Cart Price Rule form directly, select website 1 and registered groups 1/2/3, choose percent discount, and save." - } + "kind": "authoritative_successful_browser_use_execution", + "candidate_id": "gpt-5.6-sol__chromium__task-784", + "model": "gpt-5.6-sol", + "browser": "chromium", + "execution": 1, + "events_sha256": "a8f613a4972fa09661766a1d142ce5465629d7eb70fc76fd763cefd62dae9c31", + "response_sha256": "ead8795236d5df5d6b2e57260b6a982f6811245d9522d04850865b42f9cc2ff7", + "evaluation_sha256": "753c1b34dc2b4c0578b9ef17e243ef3d5dc17b849418b5ae463ebae86cf2e7d7" }, "verification": { "browsers": [ - "chromium", - "moli" + "chromium" ], "process_status": "pass", "evaluator_status": "pass", "contract": { - "kind": "corrected_replay_contract", - "correction_id": "wv-699-browser-form-array-fields-20260912", - "correction_source_sha256": "8a5e2150c0ed86d63042b6a01f24d825a3ae728a8db5ee218a23b4fd61e0aa39", - "corrected_task_sha256": "5f4c022c31dc69bcd038b3b38c6d4bcc92b1562049c5ef8a6ec22b4acac1894e" + "kind": "official_task_contract" }, "model_calls": 0, "receipt_run_ids": [ - "2c8348e892f3" + "ab8cbb4dd809" ] } } diff --git a/tests/trajectory_replay/test_webarena_dataset.py b/tests/trajectory_replay/test_webarena_dataset.py index 9ee096a85..4c56556f8 100644 --- a/tests/trajectory_replay/test_webarena_dataset.py +++ b/tests/trajectory_replay/test_webarena_dataset.py @@ -17,6 +17,44 @@ ) +def test_repository_dataset_covers_the_current_frozen_selection() -> None: + root = Path(__file__).resolve().parents[2] + profile = json.loads( + ( + root + / "evaluation/trajectory-replay/webarena-verified/current-run.json" + ).read_text(encoding="utf-8") + ) + selection = json.loads( + (root / profile["task_selection"]["path"]).read_text(encoding="utf-8") + ) + dataset = json.loads( + ( + root + / "evaluation/trajectory-replay/webarena-verified/trajectories.json" + ).read_text(encoding="utf-8") + ) + + expected_ids = {int(item["task_id"]) for item in selection["tasks"]} + actual_ids = {int(item["task_id"]) for item in dataset["trajectories"]} + required = int(profile["qualification"]["required_task_count"]) + + assert required == 90 + assert expected_ids == actual_ids + assert dataset["counts"] == { + "verified_tasks": required, + "verified_trajectories": required, + } + for item in dataset["trajectories"]: + verification = item["verification"] + assert verification["browsers"] == [ + profile["qualification"]["reference_engine"] + ] + assert verification["process_status"] == "pass" + assert verification["evaluator_status"] == "pass" + assert verification["model_calls"] == 0 + + def _write(path: Path, value: object) -> Path: path.parent.mkdir(parents=True, exist_ok=True) path.write_text(json.dumps(value), encoding="utf-8") From 70118e9c45a71f2eb3c1ffda6bb5663ace5a96b6 Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 15 Sep 2026 04:31:14 +0800 Subject: [PATCH 110/127] Bind replay failures to reviewed root causes --- harness/trajectory_replay/webarena_review.py | 90 ++++++++++++ .../trajectory-replay-failure-review.json | 135 ++++++++++++++++++ .../trajectory_replay/test_webarena_review.py | 77 ++++++++++ 3 files changed, 302 insertions(+) create mode 100644 harness/trajectory_replay/webarena_review.py create mode 100644 research/trajectory-replay-failure-review.json create mode 100644 tests/trajectory_replay/test_webarena_review.py diff --git a/harness/trajectory_replay/webarena_review.py b/harness/trajectory_replay/webarena_review.py new file mode 100644 index 000000000..be6e2142b --- /dev/null +++ b/harness/trajectory_replay/webarena_review.py @@ -0,0 +1,90 @@ +"""Validate evidence-bound root-cause review for trajectory replay results.""" + +from __future__ import annotations + +import hashlib +import json +import re +from pathlib import Path +from typing import Any + +REVIEW_SCHEMA = "lex_webarena_trajectory_replay_failure_review/1" + + +def _sha256(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def programmatic_failure(attempt: dict[str, Any]) -> dict[str, Any]: + """Return the automatic failure location without asserting a root cause.""" + + if attempt.get("process_status") != "pass": + message = str(attempt.get("error") or "BrowserTool execution failed") + match = re.search(r"RuntimeError: step (\d+) failed: ([^\n]+)", message) + return { + "stage": "browser_tool_execution", + "step": int(match.group(1)) if match else None, + "message": match.group(2) if match else message.strip(), + } + if attempt.get("evaluator_status") != "pass": + return { + "stage": "official_evaluation", + "step": None, + "message": "Official evaluator rejected the completed trajectory", + } + return {"stage": "pass", "step": None, "message": ""} + + +def load_validated_failure_review( + results_path: str | Path, review_path: str | Path +) -> dict[str, Any]: + """Load a review only when it exactly covers the bound final failures.""" + + results_file = Path(results_path).expanduser().resolve() + review_file = Path(review_path).expanduser().resolve() + results = json.loads(results_file.read_text(encoding="utf-8")) + review = json.loads(review_file.read_text(encoding="utf-8")) + if review.get("schema") != REVIEW_SCHEMA: + raise ValueError("unsupported trajectory replay failure review") + if (review.get("bindings") or {}).get("effective_results_sha256") != _sha256( + results_file + ): + raise ValueError("failure review is not bound to the effective result bytes") + + attempts = results.get("attempts") or [] + failed = { + (str(item.get("engine") or ""), int(item["task_id"])) + for item in attempts + if item.get("status") != "pass" + } + reviewed: dict[tuple[str, int], dict[str, Any]] = {} + for group in review.get("cause_groups") or []: + required_group_fields = { + "code", + "label_zh", + "label_en", + "responsibility", + "remediation_owner", + "remediation_status", + "results", + } + if required_group_fields - set(group): + raise ValueError("failure review cause group is incomplete") + for item in group["results"]: + key = (str(item.get("engine") or ""), int(item["task_id"])) + if key in reviewed: + raise ValueError(f"duplicate failure review result: {key}") + if not str(item.get("reason_zh") or "").strip() or not str( + item.get("reason_en") or "" + ).strip(): + raise ValueError(f"failure review reason is incomplete: {key}") + reviewed[key] = {**item, "cause_group": group} + + if set(reviewed) != failed: + missing = sorted(failed - set(reviewed)) + extra = sorted(set(reviewed) - failed) + raise ValueError(f"failure review coverage mismatch: missing={missing}, extra={extra}") + coverage = review.get("coverage") or {} + if coverage != {"failed_results": len(failed), "verified_results": len(reviewed)}: + raise ValueError("failure review coverage summary is incorrect") + return {"review": review, "results": results, "by_result": reviewed} diff --git a/research/trajectory-replay-failure-review.json b/research/trajectory-replay-failure-review.json new file mode 100644 index 000000000..bab37cc50 --- /dev/null +++ b/research/trajectory-replay-failure-review.json @@ -0,0 +1,135 @@ +{ + "schema": "lex_webarena_trajectory_replay_failure_review/1", + "benchmark": "WebArena-Verified 1.2.3 · Browser Trajectory Replay", + "bindings": { + "effective_results_sha256": "abaaa0e90aced74a831093f2d6d7c3371639acb984b3a2c057f3cc39632ea935" + }, + "review_protocol": { + "unit": "one final logical result per browser and task", + "method": "compare the fixed trajectory, BrowserTool result, ordered trace, network evidence, official evaluator output, and the passing reference-browser execution", + "score_effect": "none; the official replay verdict remains unchanged", + "rule": "the programmatic failure location and reviewed root cause are separate fields" + }, + "coverage": { + "failed_results": 36, + "verified_results": 36 + }, + "cause_groups": [ + { + "code": "moli_snapshot_actionability_mismatch", + "label_zh": "快照与可操作节点不一致", + "label_en": "Snapshot/actionability mismatch", + "responsibility": "browser", + "remediation_owner": "Moli BrowserTool adapter", + "remediation_status": "open", + "results": [ + {"engine":"moli","task_id":29,"reason_zh":"Moli 快照已经列出 DIY 新帖链接,但紧接着对同一 href 的点击等待 60 秒仍找不到可操作节点;同一固定轨迹在 Chromium 通过。","reason_en":"Moli's snapshot listed the DIY new-post link, but the immediately following click on the same href could not resolve an actionable node within 60 seconds; the fixed trajectory passes in Chromium."}, + {"engine":"moli","task_id":66,"reason_zh":"Moli 快照已经列出 Books 版块链接,但随后点击同一 href 超时;同一固定轨迹在 Chromium 通过。","reason_en":"Moli's snapshot listed the Books forum link, but the following click on the same href timed out; the fixed trajectory passes in Chromium."}, + {"engine":"moli","task_id":404,"reason_zh":"Moli 快照已经列出 Books 版块链接,但随后点击同一 href 超时;同一固定轨迹在 Chromium 通过。","reason_en":"Moli's snapshot listed the Books forum link, but the following click on the same href timed out; the fixed trajectory passes in Chromium."}, + {"engine":"moli","task_id":595,"reason_zh":"Moli 快照已经列出 Space 版块链接,但随后点击同一 href 超时;同一固定轨迹在 Chromium 通过。","reason_en":"Moli's snapshot listed the Space forum link, but the following click on the same href timed out; the fixed trajectory passes in Chromium."}, + {"engine":"moli","task_id":620,"reason_zh":"Moli 快照已经列出 relationship_advice 版块链接,但随后点击同一 href 超时;同一固定轨迹在 Chromium 通过。","reason_en":"Moli's snapshot listed the relationship_advice forum link, but the following click on the same href timed out; the fixed trajectory passes in Chromium."}, + {"engine":"moli","task_id":640,"reason_zh":"Moli 快照已经列出 Books 版块链接,但随后点击同一 href 超时;同一固定轨迹在 Chromium 通过。","reason_en":"Moli's snapshot listed the Books forum link, but the following click on the same href timed out; the fixed trajectory passes in Chromium."}, + {"engine":"moli","task_id":645,"reason_zh":"Moli 快照已经列出 Books 版块链接,但随后点击同一 href 超时;同一固定轨迹在 Chromium 通过。","reason_en":"Moli's snapshot listed the Books forum link, but the following click on the same href timed out; the fixed trajectory passes in Chromium."}, + {"engine":"moli","task_id":714,"reason_zh":"Moli 快照已经列出 Gadgets 当日热门链接,但随后点击同一 href 超时;同一固定轨迹在 Chromium 通过。","reason_en":"Moli's snapshot listed the Gadgets daily-top link, but the following click on the same href timed out; the fixed trajectory passes in Chromium."} + ] + }, + { + "code": "moli_visible_target_state_divergence", + "label_zh": "目标控件未进入可见可操作状态", + "label_en": "Target control did not become actionable", + "responsibility": "browser", + "remediation_owner": "Moli rendering and BrowserTool integration", + "remediation_status": "open", + "results": [ + {"engine":"moli","task_id":108,"reason_zh":"固定轨迹到达筛选步骤后,Moli 未暴露可见的 Filters 按钮,点击超时;Chromium 同轨迹通过。","reason_en":"At the filter step, Moli did not expose an actionable visible Filters button and the click timed out; Chromium passes the same trajectory."}, + {"engine":"moli","task_id":156,"reason_zh":"Moli 未暴露固定轨迹所需的 Assigned merge requests 链接实例,点击超时;Chromium 同轨迹通过。","reason_en":"Moli did not expose the Assigned merge-requests link instance required by the fixed trajectory; Chromium passes the same trajectory."}, + {"engine":"moli","task_id":163,"reason_zh":"Moli 未暴露评论列表第二页链接,点击超时;Chromium 同轨迹通过。","reason_en":"Moli did not expose the review-list page-two link and the click timed out; Chromium passes the same trajectory."}, + {"engine":"moli","task_id":243,"reason_zh":"Moli 在菜单导航后未暴露 Customers 链接,点击超时;Chromium 同轨迹通过。","reason_en":"After menu navigation, Moli did not expose the Customers link and the click timed out; Chromium passes the same trajectory."}, + {"engine":"moli","task_id":374,"reason_zh":"Moli 未暴露 Magento Blank 主题的编辑链接,点击超时;Chromium 同轨迹通过。","reason_en":"Moli did not expose the Magento Blank theme edit link and the click timed out; Chromium passes the same trajectory."}, + {"engine":"moli","task_id":399,"reason_zh":"Moli 未暴露目标论坛用户的个人页链接,点击超时;Chromium 同轨迹通过。","reason_en":"Moli did not expose the target forum user's profile link and the click timed out; Chromium passes the same trajectory."}, + {"engine":"moli","task_id":415,"reason_zh":"Moli 未暴露可见的评论文本框,填入动作超时;Chromium 同轨迹通过。","reason_en":"Moli did not expose an actionable visible comment textarea and the fill timed out; Chromium passes the same trajectory."}, + {"engine":"moli","task_id":418,"reason_zh":"Moli 未暴露 Set status 按钮,点击超时;Chromium 同轨迹通过。","reason_en":"Moli did not expose the Set status button and the click timed out; Chromium passes the same trajectory."}, + {"engine":"moli","task_id":441,"reason_zh":"Moli 未暴露目标仓库的 index.html 文件链接,点击超时;Chromium 同轨迹通过。","reason_en":"Moli did not expose the target repository's index.html link and the click timed out; Chromium passes the same trajectory."}, + {"engine":"moli","task_id":448,"reason_zh":"Moli 未暴露 GitLab 个人资料入口,点击超时;重试仍在同一步失败,而 Chromium 同轨迹通过。","reason_en":"Moli did not expose the GitLab profile entry; the retry failed at the same step while Chromium passes the same trajectory."}, + {"engine":"moli","task_id":506,"reason_zh":"Moli 在结账流程中未暴露下一步按钮,点击超时;Chromium 同轨迹完成并通过验收。","reason_en":"Moli did not expose the Next button during checkout and the click timed out; Chromium completes and passes the same trajectory."}, + {"engine":"moli","task_id":571,"reason_zh":"Moli 未暴露可见的州/省下拉框,选择动作超时;Chromium 同轨迹通过。","reason_en":"Moli did not expose an actionable visible region selector and the selection timed out; Chromium passes the same trajectory."}, + {"engine":"moli","task_id":585,"reason_zh":"Moli 未暴露 Submit Review 按钮,点击超时;Chromium 同轨迹通过。","reason_en":"Moli did not expose the Submit Review button and the click timed out; Chromium passes the same trajectory."}, + {"engine":"moli","task_id":590,"reason_zh":"Moli 未暴露里程碑标题输入框,填入动作超时;Chromium 同轨迹通过。","reason_en":"Moli did not expose the milestone-title input and the fill timed out; Chromium passes the same trajectory."}, + {"engine":"moli","task_id":658,"reason_zh":"Moli 未暴露议题标题输入框,填入动作超时;Chromium 同轨迹通过。","reason_en":"Moli did not expose the issue-title input and the fill timed out; Chromium passes the same trajectory."}, + {"engine":"moli","task_id":731,"reason_zh":"Moli 未暴露目标论坛帖的编辑链接,点击超时;Chromium 同轨迹通过。","reason_en":"Moli did not expose the target forum post's edit link and the click timed out; Chromium passes the same trajectory."} + ] + }, + { + "code": "moli_navigation_readiness_race", + "label_zh": "导航切换与后续动作竞态", + "label_en": "Navigation/readiness race", + "responsibility": "browser", + "remediation_owner": "Moli navigation lifecycle", + "remediation_status": "open", + "results": [ + {"engine":"moli","task_id":173,"reason_zh":"前一步快照已报告导航导致执行上下文销毁,Moli 随后仍未恢复到可点击 Search 的稳定文档;Chromium 同轨迹通过。","reason_en":"The preceding snapshot reported execution-context destruction during navigation, and Moli did not recover to a stable document with an actionable Search link; Chromium passes the same trajectory."}, + {"engine":"moli","task_id":196,"reason_zh":"前一步发生导航切换后,Moli 未在下一动作前恢复 Clear all 控件,点击超时;Chromium 同轨迹通过。","reason_en":"After the preceding navigation transition, Moli did not restore the Clear all control before the next action and the click timed out; Chromium passes the same trajectory."}, + {"engine":"moli","task_id":204,"reason_zh":"前一步导航尚未形成稳定文档,Moli 未暴露目标订单链接,点击超时;Chromium 同轨迹通过。","reason_en":"The preceding navigation did not settle into a stable document in Moli, so the target order link never became actionable; Chromium passes the same trajectory."}, + {"engine":"moli","task_id":313,"reason_zh":"Moli 在按键动作执行时仍处于文档导航切换,CDP 返回 Navigation is changing the document;Chromium 同轨迹通过。","reason_en":"Moli was still changing documents when the keypress was dispatched, and CDP returned 'Navigation is changing the document'; Chromium passes the same trajectory."}, + {"engine":"moli","task_id":389,"reason_zh":"前一步快照遭遇导航上下文销毁,Moli 未恢复目标 merge request 链接,点击超时;Chromium 同轨迹通过。","reason_en":"The preceding snapshot lost its execution context to navigation, and Moli did not recover the target merge-request link; Chromium passes the same trajectory."}, + {"engine":"moli","task_id":666,"reason_zh":"前一步快照遭遇导航上下文销毁,Moli 未恢复新建 merge request 链接,点击超时;Chromium 同轨迹通过。","reason_en":"The preceding snapshot lost its execution context to navigation, and Moli did not recover the new merge-request link; Chromium passes the same trajectory."} + ] + }, + { + "code": "moli_dom_cardinality_difference", + "label_zh": "可见节点数量与参考浏览器不同", + "label_en": "Visible-node cardinality differs", + "responsibility": "browser", + "remediation_owner": "Moli DOM projection", + "remediation_status": "open", + "results": [ + {"engine":"moli","task_id":314,"reason_zh":"固定轨迹要求第二个可见的 /primer/design 链接;Moli 只暴露不足两个匹配节点,Chromium 同轨迹通过。","reason_en":"The trajectory selects the second visible /primer/design link; Moli exposes fewer than two matching nodes while Chromium passes the same trajectory."}, + {"engine":"moli","task_id":349,"reason_zh":"固定轨迹要求第二个可见的目标项目链接;Moli 只暴露不足两个匹配节点,Chromium 同轨迹通过。","reason_en":"The trajectory selects the second visible target-project link; Moli exposes fewer than two matching nodes while Chromium passes the same trajectory."} + ] + }, + { + "code": "moli_dynamic_suggestion_missing", + "label_zh": "动态候选未生成", + "label_en": "Dynamic suggestion missing", + "responsibility": "browser", + "remediation_owner": "Moli page event integration", + "remediation_status": "open", + "results": [ + {"engine":"moli","task_id":576,"reason_zh":"输入用户名前缀后,Moli 页面没有生成 abisubramanya27 候选,固定选择脚本因候选不存在而失败;Chromium 同轨迹通过。","reason_en":"After entering the username prefix, Moli did not generate the abisubramanya27 suggestion, so the fixed selection script failed; Chromium passes the same trajectory."} + ] + }, + { + "code": "moli_async_side_effect_lost", + "label_zh": "异步保存副作用未发生", + "label_en": "Asynchronous save side effect was lost", + "responsibility": "browser", + "remediation_owner": "Moli script and lifecycle integration", + "remediation_status": "open", + "results": [ + {"engine":"moli","task_id":464,"reason_zh":"七个 BrowserTool 动作均返回成功,但由 setTimeout 调度的 Magento 保存没有出现在 Moli 网络记录中,官方验收因此找不到目标 POST;Chromium 同轨迹产生该请求并通过。","reason_en":"All seven BrowserTool actions returned success, but the Magento save scheduled through setTimeout never appeared in Moli's network record, so the evaluator found no target POST; Chromium emits it and passes."} + ] + }, + { + "code": "moli_ambiguous_link_activation", + "label_zh": "同一链接选择器激活了不同目标", + "label_en": "Same link selector activated a different target", + "responsibility": "browser", + "remediation_owner": "Moli DOM ordering and click dispatch", + "remediation_status": "open", + "results": [ + {"engine":"moli","task_id":465,"reason_zh":"固定轨迹点击第三个可见 href=# 链接;Chromium 进入心愿单并产生验收所需请求,Moli 留在搜索页且没有心愿单 POST,说明可见节点顺序或点击分派不同。","reason_en":"The trajectory clicks the third visible href=# link. Chromium opens the wishlist and emits the required request, while Moli remains on the search page with no wishlist POST, showing different visible-node ordering or click dispatch."} + ] + }, + { + "code": "moli_json_document_dom_shape", + "label_zh": "JSON 文档 DOM 结构不同", + "label_en": "JSON-document DOM shape differs", + "responsibility": "browser", + "remediation_owner": "Moli document rendering", + "remediation_status": "open", + "results": [ + {"engine":"moli","task_id":784,"reason_zh":"Moli 已以 200 打开 API 且快照含完整 JSON 文本,但页面没有 Chromium 提供的 pre 包装节点,后续 get_text(pre) 超时。","reason_en":"Moli opened the API with status 200 and its snapshot contains the JSON text, but the document lacks Chromium's pre wrapper, so get_text(pre) timed out."} + ] + } + ] +} diff --git a/tests/trajectory_replay/test_webarena_review.py b/tests/trajectory_replay/test_webarena_review.py new file mode 100644 index 000000000..a6d765487 --- /dev/null +++ b/tests/trajectory_replay/test_webarena_review.py @@ -0,0 +1,77 @@ +import hashlib +import json +from pathlib import Path + +import pytest + +from harness.trajectory_replay.webarena_review import ( + load_validated_failure_review, + programmatic_failure, +) + + +def _write(path: Path, value: object) -> Path: + path.write_text(json.dumps(value), encoding="utf-8") + return path + + +def test_programmatic_failure_keeps_location_separate_from_root_cause() -> None: + result = programmatic_failure( + { + "process_status": "fail", + "error": "RuntimeError: step 7 failed: Click failed: target missing\n", + } + ) + assert result == { + "stage": "browser_tool_execution", + "step": 7, + "message": "Click failed: target missing", + } + + +def test_review_requires_exact_hash_bound_failure_coverage(tmp_path: Path) -> None: + results = _write( + tmp_path / "results.json", + { + "attempts": [ + {"engine": "chromium", "task_id": 1, "status": "pass"}, + {"engine": "moli", "task_id": 1, "status": "fail"}, + ] + }, + ) + review = _write( + tmp_path / "review.json", + { + "schema": "lex_webarena_trajectory_replay_failure_review/1", + "bindings": { + "effective_results_sha256": hashlib.sha256(results.read_bytes()).hexdigest() + }, + "coverage": {"failed_results": 1, "verified_results": 1}, + "cause_groups": [ + { + "code": "browser", + "label_zh": "浏览器差异", + "label_en": "Browser difference", + "responsibility": "browser", + "remediation_owner": "browser", + "remediation_status": "open", + "results": [ + { + "engine": "moli", + "task_id": 1, + "reason_zh": "目标不存在。", + "reason_en": "Target missing.", + } + ], + } + ], + }, + ) + loaded = load_validated_failure_review(results, review) + assert set(loaded["by_result"]) == {("moli", 1)} + + payload = json.loads(review.read_text(encoding="utf-8")) + payload["cause_groups"][0]["results"][0]["task_id"] = 2 + review.write_text(json.dumps(payload), encoding="utf-8") + with pytest.raises(ValueError, match="coverage mismatch"): + load_validated_failure_review(results, review) From 5881e932e2d9e6606fe3e3dbaeae0d94e65dd404 Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 15 Sep 2026 05:03:13 +0800 Subject: [PATCH 111/127] Add evidence-backed trajectory replay report --- Makefile | 15 +- harness/trajectory_replay/webarena_report.py | 555 ++++++++++++++++++ pyproject.toml | 1 + scripts/build_webarena_replay_report.py | 28 + scripts/check_webarena_replay_report.py | 101 ++++ scripts/check_webarena_replay_visual.py | 216 +++++++ .../trajectory_replay/test_webarena_report.py | 30 + 7 files changed, 945 insertions(+), 1 deletion(-) create mode 100644 harness/trajectory_replay/webarena_report.py create mode 100644 scripts/build_webarena_replay_report.py create mode 100644 scripts/check_webarena_replay_report.py create mode 100644 scripts/check_webarena_replay_visual.py create mode 100644 tests/trajectory_replay/test_webarena_report.py diff --git a/Makefile b/Makefile index ba7fc92a2..4b2f20a3a 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ UV ?= uv PY := $(UV) run --frozen python -.PHONY: setup runtime-check naming-check test test-python test-ui fixture-smoke ci report report-check browser-use-review browser-use-report browser-use-report-check browser-use-visual-check browser-use-release-check +.PHONY: setup runtime-check naming-check test test-python test-ui fixture-smoke ci report report-check browser-use-review browser-use-report browser-use-report-check browser-use-visual-check browser-use-release-check trajectory-replay-report trajectory-replay-report-check trajectory-replay-visual-check trajectory-replay-release-check setup: $(UV) sync --frozen --all-extras @@ -58,3 +58,16 @@ browser-use-report-check: runtime-check browser-use-release-check: browser-use-report-check $(MAKE) browser-use-visual-check + +trajectory-replay-report: runtime-check + @test -n "$(EVIDENCE)" || (echo 'EVIDENCE=/absolute/path is required' >&2; exit 2) + $(PY) scripts/build_webarena_replay_report.py --results "$(EVIDENCE)" + +trajectory-replay-report-check: runtime-check + $(PY) scripts/check_webarena_replay_report.py reports/trajectory-replay + +trajectory-replay-visual-check: runtime-check + $(PY) scripts/check_webarena_replay_visual.py reports/trajectory-replay + +trajectory-replay-release-check: trajectory-replay-report-check + $(MAKE) trajectory-replay-visual-check diff --git a/harness/trajectory_replay/webarena_report.py b/harness/trajectory_replay/webarena_report.py new file mode 100644 index 000000000..e2e33875b --- /dev/null +++ b/harness/trajectory_replay/webarena_report.py @@ -0,0 +1,555 @@ +"""Build the evidence-backed WebArena Browser Trajectory Replay report.""" + +from __future__ import annotations + +import hashlib +import html +import json +import math +import re +import shutil +import statistics +from collections import defaultdict +from pathlib import Path +from typing import Any + +from harness.trajectory_replay.webarena_review import ( + load_validated_failure_review, + programmatic_failure, +) + + +def _e(value: Any) -> str: + return html.escape(str(value if value is not None else "")) + + +def _sha256(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def _stat(values: list[float]) -> dict[str, float | int | None]: + if not values: + return {"n": 0, "mean": None, "p50": None, "p95": None} + ordered = sorted(values) + + def rank(quantile: float) -> float: + return ordered[min(len(ordered) - 1, math.ceil(quantile * len(ordered)) - 1)] + return { + "n": len(values), + "mean": statistics.fmean(values), + "p50": rank(0.5), + "p95": rank(0.95), + } + + +def _clean(value: Any) -> Any: + if isinstance(value, dict): + return { + key: _clean(item) + for key, item in value.items() + if key not in {"path", "paths"} + } + if isinstance(value, list): + return [_clean(item) for item in value] + if isinstance(value, str): + value = re.sub(r"/Users/[^\s\"']+", "[local path removed]", value) + value = re.sub(r"sk-[A-Za-z0-9_-]{12,}", "[secret removed]", value) + return value + + +def _write_json(path: Path, value: Any) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text( + json.dumps(_clean(value), ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + + +def _load_artifact(ref: dict[str, Any] | None) -> Any: + if not ref or not ref.get("path"): + return None + path = Path(str(ref["path"])).expanduser().resolve() + if not path.is_file() or _sha256(path) != ref.get("sha256"): + raise ValueError(f"evidence artifact is missing or changed: {path.name}") + return json.loads(path.read_text(encoding="utf-8")) + + +def _format_ms(value: float | None) -> str: + return "—" if value is None else f"{value / 1000:.1f} 秒" + + +def _format_mib(value: float | None) -> str: + return "—" if value is None else f"{value / 1024 / 1024:.1f} MiB" + + +def _metric_lines( + metric: dict[str, Any], + formatter: Any, + method_id: str, + winners: dict[str, set[str]], + *, + lang: str, +) -> str: + labels = (("mean", "均值"), ("p50", "P50"), ("p95", "P95")) + if lang == "en": + labels = (("mean", "Mean"), ("p50", "P50"), ("p95", "P95")) + return "
    ".join( + f'{label} ' + + _winner(formatter(metric.get(key)), method_id in winners[key], lang=lang) + for key, label in labels + ) + + +def _unique_winners( + rows: list[dict[str, Any]], key: str, *, higher: bool +) -> set[str]: + numeric = [(row["id"], row.get(key)) for row in rows if isinstance(row.get(key), (int, float))] + if not numeric: + return set() + best = (max if higher else min)(value for _, value in numeric) + winners = {name for name, value in numeric if value == best} + return winners if len(winners) == 1 else set() + + +def _nav(lang: str, active: str, *, depth: int) -> str: + labels = { + "zh": (("index", "结论"), ("dataset", "评测集"), ("results", "结果与诊断"), ("environment", "环境与方法"), ("review", "核验记录")), + "en": (("index", "Summary"), ("dataset", "Dataset"), ("results", "Results & diagnosis"), ("environment", "Environment & method"), ("review", "Review history")), + }[lang] + local_prefix = "../" * depth + switch = local_prefix + ("../index.html" if lang == "en" else "en/index.html") + switch_label = "中文" if lang == "en" else "English" + brand = "Trajectory Replay" if lang == "en" else "轨迹重放" + theme_label = "Toggle theme" if lang == "en" else "切换主题" + links = "".join( + f'{label}" + for slug, label in labels + ) + return f'
    {brand}{switch_label}
    ' + + +def _shell(lang: str, active: str, title: str, body: str, *, depth: int = 0) -> str: + prefix = "../" * depth + asset_prefix = prefix if lang == "zh" else "../" + prefix + return f'''{_e(title)}{_nav(lang, active, depth=depth)}
    {body}
    WebArena-Verified 1.2.3 · Browser Trajectory Replay
    ''' + + +def _winner(value: str, won: bool, *, lang: str) -> str: + badge = "优" if lang == "zh" else "Best" + return f'{value}{badge}' if won else value + + +def _metrics_table(methods: list[dict[str, Any]], lang: str) -> str: + pass_winners = _unique_winners(methods, "pass_rate", higher=True) + metric_winners = { + metric: { + stat: _unique_winners( + [{"id": row["id"], "value": row[metric][stat]} for row in methods], + "value", + higher=False, + ) + for stat in ("mean", "p50", "p95") + } + for metric in ("duration", "action_latency", "cpu", "memory") + } + headers = ( + ("浏览器", "通过", "通过率", "成功交集端到端耗时", "单次动作耗时", "CPU 时间", "平均内存") + if lang == "zh" + else ("Browser", "Passed", "Pass rate", "End-to-end time on shared passes", "Per-action latency", "CPU time", "Mean memory") + ) + body = [] + + def time_value(value: float | None) -> str: + if lang == "zh": + return _format_ms(value) + return "—" if value is None else f"{value / 1000:.1f} s" + + for row in methods: + key = row["id"] + cells = [ + row["label"], + _winner( + f'{row["passed"]}/{row["total"]}', + key in pass_winners, + lang=lang, + ), + _winner(f'{row["pass_rate"] * 100:.1f}%', key in pass_winners, lang=lang), + _metric_lines( + row["duration"], time_value, key, metric_winners["duration"], lang=lang + ), + _metric_lines( + row["action_latency"], + time_value, + key, + metric_winners["action_latency"], + lang=lang, + ), + _metric_lines(row["cpu"], time_value, key, metric_winners["cpu"], lang=lang), + _metric_lines( + row["memory"], _format_mib, key, metric_winners["memory"], lang=lang + ), + ] + body.append("" + "".join(f"{cell}" for cell in cells) + "") + return f'
    {"".join(f"" for h in headers)}{"".join(body)}
    {_e(h)}
    ' + + +def _copy_failure_screenshot(attempt: dict[str, Any], output: Path, key: str) -> str | None: + ref = (attempt.get("evidence") or {}).get("failure_screenshot") + if not ref or not ref.get("path"): + return None + source = Path(str(ref["path"])).expanduser().resolve() + if not source.is_file() or _sha256(source) != ref.get("sha256"): + raise ValueError(f"failure screenshot is missing or changed: {key}") + target = output / "assets/screenshots" / f"{key}.png" + target.parent.mkdir(parents=True, exist_ok=True) + shutil.copyfile(source, target) + return f"assets/screenshots/{key}.png" + + +def _public_evidence( + attempt: dict[str, Any], review: dict[str, Any] | None, output: Path +) -> dict[str, Any]: + key = f'{attempt["engine"]}-{attempt["task_id"]}' + trace = _load_artifact((attempt.get("evidence") or {}).get("trace")) or [] + evaluation = _load_artifact((attempt.get("evidence") or {}).get("evaluation")) + failed = programmatic_failure(attempt) + screenshot = _copy_failure_screenshot(attempt, output, key) + public = { + "result": { + "engine": attempt["engine"], + "task_id": attempt["task_id"], + "status": attempt["status"], + "action_count": attempt["action_count"], + "completed_action_count": attempt["completed_action_count"], + "duration_ms": attempt["duration_ms"], + "timings": attempt.get("timings") or {}, + "resource": attempt.get("resource") or {}, + }, + "programmatic_failure": failed, + "review": ( + { + "cause_code": review["cause_group"]["code"], + "label_zh": review["cause_group"]["label_zh"], + "label_en": review["cause_group"]["label_en"], + "reason_zh": review["reason_zh"], + "reason_en": review["reason_en"], + "responsibility": review["cause_group"]["responsibility"], + "remediation_owner": review["cause_group"]["remediation_owner"], + "remediation_status": review["cause_group"]["remediation_status"], + } + if review + else None + ), + "trajectory": trace, + "official_evaluation": evaluation, + "screenshot": screenshot, + } + _write_json(output / "data/evidence" / f"{key}.json", public) + return public + + +def _detail_page( + lang: str, + attempt: dict[str, Any], + task: dict[str, Any], + method: dict[str, Any], + evidence: dict[str, Any], +) -> str: + failed = evidence["programmatic_failure"] + review = evidence.get("review") + zh = lang == "zh" + status = ("通过" if attempt["status"] == "pass" else "失败") if zh else attempt["status"].title() + reason = (review.get("reason_zh") if zh else review.get("reason_en")) if review else ("—") + label = (review.get("label_zh") if zh else review.get("label_en")) if review else "—" + key = f'{attempt["engine"]}-{attempt["task_id"]}' + report_root = "../" if zh else "../../" + rows = [] + for step in evidence["trajectory"]: + arguments = step.get("arguments") or {} + action = arguments.get("action") or "" + target = arguments.get("selector") or arguments.get("url") or arguments.get("key") or "" + result_lines = str(step.get("result") or "").splitlines() + result = (result_lines[0] if result_lines else "")[:220] + failed_step = failed.get("step") == step.get("index") + rows.append( + f'{step.get("index")}{_e(action)}{_e(target)}{_e(step.get("duration_ms"))} ms{_e(step.get("status"))}{_e(result)}' + ) + screenshot = ( + f'
    {
    {"失败后保存的最后页面;不等同于错误发生瞬间。" if zh else "Last page saved after failure; not necessarily the exact error instant."}
    ' + if evidence.get("screenshot") + else "" + ) + body = f''' +

    {_e(key)}

    {_e(method["label"])} · Task #{attempt["task_id"]}

    {_e(task["task"]["intent"])}

    +

    {"结论与定位" if zh else "Verdict and failure location"}

    +
    {"结果" if zh else "Verdict"}{status}{attempt["completed_action_count"]}/{attempt["action_count"]} {"个动作完成" if zh else "actions completed"}
    +
    {"程序定位" if zh else "Programmatic location"}{_e(failed["stage"])}

    {_e(failed["message"])}

    +
    {"复核根因" if zh else "Reviewed root cause"}{_e(label)}

    {_e(reason)}

    +
    {"处理" if zh else "Remediation"}{_e(review.get("remediation_status") if review else "—")}

    {_e(review.get("remediation_owner") if review else "—")}

    +
    +

    {"固定操作轨迹" if zh else "Fixed action trajectory"}

    {''.join(rows)}
    #ActionTargetTimeStatusResult
    +

    {"日志与证据" if zh else "Logs and evidence"}

    {screenshot}
    ''' + return _shell(lang, "results", f"{method['label']} Task {attempt['task_id']}", body, depth=1) + + +def _assets(output: Path) -> None: + css = r''':root{--bg:#f4f7fb;--panel:#fff;--ink:#102235;--muted:#60748a;--line:#d4e0ec;--accent:#0969da;--good:#087f4f;--bad:#c52c45;--wash:#eaf3ff}html[data-theme=dark]{--bg:#0c1520;--panel:#121f2d;--ink:#eef5fb;--muted:#9db0c2;--line:#2b4054;--accent:#62a8ff;--good:#4ed29b;--bad:#ff7d91;--wash:#142d49}*{box-sizing:border-box}[hidden]{display:none!important}body{margin:0;background:var(--bg);color:var(--ink);font:16px/1.6 Inter,ui-sans-serif,system-ui,-apple-system,"PingFang SC","Microsoft YaHei",sans-serif}.top{height:70px;display:flex;align-items:center;gap:28px;padding:0 max(24px,calc((100% - 1440px)/2));background:var(--panel);border-bottom:1px solid var(--line);position:sticky;top:0;z-index:10}.brand{font-weight:900;color:var(--ink);text-decoration:none;white-space:nowrap}.top nav{display:flex;gap:22px;overflow:auto;flex:1}.top nav a,.language{color:var(--accent);text-decoration:none;white-space:nowrap}.top nav a[aria-current=page]{color:var(--ink);font-weight:800;border-bottom:3px solid var(--ink)}.theme{border:1px solid var(--line);background:var(--panel);color:var(--ink);border-radius:50%;width:38px;height:38px}main,footer{width:min(1440px,calc(100% - 48px));margin:auto}footer{padding:36px 0;color:var(--muted);border-top:1px solid var(--line)}section{padding:34px 0}.hero{padding:46px 0 28px}.hero h1,.detail-hero h1{font-size:clamp(36px,5vw,68px);line-height:1.08;margin:8px 0 14px;letter-spacing:-.04em}.hero p,.detail-hero p{max-width:880px;color:var(--muted);font-size:18px}.eyebrow{text-transform:uppercase;letter-spacing:.14em;color:var(--accent)!important;font-size:13px!important;font-weight:800}h2{font-size:30px;margin:0 0 16px}.table-scroll{overflow:auto;border:1px solid var(--line);border-radius:16px;background:var(--panel)}table{border-collapse:collapse;width:100%;min-width:900px}th,td{text-align:left;vertical-align:top;padding:14px 16px;border-bottom:1px solid var(--line)}th{background:color-mix(in srgb,var(--wash) 70%,var(--panel));font-size:14px;position:sticky;top:0}td{overflow-wrap:anywhere}.metrics td{min-width:150px}.metric-label{display:inline-block;width:42px;color:var(--muted);font-size:13px}.winner{color:var(--good)}.winner>span{display:inline-grid;place-items:center;margin-left:7px;min-width:24px;height:24px;padding:0 6px;border-radius:12px;background:var(--good);color:white;font-size:12px}.callout{padding:16px 18px;border-left:4px solid var(--accent);background:var(--panel);border-radius:0 12px 12px 0}.summary-grid,.diagnosis-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px}.summary-grid article,.diagnosis-grid article{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:18px;min-height:150px}.summary-grid strong,.diagnosis-grid strong{display:block;font-size:24px;margin:8px 0}.summary-grid span,.diagnosis-grid span{color:var(--muted);font-size:13px}.verdict.fail{border-left:5px solid var(--bad)}.verdict.pass{border-left:5px solid var(--good)}.filters{display:flex;gap:10px;flex-wrap:wrap;margin:16px 0}.filters input,.filters select{padding:10px 12px;border:1px solid var(--line);background:var(--panel);color:var(--ink);border-radius:9px;min-width:170px}.inventory{height:620px;overflow:auto}.inventory table{min-width:1500px}.inventory tbody tr{cursor:pointer}.inventory tbody tr:hover{background:var(--wash)}.status-pass{color:var(--good);font-weight:800}.status-fail{color:var(--bad);font-weight:800}.failed-step{background:color-mix(in srgb,var(--bad) 10%,var(--panel))}.failed-step td:first-child{border-left:4px solid var(--bad)}pre{white-space:pre-wrap;overflow-wrap:anywhere;max-height:420px;overflow:auto;background:#071923;color:#dff4ff;padding:14px;border-radius:10px;font:13px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace}figure{margin:22px 0}figure img{display:block;max-width:100%;border:1px solid var(--line);border-radius:12px}figcaption{color:var(--muted);font-size:13px}.evidence-actions{display:flex;gap:12px;flex-wrap:wrap}.evidence-actions a{padding:10px 14px;border:1px solid var(--line);border-radius:9px;background:var(--panel);color:var(--accent);text-decoration:none}.detail-hero{padding-top:40px}.viewer{height:75vh}.viewer pre{height:100%;max-height:none}dialog{width:min(850px,calc(100% - 32px));border:1px solid var(--line);border-radius:16px;background:var(--panel);color:var(--ink);padding:0}dialog::backdrop{background:#07111bbf}.dialog-head{display:flex;justify-content:space-between;padding:18px;border-bottom:1px solid var(--line)}.dialog-body{padding:18px}.dialog-head button{border:0;background:none;color:var(--ink);font-size:24px}.mermaid{width:100%;max-width:100%;background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:24px;overflow:auto}.flow-fallback{display:flex;align-items:stretch;gap:10px;overflow:auto;padding:18px;background:var(--panel);border:1px solid var(--line);border-radius:14px}.flow-node{min-width:150px;padding:14px;border-radius:10px;background:var(--wash)}.flow-node strong{display:block;color:var(--accent);margin-bottom:6px}.flow-arrow{display:grid;place-items:center;color:var(--muted);font-size:24px}@media(max-width:720px){.top{padding:0 16px;gap:14px}.top nav{gap:14px}main,footer{width:calc(100% - 28px)}section{padding:26px 0}.hero h1,.detail-hero h1{font-size:36px}.inventory{height:560px}.language{display:none}}''' + js = r'''document.querySelector('.theme')?.addEventListener('click',()=>{const root=document.documentElement;root.dataset.theme=root.dataset.theme==='dark'?'light':'dark';localStorage.setItem('report-theme',root.dataset.theme)});document.documentElement.dataset.theme=localStorage.getItem('report-theme')||'light';const table=document.querySelector('[data-results]');if(table){const rows=[...table.querySelectorAll('tbody tr')],controls=[...document.querySelectorAll('[data-filter]')],dialog=document.querySelector('#diagnosis-dialog');function filter(){const q=(document.querySelector('[data-filter=q]')?.value||'').toLowerCase();rows.forEach(row=>{const ok=controls.every(c=>!c.value||c.dataset.filter==='q'||row.dataset[c.dataset.filter]===c.value)&&(!q||row.textContent.toLowerCase().includes(q));row.hidden=!ok})}controls.forEach(c=>c.addEventListener('input',filter));filter();const resultsUrl=document.documentElement.lang==='zh'?'data/results.json':'../data/results.json';fetch(resultsUrl).then(r=>r.json()).then(data=>{const byKey=Object.fromEntries(data.map(x=>[x.key,x]));rows.forEach(row=>row.addEventListener('click',e=>{if(e.target.closest('a'))return;const x=byKey[row.dataset.key];if(!x||!dialog)return;dialog.querySelector('[data-dialog-title]').textContent=x.browser+' · Task #'+x.task_id;dialog.querySelector('[data-dialog-stage]').textContent=x.failure_stage;dialog.querySelector('[data-dialog-error]').textContent=x.program_error||'—';dialog.querySelector('[data-dialog-cause]').textContent=(document.documentElement.lang==='zh'?x.reason_zh:x.reason_en)||'—';dialog.querySelector('[data-dialog-link]').href='result/'+x.key+'.html';dialog.showModal()}))});dialog?.querySelector('button')?.addEventListener('click',()=>dialog.close())}const viewer=document.querySelector('[data-evidence-viewer]');if(viewer){const file=new URLSearchParams(location.search).get('file');const safe=/^(?:\.\.\/)?data\/evidence\/[a-z0-9.-]+\.json$/.test(file||'');if(safe)fetch(file).then(r=>{if(!r.ok)throw Error(r.status);return r.json()}).then(x=>viewer.textContent=JSON.stringify(x,null,2)).catch(e=>viewer.textContent='Evidence could not be loaded: '+e.message);else viewer.textContent='No evidence file selected.'}''' + (output / "assets").mkdir(parents=True, exist_ok=True) + (output / "assets/report.css").write_text(css, encoding="utf-8") + (output / "assets/app.js").write_text(js, encoding="utf-8") + + +def build_webarena_replay_report( + *, + results_path: str | Path, + dataset_path: str | Path, + review_path: str | Path, + profile_path: str | Path, + output_dir: str | Path, +) -> dict[str, Any]: + validated = load_validated_failure_review(results_path, review_path) + results = validated["results"] + reviews = validated["by_result"] + dataset = json.loads(Path(dataset_path).read_text(encoding="utf-8")) + profile = json.loads(Path(profile_path).read_text(encoding="utf-8")) + output = Path(output_dir).resolve() + if output.exists(): + shutil.rmtree(output) + output.mkdir(parents=True) + _assets(output) + + tasks = {int(item["task_id"]): item for item in dataset["trajectories"]} + methods = {item["engine"]: item for item in profile["methods"]} + attempts = results["attempts"] + if {(item["engine"], int(item["task_id"])) for item in attempts} != { + (engine, task_id) for engine in methods for task_id in tasks + }: + raise ValueError("report requires the complete participant-task matrix") + + action_by_engine: dict[str, list[float]] = defaultdict(list) + action_by_name: dict[str, dict[str, list[float]]] = defaultdict(lambda: defaultdict(list)) + for item in results["physical_executions"]: + key = (str(item["engine"]), int(item["task_id"])) + trace = _load_artifact((item.get("evidence") or {}).get("trace")) or [] + for step in trace: + value = step.get("duration_ms") + if isinstance(value, (int, float)): + action_by_engine[key[0]].append(float(value)) + action = str((step.get("arguments") or {}).get("action") or "unknown") + action_by_name[action][key[0]].append(float(value)) + + passing_tasks = { + task_id + for task_id in tasks + if all( + next( + item for item in attempts if item["engine"] == engine and item["task_id"] == task_id + )["status"] + == "pass" + for engine in methods + ) + } + method_rows = [] + for engine, method in methods.items(): + rows = [item for item in attempts if item["engine"] == engine] + passes = sum(item["status"] == "pass" for item in rows) + duration = _stat( + [float(item["duration_ms"]) for item in rows if item["task_id"] in passing_tasks] + ) + action_latency = _stat(action_by_engine[engine]) + comparable_rows = [item for item in rows if item["task_id"] in passing_tasks] + cpu = _stat( + [float((item.get("resource") or {}).get("cpu_ms") or 0) for item in comparable_rows] + ) + memory = _stat( + [ + float((item.get("resource") or {}).get("memory_mean_bytes") or 0) + for item in comparable_rows + ] + ) + method_rows.append( + { + "id": method["id"], + "engine": engine, + "label": method["label"], + "passed": passes, + "total": len(rows), + "pass_rate": passes / len(rows), + "duration": duration, + "duration_mean": duration["mean"], + "action_latency": action_latency, + "action_mean": action_latency["mean"], + "cpu": cpu, + "cpu_mean": cpu["mean"], + "memory": memory, + "memory_mean": memory["mean"], + "physical_executions": len( + [item for item in results["physical_executions"] if item["engine"] == engine] + ), + } + ) + + public_rows = [] + for attempt in attempts: + key = (attempt["engine"], int(attempt["task_id"])) + task = tasks[key[1]] + review = reviews.get(key) + evidence = _public_evidence(attempt, review, output) + failure = evidence["programmatic_failure"] + cause = review["cause_group"] if review else {} + row = { + "key": f"{key[0]}-{key[1]}", + "engine": key[0], + "browser": methods[key[0]]["label"], + "task_id": key[1], + "intent": task["task"]["intent"], + "sites": task["task"].get("sites") or [], + "status": attempt["status"], + "completed_actions": attempt["completed_action_count"], + "action_count": attempt["action_count"], + "failure_stage": failure["stage"], + "program_error": failure["message"], + "cause_zh": cause.get("label_zh", ""), + "cause_en": cause.get("label_en", ""), + "reason_zh": review.get("reason_zh", "") if review else "", + "reason_en": review.get("reason_en", "") if review else "", + } + public_rows.append(row) + for lang in ("zh", "en"): + target = output / ("result" if lang == "zh" else "en/result") / f"{row['key']}.html" + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text( + _detail_page(lang, attempt, task, methods[key[0]], evidence), encoding="utf-8" + ) + + _write_json(output / "data/results.json", public_rows) + _write_json(output / "data/metrics.json", {"methods": method_rows, "passing_intersection": len(passing_tasks)}) + + for lang in ("zh", "en"): + zh = lang == "zh" + root = output if zh else output / "en" + root.mkdir(exist_ok=True) + action_headers = ["动作" if zh else "Action", *[item["label"] for item in methods.values()]] + action_rows = [] + for action in sorted(action_by_name): + action_metrics = { + engine: _stat(action_by_name[action][engine]) for engine in methods + } + action_winners = { + stat: _unique_winners( + [ + {"id": methods[engine]["id"], "value": metric[stat]} + for engine, metric in action_metrics.items() + ], + "value", + higher=False, + ) + for stat in ("mean", "p50", "p95") + } + cells = [] + for engine in methods: + metric = action_metrics[engine] + text = _metric_lines( + metric, + ( + lambda value, is_zh=zh: _format_ms(value) + if is_zh + else ("—" if value is None else f"{value / 1000:.1f} s") + ), + methods[engine]["id"], + action_winners, + lang=lang, + ) + cells.append(f"{text}
    {metric['n']} calls") + action_rows.append(f'{_e(action)}{"".join(cells)}') + action_table = f'
    {"".join(f"" for h in action_headers)}{"".join(action_rows)}
    {_e(h)}
    ' + method_cards = [] + for row in method_rows: + failed_count = row["total"] - row["passed"] + if failed_count: + note = ( + f"{failed_count} 条最终失败已逐条复核。" + if zh + else f"{failed_count} final failures were reviewed individually." + ) + else: + note = ( + "固定轨迹与验收合同全部通过。" + if zh + else "Every fixed trajectory and acceptance contract passed." + ) + method_cards.append( + f'
    {"浏览器结果" if zh else "Browser result"}' + f'{_e(row["label"])} {row["passed"]}/{row["total"]}' + f"

    {note}

    " + ) + task_count = len(tasks) + shared_count = len(passing_tasks) + summary_body = f'''

    WebArena-Verified · {task_count} tasks · model calls 0

    {"浏览器轨迹重放评测" if zh else "Browser Trajectory Replay"}

    {"同一组经 Chromium 资格验证的固定 BrowserTool 轨迹,依次在各注册浏览器中执行。Agent 与模型不参与测试。" if zh else "The same Chromium-qualified BrowserTool trajectories run on every registered browser. No Agent or model participates in the benchmark."}

    {"核心指标" if zh else "Core metrics"}

    {f"正确率覆盖全部 {task_count} 题;端到端耗时、CPU 和内存只比较所有浏览器共同通过的 {shared_count} 题,避免提前失败缩短工作量。单次动作统计包含重试中的实际调用;调用数不判优。" if zh else f"Correctness covers all {task_count} tasks. End-to-end time, CPU, and memory use the {shared_count} tasks passed by every browser so early failure cannot reduce workload. Per-action latency includes actual retry calls; call counts are not ranked."}

    {_metrics_table(method_rows, lang)}

    {"结论" if zh else "Findings"}

    {"".join(method_cards)}
    {"执行隔离" if zh else "Execution isolation"}{"串行" if zh else "Serial"}

    {"每题重置;方法之间销毁浏览器和站点。" if zh else "Reset per task; browser and sites torn down between methods."}

    {"浏览器动作耗时" if zh else "Browser action latency"}

    {"每格给出均值、P50、P95 和调用次数;耗时在所有注册浏览器间判优,调用次数只反映工作量。" if zh else "Each cell shows mean, P50, P95, and call count. Latency is ranked across every registered browser; counts only describe workload."}

    {action_table}
    ''' + (root / "index.html").write_text(_shell(lang, "index", "Browser Trajectory Replay", summary_body), encoding="utf-8") + + headers = ["Task", "Site", *[item["label"] for item in methods.values()]] + matrix_rows = [] + for task_id, task in tasks.items(): + values = [] + for engine in methods: + row = next(item for item in public_rows if item["engine"] == engine and item["task_id"] == task_id) + label = ("通过" if row["status"] == "pass" else "失败") if zh else row["status"].title() + values.append(f'{label}') + matrix_rows.append(f'{task_id}{_e(", ".join(task["task"].get("sites") or []))}{"".join(f"{v}" for v in values)}') + dataset_body = f'

    {"90 条标准轨迹" if zh else "90 qualified trajectories"}

    {"每题一条固定操作轨迹。60 条来自干净的 Browser Use 成功执行,30 条经修复后重新在 Chromium 验证。" if zh else "One fixed trajectory per task: 60 clean Browser Use successes and 30 repaired trajectories re-qualified on Chromium."}

    {"逐题结果矩阵" if zh else "Per-task result matrix"}

    {"".join(f"" for h in headers)}{"".join(matrix_rows)}
    {_e(h)}
    ' + (root / "dataset.html").write_text(_shell(lang, "dataset", "Dataset", dataset_body), encoding="utf-8") + + result_rows = [] + for row in public_rows: + cause = row["cause_zh"] if zh else row["cause_en"] + reason = row["reason_zh"] if zh else row["reason_en"] + status = ("通过" if row["status"] == "pass" else "失败") if zh else row["status"].title() + result_rows.append(f'{_e(row["browser"])} · {row["task_id"]}{_e(row["sites"][0] if row["sites"] else "")}{status}{row["completed_actions"]}/{row["action_count"]}{_e(row["failure_stage"])}{_e(row["program_error"])}{_e(cause)}
    {_e(reason)}') + browser_options = "".join(f'' for key, item in methods.items()) + results_body = f'''

    {"结果与失败诊断" if zh else "Results and failure diagnosis"}

    {"默认显示失败。点击任一行先看诊断弹窗,再进入详情页查看轨迹、日志和最后页面。" if zh else "Failures are shown by default. Open a row for the diagnosis dialog, then continue to the detail page for trajectory, logs, and last page."}

    {''.join(result_rows)}
    {"结果" if zh else "Result"}{"站点" if zh else "Site"}{"判定" if zh else "Verdict"}{"动作" if zh else "Actions"}{"失败阶段" if zh else "Failure stage"}{"程序报错" if zh else "Program error"}{"复核根因" if zh else "Reviewed root cause"}

    {"失败阶段" if zh else "Failure stage"}

    {"程序报错" if zh else "Program error"}

    {"复核根因" if zh else "Reviewed root cause"}

    {"打开完整详情" if zh else "Open full detail"}
    ''' + (root / "results.html").write_text(_shell(lang, "results", "Results", results_body), encoding="utf-8") + + flow_labels = ( + ("重放器", "浏览器容器", "本地站点", "逐题重置", "证据采集", "官方验收", "完全销毁") + if zh + else ("Replay runner", "Browser container", "Local sites", "Per-task reset", "Evidence", "Official evaluator", "Full teardown") + ) + fallback = '
    ' + "".join( + f'
    {index}{_e(label)}
    ' + + ('' if index < len(flow_labels) else "") + for index, label in enumerate(flow_labels, 1) + ) + "
    " + topology = '''''' + environment_body = f'''

    {"环境与评测流程" if zh else "Environment and evaluation flow"}

    {"192.168.5.2 是 Colima 虚拟机中的宿主网关:浏览器容器通过它访问运行在同一台 Mac 虚拟化环境里的 WebArena 站点。它不是公网测试网站。" if zh else "192.168.5.2 is the host gateway inside the Colima VM. Browser containers use it to reach the WebArena sites running in the same Mac virtualization environment; it is not a public test website."}

    {"执行拓扑" if zh else "Execution topology"}

    {fallback}{topology}

    {"隔离规则" if zh else "Isolation rules"}

    1

    {"一套固定端口站点环境,因此只允许一个方法运行。" if zh else "One fixed-port site stack permits only one running method."}

    90

    {"一个浏览器完整跑完全部任务,每题前恢复官方夹具。" if zh else "A browser completes all tasks; official fixtures reset before each task."}

    0

    {"没有 Agent 决策和模型请求;输入是冻结的 BrowserTool 动作。" if zh else "No Agent decisions or model calls; inputs are frozen BrowserTool actions."}

    ''' + (root / "environment.html").write_text(_shell(lang, "environment", "Environment", environment_body), encoding="utf-8") + + review_body = f'''

    {"核验记录" if zh else "Review history"}

    {"记录影响当前数据集与结论的核验轮次;逐题证据位于结果详情页。" if zh else "Review rounds that changed the current dataset or conclusions. Per-result evidence remains on result pages."}

    2026-09-15{"90 条轨迹资格核验" if zh else "90-trajectory qualification"}

    {"修复已确认的非浏览器问题,重新验证后 Chromium 90/90。" if zh else "Confirmed non-browser defects were repaired; Chromium then qualified 90/90."}

    2026-09-15{"36 条失败根因复核" if zh else "36-failure root-cause review"}

    {"自动失败位置与复核根因分离;36/36 结果绑定当前证据哈希,无未解决项。" if zh else "Programmatic locations remain separate from reviewed causes; 36/36 are bound to the current evidence hash with no unresolved items."}

    ''' + (root / "review.html").write_text(_shell(lang, "review", "Review history", review_body), encoding="utf-8") + + viewer_body = f'

    {"证据查看器" if zh else "Evidence viewer"}

    {"网页直接加载选中的净化证据;也可下载原始 JSON。" if zh else "Loads the selected sanitized evidence directly in the page; raw JSON remains downloadable."}

    ' + (root / "evidence-viewer.html").write_text(_shell(lang, "results", "Evidence viewer", viewer_body), encoding="utf-8") + + manifest = { + "schema": "lex_webarena_trajectory_replay_report/1", + "results_sha256": _sha256(Path(results_path)), + "dataset_sha256": _sha256(Path(dataset_path)), + "review_sha256": _sha256(Path(review_path)), + "logical_results": len(public_rows), + "failed_results": sum(row["status"] != "pass" for row in public_rows), + "reviewed_failures": len(reviews), + "routes": 12 + 2 * len(public_rows), + } + _write_json(output / "report-manifest.json", manifest) + return manifest diff --git a/pyproject.toml b/pyproject.toml index c51ac3685..f4bee6398 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -57,6 +57,7 @@ select = ["B", "E", "F", "I", "UP"] [tool.ruff.lint.per-file-ignores] "harness/trajectory_replay/report.py" = ["E501"] +"harness/trajectory_replay/webarena_report.py" = ["E501"] "harness/browser_use/report.py" = ["E501"] "harness/browser_use/report_i18n.py" = ["E501"] diff --git a/scripts/build_webarena_replay_report.py b/scripts/build_webarena_replay_report.py new file mode 100644 index 000000000..acb70516e --- /dev/null +++ b/scripts/build_webarena_replay_report.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +"""Build the current WebArena trajectory replay report.""" + +from __future__ import annotations + +import argparse +import json + +from harness.trajectory_replay.webarena_report import build_webarena_replay_report + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--results", required=True) + parser.add_argument("--output", default="reports/trajectory-replay") + args = parser.parse_args() + manifest = build_webarena_replay_report( + results_path=args.results, + dataset_path="evaluation/trajectory-replay/webarena-verified/trajectories.json", + review_path="research/trajectory-replay-failure-review.json", + profile_path="evaluation/trajectory-replay/webarena-verified/current-run.json", + output_dir=args.output, + ) + print(json.dumps(manifest, ensure_ascii=False)) + + +if __name__ == "__main__": + main() diff --git a/scripts/check_webarena_replay_report.py b/scripts/check_webarena_replay_report.py new file mode 100644 index 000000000..63b034743 --- /dev/null +++ b/scripts/check_webarena_replay_report.py @@ -0,0 +1,101 @@ +#!/usr/bin/env python3 +"""Run deterministic release checks for the WebArena replay report.""" + +from __future__ import annotations + +import argparse +import json +import re +from pathlib import Path +from urllib.parse import unquote, urlsplit + + +def _local_target(page: Path, raw: str) -> Path | None: + parsed = urlsplit(raw) + if parsed.scheme or raw.startswith(("#", "mailto:", "javascript:")): + return None + return (page.parent / unquote(parsed.path)).resolve() + + +def check(report_dir: Path) -> dict[str, int]: + root = report_dir.resolve() + manifest = json.loads((root / "report-manifest.json").read_text(encoding="utf-8")) + results = json.loads((root / "data/results.json").read_text(encoding="utf-8")) + if manifest["logical_results"] != 180 or len(results) != 180: + raise ValueError("report must contain the complete 90-task participant matrix") + failures = [item for item in results if item["status"] != "pass"] + if len(failures) != 36 or manifest["reviewed_failures"] != 36: + raise ValueError("report must contain all 36 reviewed final failures") + if any(not item["reason_zh"] or not item["reason_en"] for item in failures): + raise ValueError("every failed result needs a bilingual reviewed cause") + + zh_details = {path.name for path in (root / "result").glob("*.html")} + en_details = {path.name for path in (root / "en/result").glob("*.html")} + if zh_details != en_details or len(zh_details) != 180: + raise ValueError("Chinese and English result routes must match one-to-one") + core_names = ( + "index.html", + "dataset.html", + "results.html", + "environment.html", + "review.html", + "evidence-viewer.html", + ) + core = [ + *(root / name for name in core_names), + *(root / "en" / name for name in core_names), + ] + forbidden = ( + "用户提示词", + "我的思考", + "assistant reasoning", + "内部工作日志", + "中位数", + "median", + "后续 179 题", + ) + for page in core: + text = page.read_text(encoding="utf-8") + if any(term in text for term in forbidden): + raise ValueError(f"formal report contains internal or retired prose: {page}") + if "' not in results_html or 'id="diagnosis-dialog"' not in results_html: + raise ValueError("result inventory must default to failure and open a diagnosis dialog") + return {"html_pages": len(list(root.rglob("*.html"))), "checked_links": checked_links} + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("report_dir", type=Path) + args = parser.parse_args() + print(json.dumps(check(args.report_dir), ensure_ascii=False)) + + +if __name__ == "__main__": + main() diff --git a/scripts/check_webarena_replay_visual.py b/scripts/check_webarena_replay_visual.py new file mode 100644 index 000000000..605cdf4f1 --- /dev/null +++ b/scripts/check_webarena_replay_visual.py @@ -0,0 +1,216 @@ +#!/usr/bin/env python3 +"""Render every replay report route class and reject unreadable layouts.""" + +from __future__ import annotations + +import argparse +import hashlib +import http.server +import json +import os +import shutil +import threading +from pathlib import Path + +from playwright.sync_api import sync_playwright + + +def _tree_sha256(root: Path) -> str: + digest = hashlib.sha256() + for path in sorted(item for item in root.rglob("*") if item.is_file()): + relative = path.relative_to(root).as_posix().encode() + digest.update(relative) + digest.update(hashlib.sha256(path.read_bytes()).digest()) + return digest.hexdigest() + + +def _browser_executable() -> str: + configured = os.environ.get("BROWSER_USE_VISUAL_BROWSER") or os.environ.get( + "CHROMIUM_BINARY" + ) + for candidate in ( + configured, + "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome", + shutil.which("google-chrome"), + shutil.which("chromium"), + ): + if candidate and Path(candidate).is_file(): + return candidate + raise ValueError("visual QA requires Chrome or Chromium") + + +class _QuietHandler(http.server.SimpleHTTPRequestHandler): + def log_message(self, *_args: object) -> None: + return + + +def _routes(report: Path) -> list[str]: + failed = json.loads((report / "data/results.json").read_text(encoding="utf-8")) + failed_key = next(item["key"] for item in failed if item["status"] == "fail") + fixed = [ + "index.html", + "dataset.html", + "results.html", + "environment.html", + "review.html", + ] + routes = [*fixed, *(f"en/{item}" for item in fixed)] + routes.extend( + ( + f"result/{failed_key}.html", + f"en/result/{failed_key}.html", + f"evidence-viewer.html?file=data/evidence/{failed_key}.json", + f"en/evidence-viewer.html?file=../data/evidence/{failed_key}.json", + ) + ) + return routes + + +def check(report: Path, receipt: Path) -> None: + report = report.resolve() + receipt = receipt.resolve() + screenshot_dir = receipt.parent / "trajectory-replay-visual" + screenshot_dir.mkdir(parents=True, exist_ok=True) + handler = lambda *args, **kwargs: _QuietHandler( # noqa: E731 + *args, directory=str(report), **kwargs + ) + server = http.server.ThreadingHTTPServer(("127.0.0.1", 0), handler) + thread = threading.Thread(target=server.serve_forever, daemon=True) + thread.start() + failures: list[dict[str, object]] = [] + states: list[dict[str, object]] = [] + executable = _browser_executable() + try: + with sync_playwright() as playwright: + browser = playwright.chromium.launch( + executable_path=executable, + headless=True, + ) + try: + for index, route in enumerate(_routes(report)): + for width, height in ((1440, 1000), (390, 844)): + for theme in ("light", "dark"): + page = browser.new_page(viewport={"width": width, "height": height}) + url = f"http://127.0.0.1:{server.server_port}/{route}" + page.goto(url, wait_until="load", timeout=10_000) + if "evidence-viewer" in route: + page.wait_for_function( + """() => { +const node=document.querySelector('[data-evidence-viewer]'); +return node && node.textContent.trim().startsWith('{'); +}""", + timeout=5_000, + ) + page.evaluate( + "value => document.documentElement.dataset.theme=value", + theme, + ) + result = page.evaluate( + """() => { +const root=document.documentElement; +const clipped=[]; +const layoutSelector='h1,h2,.filters input,.filters select,'+ + '.diagnosis-grid article,.summary-grid article'; +for(const element of document.querySelectorAll(layoutSelector)){ + if(!element.getClientRects().length)continue; + const box=element.getBoundingClientRect(); + if(box.left < -2 || box.right > innerWidth + 2){ + clipped.push(element.tagName+'.'+element.className); + } +} +const tiny=[]; +for(const element of document.querySelectorAll('main p,main td,main th,main a,main button')){ + if(element.getClientRects().length && + parseFloat(getComputedStyle(element).fontSize) < 12){ + tiny.push(element.tagName); + } +} +const viewer=document.querySelector('[data-evidence-viewer]'); +const topology=document.querySelector('.mermaid svg,[data-topology-fallback]:not([hidden])'); +const rawMermaid=[...document.querySelectorAll('.mermaid')].some( + node => !node.hidden && node.textContent.includes('flowchart LR') +); +return { + overflow:root.scrollWidth-root.clientWidth, + clipped:[...new Set(clipped)], + tiny:[...new Set(tiny)], + viewerError:viewer ? /could not be loaded|未选择|No evidence/.test(viewer.textContent) : false, + topologyMissing:location.pathname.endsWith('/environment.html') && !topology, + rawMermaid +}; +}""" + ) + state = { + "route": route, + "width": width, + "theme": theme, + **result, + } + states.append(state) + if any( + ( + result["overflow"] > 2, + result["clipped"], + result["tiny"], + result["viewerError"], + result["topologyMissing"], + result["rawMermaid"], + ) + ): + failures.append(state) + if ( + theme == "light" + and ( + route + in { + "index.html", + "results.html", + "environment.html", + } + or route.startswith("result/") + ) + ): + shot = screenshot_dir / f"{index}-{width}.png" + page.screenshot(path=str(shot), full_page=True) + page.close() + finally: + browser.close() + finally: + server.shutdown() + server.server_close() + thread.join(timeout=5) + receipt.parent.mkdir(parents=True, exist_ok=True) + receipt.write_text( + json.dumps( + { + "schema": "lex_webarena_replay_visual_qa/1", + "report_tree_sha256": _tree_sha256(report), + "browser_executable": executable, + "route_classes": len(_routes(report)), + "states_checked": len(states), + "failures": failures, + }, + ensure_ascii=False, + indent=2, + ), + encoding="utf-8", + ) + if failures: + raise ValueError(f"visual QA failed: {failures[0]}") + print(f"Trajectory replay visual QA passed: {len(states)} rendered states") + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("report", type=Path) + parser.add_argument( + "--receipt", + type=Path, + default=Path("publish/trajectory-replay-visual-qa.json"), + ) + args = parser.parse_args() + check(args.report, args.receipt) + + +if __name__ == "__main__": + main() diff --git a/tests/trajectory_replay/test_webarena_report.py b/tests/trajectory_replay/test_webarena_report.py new file mode 100644 index 000000000..8aebf367b --- /dev/null +++ b/tests/trajectory_replay/test_webarena_report.py @@ -0,0 +1,30 @@ +from harness.trajectory_replay.webarena_report import _metrics_table + + +def _method(name: str, pass_rate: float, base: float) -> dict: + metric = {"mean": base, "p50": base + 1, "p95": base + 2} + return { + "id": name, + "label": name, + "passed": int(pass_rate * 90), + "total": 90, + "pass_rate": pass_rate, + "duration": metric, + "action_latency": metric, + "cpu": metric, + "memory": metric, + } + + +def test_core_metrics_are_registry_driven_and_mark_only_unique_winners() -> None: + rendered = _metrics_table( + [ + _method("Browser A", 1.0, 3.0), + _method("Browser B", 0.8, 2.0), + _method("Browser C", 0.8, 1.0), + ], + "en", + ) + assert all(name in rendered for name in ("Browser A", "Browser B", "Browser C")) + assert rendered.count(">Best") == 14 + assert "median" not in rendered.lower() From b8c1022cae46817d2eacfb062449d50eb053e230 Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 15 Sep 2026 05:15:10 +0800 Subject: [PATCH 112/127] Harden replay cleanup and environment locking --- harness/trajectory_replay/webarena_dataset.py | 34 ++++++++++---- harness/trajectory_replay/webarena_replay.py | 24 ++++++++-- .../test_webarena_dataset.py | 46 +++++++++++++++++++ .../trajectory_replay/test_webarena_replay.py | 11 +++++ 4 files changed, 101 insertions(+), 14 deletions(-) diff --git a/harness/trajectory_replay/webarena_dataset.py b/harness/trajectory_replay/webarena_dataset.py index 3afc42165..8e6e82b1f 100644 --- a/harness/trajectory_replay/webarena_dataset.py +++ b/harness/trajectory_replay/webarena_dataset.py @@ -77,6 +77,14 @@ def _validate_qualification_receipt( required_engines: tuple[str, ...], expected_resources: dict[str, Any], ) -> None: + def required_int(value: Any, field: str) -> int: + try: + return int(value) + except (TypeError, ValueError) as exc: + raise ValueError( + f"replay receipt has invalid {field}: {receipt_path}" + ) from exc + if receipt.get("schema") != RECEIPT_SCHEMA: raise ValueError(f"unsupported replay receipt: {receipt_path}") if ( @@ -100,7 +108,9 @@ def _validate_qualification_receipt( not image.get("id") for image in runtime["site_images"].values() ): raise ValueError(f"replay receipt is missing site image identity: {receipt_path}") - receipt_task_ids = [int(task_id) for task_id in receipt.get("task_ids") or []] + receipt_task_ids = [ + required_int(task_id, "task_id") for task_id in receipt.get("task_ids") or [] + ] if ( not receipt_task_ids or len(receipt_task_ids) != len(set(receipt_task_ids)) @@ -137,14 +147,15 @@ def _validate_qualification_receipt( for attempt in receipt.get("attempts") or []: if attempt.get("schema") != "lex_webarena_trajectory_replay_attempt/1": raise ValueError(f"replay receipt has an invalid attempt schema: {receipt_path}") + attempt_task_id = required_int(attempt.get("task_id"), "attempt task_id") session = sessions_by_id.get(str(attempt.get("browser_session_id") or "")) if ( session is None or session.get("engine") != attempt.get("engine") - or int(attempt.get("task_id")) not in session.get("task_ids", []) + or attempt_task_id not in session.get("task_ids", []) ): raise ValueError(f"replay attempt is not bound to a browser session: {receipt_path}") - attempt_keys.append((str(attempt.get("engine")), int(attempt.get("task_id")))) + attempt_keys.append((str(attempt.get("engine")), attempt_task_id)) expected_keys = { (engine, task_id) for engine in required_engines for task_id in receipt_task_ids } @@ -153,14 +164,17 @@ def _validate_qualification_receipt( f"replay receipt does not contain the complete method-task matrix: {receipt_path}" ) physical_executions = receipt.get("physical_executions") or [] - physical_keys = [ - ( - str(item.get("engine")), - int(item.get("task_id")), - int(item.get("physical_execution")), + physical_keys = [] + for item in physical_executions: + physical_keys.append( + ( + str(item.get("engine")), + required_int(item.get("task_id"), "physical task_id"), + required_int( + item.get("physical_execution"), "physical execution number" + ), + ) ) - for item in physical_executions - ] if ( not physical_executions or any(item.get("status") != "complete" for item in physical_executions) diff --git a/harness/trajectory_replay/webarena_replay.py b/harness/trajectory_replay/webarena_replay.py index 002fd9b55..41a9c6d77 100644 --- a/harness/trajectory_replay/webarena_replay.py +++ b/harness/trajectory_replay/webarena_replay.py @@ -21,6 +21,7 @@ _cdp_metadata, _configure_benchmark, _container_host_gateway, + _docker_daemon_lock_namespace, _docker_image_metadata, _login_context, _physical_execution_lock, @@ -451,14 +452,25 @@ def _run_one( # noqa: PLR0913 pass readiness_retries = locals().get("readiness_retries", 0) finally: + cleanup_errors: list[str] = [] duration_ms = (time.perf_counter() - started) * 1000 if sampler_started: - sampler.stop() - after = resources.counters() if before is not None else None + try: + sampler.stop() + except Exception as exc: + cleanup_errors.append(f"resource sampler stop failed: {exc}") + try: + after = resources.counters() if before is not None else None + except Exception as exc: + after = None + cleanup_errors.append(f"final resource counters failed: {exc}") resource_duration_ms = ( (time.perf_counter() - resource_started) * 1000 if resource_started is not None else 0.0 ) - tool.close() + try: + tool.close() + except Exception as exc: + cleanup_errors.append(f"browser tool close failed: {exc}") timings["browser_resource_measurement_ms"] = resource_duration_ms @@ -504,6 +516,7 @@ def _run_one( # noqa: PLR0913 else {} ), "error": error, + "cleanup_errors": cleanup_errors, "evidence": { "trace": _evidence_file(trace_path), "network_har": _evidence_file(har_path), @@ -838,6 +851,7 @@ def run_replay_suite( # noqa: PLR0913 browser_eval_commit = _require_clean_git_checkout(repo_root) cowagent_commit = _require_clean_git_checkout(cowagent) client = docker.from_env() + docker_info = client.info() browser_images = { engine: _docker_image_metadata(client.images.get(ENGINE_IMAGES[engine])) for engine in engines @@ -870,7 +884,9 @@ def run_replay_suite( # noqa: PLR0913 qualification_dataset_sha256=qualification_dataset_sha256, ) completed = {(str(item["engine"]), int(item["task_id"])) for item in attempts} - lock_path = _physical_execution_lock_path() + lock_path = _physical_execution_lock_path( + _docker_daemon_lock_namespace(docker_info) + ) receipt = { "schema": RECEIPT_SCHEMA, "run_id": run_id, diff --git a/tests/trajectory_replay/test_webarena_dataset.py b/tests/trajectory_replay/test_webarena_dataset.py index 4c56556f8..1944438a9 100644 --- a/tests/trajectory_replay/test_webarena_dataset.py +++ b/tests/trajectory_replay/test_webarena_dataset.py @@ -3,6 +3,7 @@ from importlib.metadata import version from pathlib import Path +import pytest from webarena_verified.core.utils.checksum import compute_evaluator_checksum from harness.trajectory_replay.webarena_dataset import ( @@ -357,6 +358,51 @@ def _receipt( } +def test_qualification_receipt_rejects_missing_attempt_identifier(tmp_path: Path) -> None: + profile, selection_sha256, repair_sha256 = _profile(tmp_path, [1]) + seeds = _write( + tmp_path / "seeds.json", + { + "schema": "lex_browser_use_replay_seeds/2", + "source": {"selection_sha256": selection_sha256}, + "expert_repairs": {"repair_spec_sha256": repair_sha256}, + "tasks": [ + { + "task_id": 1, + "canonical_candidate_id": "candidate", + "replay_status": "ready", + "candidates": [ + { + "id": "candidate", + "tool_calls": [{"action": "snapshot"}], + "recorded_response": {}, + } + ], + } + ], + }, + ) + payload = _receipt( + seeds, + profile, + task_ids=[1], + attempts=[ + { + "task_id": 1, + "source_candidate_id": "candidate", + "engine": "chromium", + "process_status": "pass", + "evaluator_status": "pass", + } + ], + ) + payload["attempts"][0]["task_id"] = None + receipt = _write(tmp_path / "receipt.json", payload) + + with pytest.raises(ValueError, match="invalid attempt task_id"): + build_verified_trajectory_dataset(seeds, [receipt], profile_path=profile) + + def test_promotion_requires_candidate_to_pass_reference_qualification(tmp_path: Path) -> None: profile, selection_sha256, repair_sha256 = _profile(tmp_path, [44]) seeds = _write( diff --git a/tests/trajectory_replay/test_webarena_replay.py b/tests/trajectory_replay/test_webarena_replay.py index 8fde3d532..baa7ae81e 100644 --- a/tests/trajectory_replay/test_webarena_replay.py +++ b/tests/trajectory_replay/test_webarena_replay.py @@ -13,6 +13,7 @@ _execute_replay_call, _origin, _rebind_url, + _run_one, _validated_resume, run_replay_suite, select_methods, @@ -57,6 +58,16 @@ def test_qualification_scope_is_separate_from_diagnostics() -> None: assert "formal correction shards must cover every registered browser" in source +def test_replay_cleanup_and_locking_preserve_run_isolation() -> None: + source = inspect.getsource(run_replay_suite) + run_one_source = inspect.getsource(_run_one) + + assert "_docker_daemon_lock_namespace(docker_info)" in source + assert 'cleanup_errors.append(f"resource sampler stop failed: {exc}")' in run_one_source + assert 'cleanup_errors.append(f"browser tool close failed: {exc}")' in run_one_source + assert '"cleanup_errors": cleanup_errors' in run_one_source + + def test_formal_dataset_binding_requires_all_canonical_trajectories(tmp_path: Path) -> None: plan = { "seed_manifest_sha256": "seed-hash", From cf53a11f0c7f8c80cfe6ac5ecb59336671a07468 Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 15 Sep 2026 05:32:06 +0800 Subject: [PATCH 113/127] Localize replay report interface labels --- harness/trajectory_replay/webarena_report.py | 5 +++-- scripts/check_webarena_replay_report.py | 13 +++++++++++++ 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/harness/trajectory_replay/webarena_report.py b/harness/trajectory_replay/webarena_report.py index e2e33875b..28836773c 100644 --- a/harness/trajectory_replay/webarena_report.py +++ b/harness/trajectory_replay/webarena_report.py @@ -289,7 +289,7 @@ def _detail_page(
    {"复核根因" if zh else "Reviewed root cause"}{_e(label)}

    {_e(reason)}

    {"处理" if zh else "Remediation"}{_e(review.get("remediation_status") if review else "—")}

    {_e(review.get("remediation_owner") if review else "—")}

    -

    {"固定操作轨迹" if zh else "Fixed action trajectory"}

    {''.join(rows)}
    #ActionTargetTimeStatusResult
    +

    {"固定操作轨迹" if zh else "Fixed action trajectory"}

    {''.join(rows)}
    #{"动作" if zh else "Action"}{"目标" if zh else "Target"}{"耗时" if zh else "Time"}{"状态" if zh else "Status"}{"结果" if zh else "Result"}

    {"日志与证据" if zh else "Logs and evidence"}

    {screenshot}
    ''' return _shell(lang, "results", f"{method['label']} Task {attempt['task_id']}", body, depth=1) @@ -463,7 +463,8 @@ def build_webarena_replay_report( action_winners, lang=lang, ) - cells.append(f"{text}
    {metric['n']} calls") + calls_label = "次调用" if zh else "calls" + cells.append(f"{text}
    {metric['n']} {calls_label}") action_rows.append(f'{_e(action)}{"".join(cells)}') action_table = f'
    {"".join(f"" for h in action_headers)}{"".join(action_rows)}
    {_e(h)}
    ' method_cards = [] diff --git a/scripts/check_webarena_replay_report.py b/scripts/check_webarena_replay_report.py index 63b034743..c996acd90 100644 --- a/scripts/check_webarena_replay_report.py +++ b/scripts/check_webarena_replay_report.py @@ -61,6 +61,19 @@ def check(report_dir: Path) -> dict[str, int]: if "Action", + ">Target", + ">Time", + ">Status", + ">Result", + " calls", + ) + for page in [*core[: len(core_names)], *(root / "result").glob("*.html")]: + text = page.read_text(encoding="utf-8") + if any(marker in text for marker in untranslated_zh_markers): + raise ValueError(f"Chinese report contains untranslated interface text: {page}") + checked_links = 0 for page in root.rglob("*.html"): text = page.read_text(encoding="utf-8") From 2cd804f66ce2e4bc88aada9d8a8da7f89aefabdc Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 15 Sep 2026 05:46:46 +0800 Subject: [PATCH 114/127] Make replay release checks registry driven --- .../trajectory_replay/webarena_corrections.py | 3 ++ harness/trajectory_replay/webarena_report.py | 3 ++ scripts/check_webarena_replay_report.py | 29 +++++++++++++++---- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/harness/trajectory_replay/webarena_corrections.py b/harness/trajectory_replay/webarena_corrections.py index 0a4e07239..cc07725ad 100644 --- a/harness/trajectory_replay/webarena_corrections.py +++ b/harness/trajectory_replay/webarena_corrections.py @@ -273,6 +273,9 @@ def evaluate_corrected_contract( evaluator_context = context if ordinary_network_filtering and config.evaluator == "NetworkEventEvaluator": task_data = task.model_dump(mode="json") + # webarena-verified 1.2.3 reads the task type used by + # NetworkEventEvaluator from eval[0].expected, regardless of the + # network evaluator's own position in the task configuration. task_data["eval"][0]["expected"]["task_type"] = "MUTATE" filtering_task = type(task).model_validate(task_data) evaluator_context = context.model_copy(update={"task": filtering_task}) diff --git a/harness/trajectory_replay/webarena_report.py b/harness/trajectory_replay/webarena_report.py index 28836773c..35f2b3123 100644 --- a/harness/trajectory_replay/webarena_report.py +++ b/harness/trajectory_replay/webarena_report.py @@ -547,6 +547,9 @@ def build_webarena_replay_report( "results_sha256": _sha256(Path(results_path)), "dataset_sha256": _sha256(Path(dataset_path)), "review_sha256": _sha256(Path(review_path)), + "task_count": len(tasks), + "participant_count": len(methods), + "participants": list(methods), "logical_results": len(public_rows), "failed_results": sum(row["status"] != "pass" for row in public_rows), "reviewed_failures": len(reviews), diff --git a/scripts/check_webarena_replay_report.py b/scripts/check_webarena_replay_report.py index c996acd90..db038f8c3 100644 --- a/scripts/check_webarena_replay_report.py +++ b/scripts/check_webarena_replay_report.py @@ -21,17 +21,36 @@ def check(report_dir: Path) -> dict[str, int]: root = report_dir.resolve() manifest = json.loads((root / "report-manifest.json").read_text(encoding="utf-8")) results = json.loads((root / "data/results.json").read_text(encoding="utf-8")) - if manifest["logical_results"] != 180 or len(results) != 180: - raise ValueError("report must contain the complete 90-task participant matrix") + participants = manifest.get("participants") or [] + task_count = int(manifest.get("task_count") or 0) + participant_count = int(manifest.get("participant_count") or 0) + expected_results = task_count * participant_count + if ( + task_count < 1 + or participant_count < 1 + or len(participants) != participant_count + or len(set(participants)) != participant_count + or manifest["logical_results"] != expected_results + or len(results) != expected_results + ): + raise ValueError("report must contain the complete manifest-declared participant-task matrix") + task_ids = {int(item["task_id"]) for item in results} + result_keys = {(str(item["engine"]), int(item["task_id"])) for item in results} + expected_keys = {(participant, task_id) for participant in participants for task_id in task_ids} + if len(task_ids) != task_count or result_keys != expected_keys: + raise ValueError("report results do not match the manifest-declared participant-task matrix") failures = [item for item in results if item["status"] != "pass"] - if len(failures) != 36 or manifest["reviewed_failures"] != 36: - raise ValueError("report must contain all 36 reviewed final failures") + if ( + manifest["failed_results"] != len(failures) + or manifest["reviewed_failures"] != len(failures) + ): + raise ValueError("report must contain a review for every final failure") if any(not item["reason_zh"] or not item["reason_en"] for item in failures): raise ValueError("every failed result needs a bilingual reviewed cause") zh_details = {path.name for path in (root / "result").glob("*.html")} en_details = {path.name for path in (root / "en/result").glob("*.html")} - if zh_details != en_details or len(zh_details) != 180: + if zh_details != en_details or len(zh_details) != expected_results: raise ValueError("Chinese and English result routes must match one-to-one") core_names = ( "index.html", From f70963cb971c73a88f454a74a621f54e3d86fafb Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 15 Sep 2026 06:02:02 +0800 Subject: [PATCH 115/127] Handle replay release check edge cases --- scripts/check_webarena_replay_report.py | 16 +++++++ scripts/check_webarena_replay_visual.py | 22 ++++++---- .../test_webarena_visual_check.py | 42 +++++++++++++++++++ 3 files changed, 71 insertions(+), 9 deletions(-) create mode 100644 tests/trajectory_replay/test_webarena_visual_check.py diff --git a/scripts/check_webarena_replay_report.py b/scripts/check_webarena_replay_report.py index db038f8c3..461f16ea5 100644 --- a/scripts/check_webarena_replay_report.py +++ b/scripts/check_webarena_replay_report.py @@ -21,6 +21,22 @@ def check(report_dir: Path) -> dict[str, int]: root = report_dir.resolve() manifest = json.loads((root / "report-manifest.json").read_text(encoding="utf-8")) results = json.loads((root / "data/results.json").read_text(encoding="utf-8")) + required_manifest_fields = ( + "task_count", + "participant_count", + "participants", + "logical_results", + "failed_results", + "reviewed_failures", + ) + missing_manifest_fields = [ + field for field in required_manifest_fields if field not in manifest + ] + if missing_manifest_fields: + raise ValueError( + "report manifest is missing required fields: " + + ", ".join(missing_manifest_fields) + ) participants = manifest.get("participants") or [] task_count = int(manifest.get("task_count") or 0) participant_count = int(manifest.get("participant_count") or 0) diff --git a/scripts/check_webarena_replay_visual.py b/scripts/check_webarena_replay_visual.py index 605cdf4f1..1c4a7b840 100644 --- a/scripts/check_webarena_replay_visual.py +++ b/scripts/check_webarena_replay_visual.py @@ -45,8 +45,11 @@ def log_message(self, *_args: object) -> None: def _routes(report: Path) -> list[str]: - failed = json.loads((report / "data/results.json").read_text(encoding="utf-8")) - failed_key = next(item["key"] for item in failed if item["status"] == "fail") + results = json.loads((report / "data/results.json").read_text(encoding="utf-8")) + failed_key = next( + (item["key"] for item in results if item["status"] == "fail"), + None, + ) fixed = [ "index.html", "dataset.html", @@ -55,14 +58,15 @@ def _routes(report: Path) -> list[str]: "review.html", ] routes = [*fixed, *(f"en/{item}" for item in fixed)] - routes.extend( - ( - f"result/{failed_key}.html", - f"en/result/{failed_key}.html", - f"evidence-viewer.html?file=data/evidence/{failed_key}.json", - f"en/evidence-viewer.html?file=../data/evidence/{failed_key}.json", + if failed_key is not None: + routes.extend( + ( + f"result/{failed_key}.html", + f"en/result/{failed_key}.html", + f"evidence-viewer.html?file=data/evidence/{failed_key}.json", + f"en/evidence-viewer.html?file=../data/evidence/{failed_key}.json", + ) ) - ) return routes diff --git a/tests/trajectory_replay/test_webarena_visual_check.py b/tests/trajectory_replay/test_webarena_visual_check.py new file mode 100644 index 000000000..1be5919f3 --- /dev/null +++ b/tests/trajectory_replay/test_webarena_visual_check.py @@ -0,0 +1,42 @@ +import importlib.util +import json +from pathlib import Path + + +_SCRIPT = Path(__file__).parents[2] / "scripts/check_webarena_replay_visual.py" +_SPEC = importlib.util.spec_from_file_location("check_webarena_replay_visual", _SCRIPT) +assert _SPEC is not None and _SPEC.loader is not None +_MODULE = importlib.util.module_from_spec(_SPEC) +_SPEC.loader.exec_module(_MODULE) +_routes = _MODULE._routes + + +def _write_results(tmp_path, results): + data = tmp_path / "data" + data.mkdir() + (data / "results.json").write_text(json.dumps(results), encoding="utf-8") + + +def test_routes_include_failure_detail_and_evidence(tmp_path) -> None: + _write_results( + tmp_path, + [ + {"key": "chromium-1", "status": "pass"}, + {"key": "moli-1", "status": "fail"}, + ], + ) + + routes = _routes(tmp_path) + + assert "result/moli-1.html" in routes + assert "evidence-viewer.html?file=data/evidence/moli-1.json" in routes + assert len(routes) == 14 + + +def test_routes_support_an_all_pass_report(tmp_path) -> None: + _write_results(tmp_path, [{"key": "chromium-1", "status": "pass"}]) + + routes = _routes(tmp_path) + + assert all("result/" not in route and "evidence-viewer" not in route for route in routes) + assert len(routes) == 10 From 3f66f5672a37061bde243de828c51e321ac8f0f6 Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 15 Sep 2026 06:28:48 +0800 Subject: [PATCH 116/127] Derive replay report prose from evidence --- harness/trajectory_replay/webarena_report.py | 22 +++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/harness/trajectory_replay/webarena_report.py b/harness/trajectory_replay/webarena_report.py index 35f2b3123..71416c99c 100644 --- a/harness/trajectory_replay/webarena_report.py +++ b/harness/trajectory_replay/webarena_report.py @@ -428,6 +428,22 @@ def build_webarena_replay_report( _write_json(output / "data/results.json", public_rows) _write_json(output / "data/metrics.json", {"methods": method_rows, "passing_intersection": len(passing_tasks)}) + source_counts: dict[str, int] = defaultdict(int) + for task in tasks.values(): + source_counts[str((task.get("source") or {}).get("kind") or "unknown")] += 1 + browser_use_source_count = source_counts["authoritative_successful_browser_use_execution"] + repaired_source_count = source_counts["expert_repair_spec"] + reference_engine = str((profile.get("qualification") or {}).get("reference_engine") or "") + reference_method = methods.get(reference_engine) + reference_row = next( + (item for item in method_rows if item["engine"] == reference_engine), + None, + ) + if reference_method is None or reference_row is None: + raise ValueError("profile qualification reference engine must be a registered participant") + reference_label = _e(reference_method["label"]) + reviewed_failure_count = len(reviews) + for lang in ("zh", "en"): zh = lang == "zh" root = output if zh else output / "en" @@ -501,7 +517,7 @@ def build_webarena_replay_report( label = ("通过" if row["status"] == "pass" else "失败") if zh else row["status"].title() values.append(f'{label}') matrix_rows.append(f'{task_id}{_e(", ".join(task["task"].get("sites") or []))}{"".join(f"{v}" for v in values)}') - dataset_body = f'

    {"90 条标准轨迹" if zh else "90 qualified trajectories"}

    {"每题一条固定操作轨迹。60 条来自干净的 Browser Use 成功执行,30 条经修复后重新在 Chromium 验证。" if zh else "One fixed trajectory per task: 60 clean Browser Use successes and 30 repaired trajectories re-qualified on Chromium."}

    {"逐题结果矩阵" if zh else "Per-task result matrix"}

    {"".join(f"" for h in headers)}{"".join(matrix_rows)}
    {_e(h)}
    ' + dataset_body = f'

    {f"{task_count} 条标准轨迹" if zh else f"{task_count} qualified trajectories"}

    {f"每题一条固定操作轨迹。{browser_use_source_count} 条来自干净的 Browser Use 成功执行,{repaired_source_count} 条经修复后重新在 {reference_label} 验证。" if zh else f"One fixed trajectory per task: {browser_use_source_count} clean Browser Use successes and {repaired_source_count} repaired trajectories re-qualified on {reference_label}."}

    {"逐题结果矩阵" if zh else "Per-task result matrix"}

    {"".join(f"" for h in headers)}{"".join(matrix_rows)}
    {_e(h)}
    ' (root / "dataset.html").write_text(_shell(lang, "dataset", "Dataset", dataset_body), encoding="utf-8") result_rows = [] @@ -533,10 +549,10 @@ def build_webarena_replay_report( V --> X[Evidence-bound result] X --> T[Teardown browser and sites] T --> R''' - environment_body = f'''

    {"环境与评测流程" if zh else "Environment and evaluation flow"}

    {"192.168.5.2 是 Colima 虚拟机中的宿主网关:浏览器容器通过它访问运行在同一台 Mac 虚拟化环境里的 WebArena 站点。它不是公网测试网站。" if zh else "192.168.5.2 is the host gateway inside the Colima VM. Browser containers use it to reach the WebArena sites running in the same Mac virtualization environment; it is not a public test website."}

    {"执行拓扑" if zh else "Execution topology"}

    {fallback}{topology}

    {"隔离规则" if zh else "Isolation rules"}

    1

    {"一套固定端口站点环境,因此只允许一个方法运行。" if zh else "One fixed-port site stack permits only one running method."}

    90

    {"一个浏览器完整跑完全部任务,每题前恢复官方夹具。" if zh else "A browser completes all tasks; official fixtures reset before each task."}

    0

    {"没有 Agent 决策和模型请求;输入是冻结的 BrowserTool 动作。" if zh else "No Agent decisions or model calls; inputs are frozen BrowserTool actions."}

    ''' + environment_body = f'''

    {"环境与评测流程" if zh else "Environment and evaluation flow"}

    {"192.168.5.2 是 Colima 虚拟机中的宿主网关:浏览器容器通过它访问运行在同一台 Mac 虚拟化环境里的 WebArena 站点。它不是公网测试网站。" if zh else "192.168.5.2 is the host gateway inside the Colima VM. Browser containers use it to reach the WebArena sites running in the same Mac virtualization environment; it is not a public test website."}

    {"执行拓扑" if zh else "Execution topology"}

    {fallback}{topology}

    {"隔离规则" if zh else "Isolation rules"}

    1

    {"一套固定端口站点环境,因此只允许一个方法运行。" if zh else "One fixed-port site stack permits only one running method."}

    {task_count}

    {"一个浏览器完整跑完全部任务,每题前恢复官方夹具。" if zh else "A browser completes all tasks; official fixtures reset before each task."}

    0

    {"没有 Agent 决策和模型请求;输入是冻结的 BrowserTool 动作。" if zh else "No Agent decisions or model calls; inputs are frozen BrowserTool actions."}

    ''' (root / "environment.html").write_text(_shell(lang, "environment", "Environment", environment_body), encoding="utf-8") - review_body = f'''

    {"核验记录" if zh else "Review history"}

    {"记录影响当前数据集与结论的核验轮次;逐题证据位于结果详情页。" if zh else "Review rounds that changed the current dataset or conclusions. Per-result evidence remains on result pages."}

    2026-09-15{"90 条轨迹资格核验" if zh else "90-trajectory qualification"}

    {"修复已确认的非浏览器问题,重新验证后 Chromium 90/90。" if zh else "Confirmed non-browser defects were repaired; Chromium then qualified 90/90."}

    2026-09-15{"36 条失败根因复核" if zh else "36-failure root-cause review"}

    {"自动失败位置与复核根因分离;36/36 结果绑定当前证据哈希,无未解决项。" if zh else "Programmatic locations remain separate from reviewed causes; 36/36 are bound to the current evidence hash with no unresolved items."}

    ''' + review_body = f'''

    {"核验记录" if zh else "Review history"}

    {"记录影响当前数据集与结论的核验轮次;逐题证据位于结果详情页。" if zh else "Review rounds that changed the current dataset or conclusions. Per-result evidence remains on result pages."}

    2026-09-15{f"{task_count} 条轨迹资格核验" if zh else f"{task_count}-trajectory qualification"}

    {f"修复已确认的非浏览器问题,重新验证后 {reference_label} {reference_row['passed']}/{reference_row['total']}。" if zh else f"Confirmed non-browser defects were repaired; {reference_label} then qualified {reference_row['passed']}/{reference_row['total']}."}

    2026-09-15{f"{reviewed_failure_count} 条失败根因复核" if zh else f"{reviewed_failure_count}-failure root-cause review"}

    {f"自动失败位置与复核根因分离;{reviewed_failure_count}/{reviewed_failure_count} 结果绑定当前证据哈希,无未解决项。" if zh else f"Programmatic locations remain separate from reviewed causes; {reviewed_failure_count}/{reviewed_failure_count} are bound to the current evidence hash with no unresolved items."}

    ''' (root / "review.html").write_text(_shell(lang, "review", "Review history", review_body), encoding="utf-8") viewer_body = f'

    {"证据查看器" if zh else "Evidence viewer"}

    {"网页直接加载选中的净化证据;也可下载原始 JSON。" if zh else "Loads the selected sanitized evidence directly in the page; raw JSON remains downloadable."}

    ' From 17f2d1d49589510fe5ebff6e8363be53f281dd4f Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 15 Sep 2026 11:25:22 +0800 Subject: [PATCH 117/127] Make Artifact Site the replay release target --- .agents/skills/benchmark-report/SKILL.md | 1 + .env.example | 5 +- AGENTS.md | 2 +- Makefile | 9 +- README.md | 2 +- .../webarena-verified/current-run.json | 8 + harness/web_fetch/artifact_auth.py | 31 +- research/report-changes.json | 34 ++ scripts/authorize_artifact_hub.py | 11 +- .../package_webarena_replay_artifact_site.py | 119 +++++++ .../publish_webarena_replay_artifact_site.py | 293 ++++++++++++++++++ tests/test_artifact_auth.py | 21 +- .../test_webarena_publish.py | 102 ++++++ 13 files changed, 615 insertions(+), 23 deletions(-) create mode 100644 scripts/package_webarena_replay_artifact_site.py create mode 100644 scripts/publish_webarena_replay_artifact_site.py create mode 100644 tests/trajectory_replay/test_webarena_publish.py diff --git a/.agents/skills/benchmark-report/SKILL.md b/.agents/skills/benchmark-report/SKILL.md index 755cde462..141cbc225 100644 --- a/.agents/skills/benchmark-report/SKILL.md +++ b/.agents/skills/benchmark-report/SKILL.md @@ -21,6 +21,7 @@ Read the repository's `AGENTS.md` first. Project rules override this general ski 8. Build the public report from the frozen evidence. Every page should answer a reader question. Show task and run identifiers, the exact expected-versus-actual evidence behind a verdict, denominators and unavailable metrics, method differences, limits, concise reproduction instructions, and a compact review history. 9. Release only after code tests, evidence integrity, report links, responsive/visual checks, secret scans, target-site checks, and deployed-output verification pass. Preserve earlier sites unless replacement was explicitly requested. 10. Present one authoritative logical result per method and task. The frozen protocol chooses the score-bearing execution; a renderer must never silently replace that rule. Keep every physical call in the evidence chain and aggregate all latency and cost. Do not make execution ordinal labels part of the public comparison unless retry behavior is the declared subject. +11. In this repository, a finished report release defaults to Artifact Site at `https://artifact-hub.app.lexmount.com`, following its current [`for-agents.md`](https://artifact-hub.app.lexmount.com/for-agents.md). Build and validate before the external write, bind the returned slug, share URL, and version in the current profile, use optimistic locking for later updates, and verify both the unauthenticated share page and exported file tree. Use repository publish targets when available; do not reconstruct a target from prose or an earlier receipt. ## Stop conditions diff --git a/.env.example b/.env.example index 4ae1a0e6c..d006af421 100644 --- a/.env.example +++ b/.env.example @@ -20,8 +20,9 @@ KITESURF_CDP_ENDPOINT=wss://kitesurf.cloudflare.app/devtools/browser OPENAI_API_KEY= OPENAI_BASE_URL=https://api.openai.com/v1 -# Optional publishing override; default: ~/.config/artifact-hub/token -ARTIFACT_HUB_TOKEN= +# Optional Artifact Site publishing override; default: +# ~/.config/artifact-site/tokens/ +ARTIFACT_SITE_TOKEN= # Set explicitly in the environment for both report build and packaging. WEBFETCH_REPORT_PUBLIC_URL= diff --git a/AGENTS.md b/AGENTS.md index 1863aeaf6..66401f3c4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -6,7 +6,7 @@ - Treat `evaluation/current-report.json` as the only current matrix and publication-target authority. Publishing must use the bound site and expected remote version; do not copy target IDs from prose, old receipts or earlier reports. - Persist provider calls outside Git under `~/.browser-eval/` (or `BROWSER_EVAL_DATA_HOME`). Git may contain compact benchmark definitions, annotations, independent source evidence and public release metadata, but never measured outputs, execution manifests, provider artifacts, rerun directories, credentials or internal work logs. - Web Fetch keeps one current authoritative logical result per method and task. A declared failure retry retains every physical call, counts total retry latency and uses the final response, never the best response. A proven wrong command, version, scorer, reference or timing is a correction: preserve the prior evidence, supersede the complete declared scope and disclose score, denominator and latency effects. -- Evaluation credentials come only from the repository `.env`. Artifact Hub publishing is the sole user-scoped exception: read explicit `ARTIFACT_HUB_TOKEN`, then `~/.config/artifact-hub/tokens/`; never load `.env` for publishing. Save new authorizations per target host without overwriting another deployment's token. Keep `.env.example` current and publish only account requirements, official acquisition links and variable names. +- Evaluation credentials come only from the repository `.env`. Artifact Site publishing is the sole user-scoped exception: read explicit `ARTIFACT_SITE_TOKEN`, then `~/.config/artifact-site/tokens/`; the previous `ARTIFACT_HUB_TOKEN` and `~/.config/artifact-hub/` locations are read-only migration fallbacks. Never load `.env` for publishing. Save new authorizations per target host without overwriting another deployment's token. Keep `.env.example` current and publish only account requirements, official acquisition links and variable names. - Chinese and English are two localized views of one report model. Their HTML route inventories, section order, tables, values, filters and evidence links must match one-to-one. Localize interface and explanatory prose for each language; keep task names, IDs, URLs, references, provider output and other measured evidence unchanged. - Web, dynamic, popular-site and document text use shared `score_attempt` orchestration and one task-result predicate. Task-specific requirements live in case configuration, not provider- or track-specific scoring branches. Binary acceptance and text diagnostics remain separate; missing reference coverage is unavailable, not zero or perfect. - Dynamic-content reference capture follows `harness/web_fetch/README.md`: retain paired initial HTTP bytes and independently rendered visible text, bound completion and the full declared scope. Distinguish embedded state from follow-up fetching; never require JavaScript as the only valid route. diff --git a/Makefile b/Makefile index 4b2f20a3a..39c8e7fad 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ UV ?= uv PY := $(UV) run --frozen python -.PHONY: setup runtime-check naming-check test test-python test-ui fixture-smoke ci report report-check browser-use-review browser-use-report browser-use-report-check browser-use-visual-check browser-use-release-check trajectory-replay-report trajectory-replay-report-check trajectory-replay-visual-check trajectory-replay-release-check +.PHONY: setup runtime-check naming-check test test-python test-ui fixture-smoke ci report report-check browser-use-review browser-use-report browser-use-report-check browser-use-visual-check browser-use-release-check trajectory-replay-report trajectory-replay-report-check trajectory-replay-visual-check trajectory-replay-release-check trajectory-replay-package trajectory-replay-publish setup: $(UV) sync --frozen --all-extras @@ -71,3 +71,10 @@ trajectory-replay-visual-check: runtime-check trajectory-replay-release-check: trajectory-replay-report-check $(MAKE) trajectory-replay-visual-check + +trajectory-replay-package: runtime-check + @test -n "$(EVIDENCE)" || (echo 'EVIDENCE=/absolute/path/effective-results.json is required' >&2; exit 2) + $(PY) scripts/package_webarena_replay_artifact_site.py --evidence "$(EVIDENCE)" + +trajectory-replay-publish: runtime-check + $(PY) scripts/publish_webarena_replay_artifact_site.py diff --git a/README.md b/README.md index fcfc6d7ba..9a77f0045 100644 --- a/README.md +++ b/README.md @@ -164,7 +164,7 @@ uv run --frozen python scripts/verify_artifact_hub.py --slug UUAhhQMnrGwm --expe uv run --frozen python scripts/publish_artifact_hub.py --receipt publish/headless-site-receipt.json --update --expected-version <新站点当前服务端版本标识> ``` -打包输出 `publish/webfetch-sites.zip`,将完整证据去重压缩并按需加载。Sites 发布令牌依次从显式进程环境 `ARTIFACT_HUB_TOKEN`、目标服务的 `~/.config/artifact-hub/tokens/` 读取,不加载仓库 `.env`。新授权按目标 host 保存,文件权限为 0600;不同部署的令牌不通用,也不互相覆盖。host 包含显式端口(如有)。其他评测密钥仍只读取仓库配置。当前五浏览器报告使用 `https://artifact-hub.app.lexmount.com` 的独立站点 `UUAhhQMnrGwm`;旧 `.dev` 站点 `bWkRP157EtF6` 和原报告 `UeZE1vfcEOrM` 均保留。构建和打包须使用当前报告配置中的公开地址;回执的服务地址与 slug 共同决定更新目标。服务端版本标识仅用于并发保护。发布令牌和管理回执不得提交或放入站点。 +打包输出 `publish/webfetch-sites.zip`,将完整证据去重压缩并按需加载。Artifact Site 发布令牌依次从显式进程环境 `ARTIFACT_SITE_TOKEN`、目标服务的 `~/.config/artifact-site/tokens/` 读取,不加载仓库 `.env`;旧产品名下的环境变量和凭据路径只作为迁移回退。新授权按目标 host 保存,文件权限为 0600;不同部署的令牌不通用,也不互相覆盖。host 包含显式端口(如有)。其他评测密钥仍只读取仓库配置。当前五浏览器报告使用 `https://artifact-hub.app.lexmount.com` 的独立站点 `UUAhhQMnrGwm`;旧 `.dev` 站点 `bWkRP157EtF6` 和原报告 `UeZE1vfcEOrM` 均保留。构建和打包须使用当前报告配置中的公开地址;回执的服务地址与 slug 共同决定更新目标。服务端版本标识仅用于并发保护。发布令牌和管理回执不得提交或放入站点。 ## 内容规范 diff --git a/evaluation/trajectory-replay/webarena-verified/current-run.json b/evaluation/trajectory-replay/webarena-verified/current-run.json index a15fa17df..e9fca59b8 100644 --- a/evaluation/trajectory-replay/webarena-verified/current-run.json +++ b/evaluation/trajectory-replay/webarena-verified/current-run.json @@ -53,5 +53,13 @@ "authority": "official programmatic evaluator or a hash-bound replay correction", "promotion_gate": "the canonical trajectory passes execution and official evaluation on the frozen reference browser before any measured browser comparison", "model_calls": 0 + }, + "publication": { + "base_url": "https://artifact-hub.app.lexmount.com", + "title": "WebArena-Verified 浏览器轨迹重放评测", + "state": "verified", + "site_slug": "7dF7uNK5SKIx", + "share_url": "https://artifact-hub.app.lexmount.com/v/hkZt38QFBpNIKtThKnqjpnBQk8At19PM", + "expected_version": "ver_f4cebfc3cc804813bf40cb1deeeb4498" } } diff --git a/harness/web_fetch/artifact_auth.py b/harness/web_fetch/artifact_auth.py index 478a3e15a..dcaff90a2 100644 --- a/harness/web_fetch/artifact_auth.py +++ b/harness/web_fetch/artifact_auth.py @@ -1,4 +1,4 @@ -"""Artifact Hub's user-scoped publishing credential, separate from eval secrets.""" +"""Artifact Site's user-scoped publishing credential, separate from eval secrets.""" import os from pathlib import Path import stat @@ -8,29 +8,44 @@ def token_path(base_url): - root = Path.home() / '.config/artifact-hub' + root = Path.home() / '.config/artifact-site' parsed = urlsplit(base_url) host = parsed.netloc.lower() if (parsed.scheme not in {'http', 'https'} or not parsed.hostname or parsed.username is not None or parsed.password is not None or host in {'.', '..'} or any(c.isspace() or c in '/\\' for c in host)): - raise ValueError('Artifact Hub base URL requires an HTTP(S) host without credentials') + raise ValueError('Artifact Site base URL requires an HTTP(S) host without credentials') # Include an explicit port, matching the server manual's host key. return root / 'tokens' / host def validate_token(token): if not token.startswith('ahp_') or any(c.isspace() for c in token): - raise ValueError('Invalid Artifact Hub token format') + raise ValueError('Invalid Artifact Site token format') return token def load_token(*, base_url): - # Explicit process environment, then the target host. Never .env. - path = token_path(base_url) - token = os.environ.get('ARTIFACT_HUB_TOKEN', '').strip() + # Explicit process environment, then the target host. Never .env. Previous + # product-name locations remain read-only migration fallbacks. + host = urlsplit(base_url).netloc.lower() + token = os.environ.get('ARTIFACT_SITE_TOKEN', '').strip() if not token: - token = path.read_text().strip() + token = os.environ.get('ARTIFACT_HUB_TOKEN', '').strip() + candidates = ( + token_path(base_url), + Path.home() / '.config/artifact-hub' / 'tokens' / host, + Path.home() / '.config/artifact-site' / 'token', + Path.home() / '.config/artifact-hub' / 'token', + ) + if not token: + for path in candidates: + try: + token = path.read_text().strip() + except FileNotFoundError: + continue + if token: + break return validate_token(token) diff --git a/research/report-changes.json b/research/report-changes.json index 5d9ddf84f..4fecc7f55 100644 --- a/research/report-changes.json +++ b/research/report-changes.json @@ -4,6 +4,40 @@ "history_scope_i18n": { "en": "This history begins with revisions supported by a complete audit trail. Earlier Artifact Hub versions remain available, but no itemized history is reconstructed where the supporting record is incomplete." }, + "trajectory_replay_releases": [ + { + "id": "2026-09-15-webarena-verified-90-artifact-site", + "date": "2026-09-15", + "title": "发布 90 题浏览器轨迹重放评测", + "summary": "发布 Chromium 与 Moli 1.1.3 的 90 题固定轨迹重放结果:Chromium 90/90,Moli 54/90;36 个最终失败全部完成证据绑定复核。Agent 与模型调用为零。", + "publication": { + "service": "Artifact Site", + "site_slug": "7dF7uNK5SKIx", + "share_url": "https://artifact-hub.app.lexmount.com/v/hkZt38QFBpNIKtThKnqjpnBQk8At19PM", + "version": "ver_f4cebfc3cc804813bf40cb1deeeb4498" + }, + "effects": { + "score": "首次发布;Chromium 90/90,Moli 54/90。", + "denominator": "每个浏览器 90 题,共 180 个逻辑结果。", + "latency": "发布均值、P50 与 P95;共同通过的 54 题用于端到端、CPU 与内存比较。", + "evidence": "591 个公开文件、372 个 HTML 页面和 4,856 条本地链接通过检查;36/36 个失败已复核。", + "limitations": "每个浏览器每题只有一个正式逻辑结果;结论限于冻结任务、版本和本地环境。" + }, + "i18n": { + "en": { + "title": "Publish the 90-task browser trajectory-replay evaluation", + "summary": "Published fixed-trajectory replay results for Chromium and Moli 1.1.3 across 90 tasks: Chromium 90/90 and Moli 54/90. All 36 final failures have evidence-bound reviews. Agent and model calls are zero.", + "effects": { + "score": "Initial release: Chromium 90/90 and Moli 54/90.", + "denominator": "90 tasks per browser and 180 logical results in total.", + "latency": "Mean, P50, and P95 are reported; end-to-end, CPU, and memory comparisons use the 54 tasks both browsers passed.", + "evidence": "Checks passed for 591 public files, 372 HTML pages, and 4,856 local links; all 36 failures are reviewed.", + "limitations": "There is one formal logical result per browser and task. Conclusions are limited to the frozen tasks, versions, and local environment." + } + } + } + } + ], "browser_use_reviews": [ { "id": "review-2026-09-14-complete-90-task-primary-result", diff --git a/scripts/authorize_artifact_hub.py b/scripts/authorize_artifact_hub.py index 0067bea6f..87941fb99 100644 --- a/scripts/authorize_artifact_hub.py +++ b/scripts/authorize_artifact_hub.py @@ -1,4 +1,4 @@ -"""Authorize Artifact Hub and save its token in the user-scoped credential file.""" +"""Authorize Artifact Site and save its token in the user-scoped credential file.""" import argparse import json import os @@ -18,10 +18,15 @@ def main(): action.add_argument('--persist-existing', action='store_true') parser.add_argument('--timeout', type=int, default=540) parser.add_argument('--replace-existing', action='store_true', help='Explicitly back up and replace a different user token') - parser.add_argument('--base-url',default=os.environ.get('ARTIFACT_HUB_BASE_URL','https://artifact-hub.app.lexmount.com')) + parser.add_argument('--base-url', default=os.environ.get( + 'ARTIFACT_SITE_BASE_URL', + os.environ.get('ARTIFACT_HUB_BASE_URL', 'https://artifact-hub.app.lexmount.com'), + )) args = parser.parse_args() if args.persist_existing: - backup=save_token(os.environ.get('ARTIFACT_HUB_TOKEN', '').strip(),base_url=args.base_url,replace_existing=args.replace_existing) + token = (os.environ.get('ARTIFACT_SITE_TOKEN', '').strip() + or os.environ.get('ARTIFACT_HUB_TOKEN', '').strip()) + backup=save_token(token,base_url=args.base_url,replace_existing=args.replace_existing) if backup:print('Previous credential backed up:',backup) print('Existing authorization saved to the user credential file; token not displayed.') return diff --git a/scripts/package_webarena_replay_artifact_site.py b/scripts/package_webarena_replay_artifact_site.py new file mode 100644 index 000000000..15dcb5307 --- /dev/null +++ b/scripts/package_webarena_replay_artifact_site.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python3 +"""Validate and package the current trajectory-replay report for Artifact Site.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import zipfile +from pathlib import Path + +if __package__: + from scripts.check_webarena_replay_report import check as check_report + from scripts.check_webarena_replay_visual import check as check_visual +else: + from check_webarena_replay_report import check as check_report + from check_webarena_replay_visual import check as check_visual + +PROFILE = Path("evaluation/trajectory-replay/webarena-verified/current-run.json") +DATASET = Path("evaluation/trajectory-replay/webarena-verified/trajectories.json") +REVIEW = Path("research/trajectory-replay-failure-review.json") +ARCHIVE = Path("publish/trajectory-replay-artifact-site.zip") +ATTESTATION = Path("publish/trajectory-replay-artifact-site.attestation.json") +VISUAL_RECEIPT = Path("publish/trajectory-replay-visual-qa.json") +MAX_FILES = 2_000 +MAX_ARCHIVE_BYTES = 24 * 1024 * 1024 +SKIP_PARTS = {".git", "node_modules", "__MACOSX"} + + +def sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def tree_sha256(root: Path) -> str: + digest = hashlib.sha256() + for path in sorted(item for item in root.rglob("*") if item.is_file()): + relative = path.relative_to(root).as_posix().encode() + digest.update(relative) + digest.update(hashlib.sha256(path.read_bytes()).digest()) + return digest.hexdigest() + + +def validate_release(report: Path, visual_receipt: Path) -> dict[str, int]: + summary = check_report(report) + try: + visual = json.loads(visual_receipt.read_text(encoding="utf-8")) + except (OSError, ValueError): + visual = {} + if ( + visual.get("schema") != "lex_webarena_replay_visual_qa/1" + or visual.get("report_tree_sha256") != tree_sha256(report) + or visual.get("failures") + or int(visual.get("states_checked") or 0) <= 0 + ): + check_visual(report, visual_receipt) + return summary + + +def package(report: Path, evidence: Path, output: Path, visual_receipt: Path) -> dict: + if not evidence.is_file(): + raise ValueError(f"missing replay evidence: {evidence}") + summary = validate_release(report, visual_receipt) + files = sorted(path for path in report.rglob("*") if path.is_file()) + if not files or len(files) > MAX_FILES: + raise ValueError(f"Artifact Site file count is outside 1..{MAX_FILES}: {len(files)}") + output.parent.mkdir(parents=True, exist_ok=True) + with zipfile.ZipFile(output, "w", zipfile.ZIP_DEFLATED) as archive: + for path in files: + relative = path.relative_to(report) + if path.is_symlink() or SKIP_PARTS.intersection(relative.parts): + raise ValueError(f"unsafe publication path: {relative}") + if any(part.startswith(".") for part in relative.parts): + raise ValueError(f"dot-leading publication path is unsupported: {relative}") + archive.write(path, relative.as_posix()) + if output.stat().st_size > MAX_ARCHIVE_BYTES: + raise ValueError("publication package exceeds Artifact Site one-shot upload limit") + attestation = { + "schema": "lex_webarena_replay_publication_attestation/1", + "archive": str(output), + "archive_sha256": sha256(output), + "archive_bytes": output.stat().st_size, + "report": str(report), + "report_tree_sha256": tree_sha256(report), + "file_count": len(files), + "profile_sha256": sha256(PROFILE), + "dataset_sha256": sha256(DATASET), + "review_sha256": sha256(REVIEW), + "evidence": str(evidence), + "evidence_sha256": sha256(evidence), + **summary, + } + ATTESTATION.write_text( + json.dumps(attestation, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + return attestation + + +def main() -> None: + parser = argparse.ArgumentParser() + parser.add_argument("--report", type=Path, default=Path("reports/trajectory-replay")) + parser.add_argument("--evidence", type=Path, required=True) + parser.add_argument("--output", type=Path, default=ARCHIVE) + parser.add_argument("--visual-receipt", type=Path, default=VISUAL_RECEIPT) + args = parser.parse_args() + print( + json.dumps( + package(args.report, args.evidence, args.output, args.visual_receipt), + ensure_ascii=False, + ) + ) + + +if __name__ == "__main__": + main() diff --git a/scripts/publish_webarena_replay_artifact_site.py b/scripts/publish_webarena_replay_artifact_site.py new file mode 100644 index 000000000..e47b6621f --- /dev/null +++ b/scripts/publish_webarena_replay_artifact_site.py @@ -0,0 +1,293 @@ +#!/usr/bin/env python3 +"""Create or atomically update the current replay report on Artifact Site.""" + +from __future__ import annotations + +import argparse +import hashlib +import json +import os +import tempfile +import urllib.parse +import urllib.request +import uuid +import zipfile +from pathlib import Path +from urllib.error import HTTPError + +from harness.web_fetch.artifact_auth import load_token + +ARCHIVE = Path("publish/trajectory-replay-artifact-site.zip") +ATTESTATION = Path("publish/trajectory-replay-artifact-site.attestation.json") +RECEIPT = Path("publish/trajectory-replay-artifact-site-receipt.json") +PROFILE = Path("evaluation/trajectory-replay/webarena-verified/current-run.json") +DATASET = Path("evaluation/trajectory-replay/webarena-verified/trajectories.json") +REVIEW = Path("research/trajectory-replay-failure-review.json") +PUBLIC_REPORT_MARKER = "Browser Trajectory Replay" + + +def sha256(path: Path) -> str: + digest = hashlib.sha256() + with path.open("rb") as stream: + for chunk in iter(lambda: stream.read(1024 * 1024), b""): + digest.update(chunk) + return digest.hexdigest() + + +def tree_sha256(root: Path) -> str: + digest = hashlib.sha256() + for path in sorted(item for item in root.rglob("*") if item.is_file()): + relative = path.relative_to(root).as_posix().encode() + digest.update(relative) + digest.update(hashlib.sha256(path.read_bytes()).digest()) + return digest.hexdigest() + + +def response_json(status: int, raw: bytes, *, operation: str) -> dict: + if not 200 <= status < 300: + detail = raw.decode("utf-8", errors="replace").strip()[:500] or "empty response" + raise RuntimeError(f"{operation} failed with HTTP {status}: {detail}") + try: + value = json.loads(raw) + except (TypeError, ValueError) as error: + raise RuntimeError(f"{operation} returned HTTP {status} without valid JSON") from error + if not isinstance(value, dict): + raise RuntimeError(f"{operation} returned non-object JSON") + return value + + +def publication_target(*, require_bound: bool) -> tuple[dict, str, str, str]: + profile = json.loads(PROFILE.read_text(encoding="utf-8")) + publication = profile.get("publication") or {} + base = str(publication.get("base_url") or "").rstrip("/") + slug = str(publication.get("site_slug") or "") + version = str(publication.get("expected_version") or "") + if not base.startswith("https://"): + raise ValueError("current replay profile has no valid Artifact Site base URL") + if require_bound and (not slug or not version.startswith("ver_")): + raise ValueError("current replay profile has no bound Artifact Site target") + return profile, base, slug, version + + +def request( + base: str, + path: str, + data: bytes | None = None, + content_type: str | None = None, + *, + method: str | None = None, + authenticated: bool = True, +) -> tuple[int, bytes, dict[str, str]]: + headers = {"User-Agent": "Trajectory-Replay-Artifact-Site-Publisher/1.0"} + if authenticated: + headers["Authorization"] = "Bearer " + load_token(base_url=base) + if content_type: + headers["Content-Type"] = content_type + req = urllib.request.Request(base + path, data=data, headers=headers, method=method) + try: + with urllib.request.urlopen(req, timeout=240) as response: + return response.status, response.read(), dict(response.headers.items()) + except HTTPError as error: + try: + return error.code, error.read(), dict(error.headers.items()) + finally: + error.close() + + +def validate_package() -> dict: + if not ARCHIVE.is_file() or not ATTESTATION.is_file(): + raise ValueError("missing validated replay package or attestation") + attestation = json.loads(ATTESTATION.read_text(encoding="utf-8")) + report = Path(str(attestation.get("report") or "")) + evidence = Path(str(attestation.get("evidence") or "")) + checks = { + "schema": attestation.get("schema") == "lex_webarena_replay_publication_attestation/1", + "archive": sha256(ARCHIVE) == attestation.get("archive_sha256"), + "report": report.is_dir() and tree_sha256(report) == attestation.get("report_tree_sha256"), + "profile": sha256(PROFILE) == attestation.get("profile_sha256"), + "dataset": sha256(DATASET) == attestation.get("dataset_sha256"), + "review": sha256(REVIEW) == attestation.get("review_sha256"), + "evidence": evidence.is_file() and sha256(evidence) == attestation.get("evidence_sha256"), + } + failed = [name for name, valid in checks.items() if not valid] + if failed: + raise ValueError("publication package is stale: " + ", ".join(failed)) + return attestation + + +def multipart_archive(title: str) -> tuple[bytes, str]: + boundary = "----trajectoryreplay" + uuid.uuid4().hex + parts = [] + for key, value in (("mode", "zip"), ("title", title)): + parts.append( + ( + f'--{boundary}\r\nContent-Disposition: form-data; name="{key}"\r\n\r\n{value}\r\n' + ).encode() + ) + parts.append( + ( + f'--{boundary}\r\nContent-Disposition: form-data; name="file"; ' + 'filename="browser-trajectory-replay.zip"\r\n' + "Content-Type: application/zip\r\n\r\n" + ).encode() + + ARCHIVE.read_bytes() + + b"\r\n" + ) + parts.append(f"--{boundary}--\r\n".encode()) + return b"".join(parts), "multipart/form-data; boundary=" + boundary + + +def current_version(site: dict) -> str: + nested = site.get("site") if isinstance(site.get("site"), dict) else site + version = str(nested.get("currentVersionId") or (nested.get("version") or {}).get("id") or "") + if not version.startswith("ver_"): + raise RuntimeError("Artifact Site metadata omitted current version") + return version + + +def public_share_path(base: str, share_url: str) -> str: + target = urllib.parse.urlsplit(share_url) + authority = urllib.parse.urlsplit(base) + if ( + target.scheme != authority.scheme + or target.netloc != authority.netloc + or not target.path.startswith("/v/") + or target.query + or target.fragment + ): + raise ValueError("current replay profile has no valid same-origin public share URL") + return target.path + + +def archive_hashes(path: Path) -> dict[str, str]: + with zipfile.ZipFile(path) as archive: + return { + item.filename: hashlib.sha256(archive.read(item)).hexdigest() + for item in archive.infolist() + if not item.is_dir() + } + + +def verify_export(base: str, slug: str) -> None: + status, raw, _ = request(base, f"/api/sites/{urllib.parse.quote(slug)}/export") + if status != 200: + response_json(status, raw, operation="site export") + with tempfile.NamedTemporaryFile(suffix=".zip") as stream: + stream.write(raw) + stream.flush() + if archive_hashes(Path(stream.name)) != archive_hashes(ARCHIVE): + raise RuntimeError("deployed Artifact Site file tree differs from package") + + +def bind_publication(profile: dict, *, slug: str, share_url: str, version: str) -> None: + publication = profile.setdefault("publication", {}) + publication.update( + { + "site_slug": slug, + "share_url": share_url, + "expected_version": version, + "state": "verified", + } + ) + PROFILE.write_text(json.dumps(profile, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + attestation = json.loads(ATTESTATION.read_text(encoding="utf-8")) + attestation["profile_sha256"] = sha256(PROFILE) + ATTESTATION.write_text( + json.dumps(attestation, ensure_ascii=False, indent=2) + "\n", + encoding="utf-8", + ) + + +def save_receipt(value: dict) -> None: + safe = { + key: item for key, item in value.items() if key not in {"editToken", "claimToken", "token"} + } + RECEIPT.parent.mkdir(parents=True, exist_ok=True) + RECEIPT.write_text(json.dumps(safe, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") + os.chmod(RECEIPT, 0o600) + + +def main(*, create: bool) -> None: + validate_package() + profile, base, bound_slug, expected_version = publication_target(require_bound=not create) + publication = profile["publication"] + title = str(publication.get("title") or "Browser Trajectory Replay") + payload, content_type = multipart_archive(title) + if create: + if bound_slug or expected_version: + raise ValueError("replay report is already bound; update it without --create") + status, raw, _ = request(base, "/api/sites", payload, content_type) + created = response_json(status, raw, operation="site creation") + slug = str(created.get("slug") or "") + if not slug: + raise RuntimeError("site creation response omitted slug") + save_receipt(created) + share_status, share_raw, _ = request( + base, + f"/api/sites/{urllib.parse.quote(slug)}/shares", + json.dumps({"policy": "public"}).encode(), + "application/json", + method="POST", + ) + share = response_json(share_status, share_raw, operation="public share creation") + share_url = str(share.get("url") or "") + if not share_url.startswith("https://"): + raise RuntimeError("share creation response omitted public URL") + else: + slug = bound_slug + share_url = str(publication.get("share_url") or "") + status, raw, _ = request(base, f"/api/sites/{urllib.parse.quote(slug)}") + before = response_json(status, raw, operation="site lookup") + if current_version(before) != expected_version: + raise ValueError("Artifact Site version differs from current profile authority") + query = urllib.parse.urlencode({"expected_version": expected_version}) + status, raw, _ = request( + base, + f"/api/sites/{urllib.parse.quote(slug)}/versions?{query}", + payload, + content_type, + ) + response_json(status, raw, operation="site version upload") + status, raw, _ = request(base, f"/api/sites/{urllib.parse.quote(slug)}") + metadata = response_json(status, raw, operation="site lookup after upload") + version = current_version(metadata) + verify_export(base, slug) + public_status, public_body, _ = request( + base, + public_share_path(base, share_url), + authenticated=False, + ) + if public_status != 200: + response_json(public_status, public_body, operation="public share verification") + preview_status, preview_body, _ = request( + base, + f"/api/preview/{urllib.parse.quote(slug)}", + authenticated=False, + ) + if preview_status != 200 or PUBLIC_REPORT_MARKER not in preview_body.decode( + "utf-8", errors="replace" + ): + raise RuntimeError("deployed preview is unavailable or missing report marker") + bind_publication(profile, slug=slug, share_url=share_url, version=version) + save_receipt( + { + "base_url": base, + "slug": slug, + "share_url": share_url, + "version": version, + "archive_sha256": sha256(ARCHIVE), + "verified": True, + } + ) + print( + json.dumps( + {"slug": slug, "url": share_url, "version": version}, + ensure_ascii=False, + ) + ) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument("--create", action="store_true") + main(create=parser.parse_args().create) diff --git a/tests/test_artifact_auth.py b/tests/test_artifact_auth.py index 3e244a207..5dca7b30c 100644 --- a/tests/test_artifact_auth.py +++ b/tests/test_artifact_auth.py @@ -13,7 +13,7 @@ class ArtifactAuthTests(unittest.TestCase): def test_authorization_persists_for_target_without_loading_repository_env(self): script = Path(__file__).resolve().parents[1] / 'scripts/authorize_artifact_hub.py' - with patch.dict(os.environ, {'ARTIFACT_HUB_TOKEN': 'ahp_process_fixture'}, clear=True), \ + with patch.dict(os.environ, {'ARTIFACT_SITE_TOKEN': 'ahp_process_fixture'}, clear=True), \ patch('sys.argv', [str(script), '--persist-existing', '--base-url', 'https://app.example']), \ patch('harness.web_fetch.config.load_runtime_env', side_effect=AssertionError('must not read .env')), \ patch('harness.web_fetch.artifact_auth.save_token', return_value=None) as save, \ @@ -29,8 +29,7 @@ def test_per_host_precedence_and_independent_saves(self): legacy.write_text('ahp_legacy_fixture') app = 'https://artifact-hub.app.example' dev = 'https://artifact-hub.dev.example' - with self.assertRaises(FileNotFoundError): - load_token(base_url=app) + self.assertEqual(load_token(base_url=app), 'ahp_legacy_fixture') save_token('ahp_app_fixture', base_url=app) save_token('ahp_dev_fixture', base_url=dev) self.assertEqual(load_token(base_url=app), 'ahp_app_fixture') @@ -40,15 +39,14 @@ def test_per_host_precedence_and_independent_saves(self): with patch.dict(os.environ, {'ARTIFACT_HUB_TOKEN': 'ahp_environment_fixture'}): self.assertEqual(load_token(base_url=app), 'ahp_environment_fixture') token_path(app).write_text('') - with self.assertRaises(ValueError): - load_token(base_url=app) + self.assertEqual(load_token(base_url=app), 'ahp_legacy_fixture') token_path(app).write_text('not-a-valid-token') with self.assertRaises(ValueError): load_token(base_url=app) def test_host_key_includes_port_and_cannot_escape_credential_directory(self): with tempfile.TemporaryDirectory() as directory, patch('pathlib.Path.home', return_value=Path(directory)): - root = Path(directory) / '.config/artifact-hub/tokens' + root = Path(directory) / '.config/artifact-site/tokens' self.assertEqual(token_path('https://EXAMPLE.com:8443/path'), root / 'example.com:8443') for invalid in ['file:///tmp/token', 'https://..', 'https://.', 'https://user:password@example.com', 'https://example.com\\evil']: @@ -77,9 +75,18 @@ def test_store_and_reuse_without_repository_env(self): self.assertEqual(load_token(base_url='https://app.example'), 'ahp_test_fixture') def test_explicit_override(self): - with patch.dict(os.environ, {'ARTIFACT_HUB_TOKEN': 'ahp_override_fixture'}): + with patch.dict(os.environ, {'ARTIFACT_SITE_TOKEN': 'ahp_override_fixture'}): self.assertEqual(load_token(base_url='https://missing.example'), 'ahp_override_fixture') + def test_previous_name_is_a_read_only_migration_fallback(self): + with tempfile.TemporaryDirectory() as directory, patch('pathlib.Path.home', return_value=Path(directory)), patch.dict(os.environ, {}, clear=True): + old = Path(directory) / '.config/artifact-hub/tokens/app.example' + old.parent.mkdir(parents=True) + old.write_text('ahp_old_name_fixture') + self.assertEqual(load_token(base_url='https://app.example'), 'ahp_old_name_fixture') + save_token('ahp_new_name_fixture', base_url='https://new.example') + self.assertIn('.config/artifact-site/', str(token_path('https://new.example'))) + def test_invalid_and_symlink(self): with tempfile.TemporaryDirectory() as directory, patch('pathlib.Path.home', return_value=Path(directory)): path = token_path('https://app.example') diff --git a/tests/trajectory_replay/test_webarena_publish.py b/tests/trajectory_replay/test_webarena_publish.py new file mode 100644 index 000000000..818c0b531 --- /dev/null +++ b/tests/trajectory_replay/test_webarena_publish.py @@ -0,0 +1,102 @@ +from __future__ import annotations + +import hashlib +import json +from pathlib import Path + +import scripts.package_webarena_replay_artifact_site as packager +import scripts.publish_webarena_replay_artifact_site as publisher + + +def _sha256(path: Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def test_response_json_keeps_http_failure_details() -> None: + assert publisher.response_json(200, b'{"ok":true}', operation="upload") == {"ok": True} + for status, payload, expected in ( + (413, b'{"error":"too large"}', "HTTP 413"), + (502, b"", "empty response"), + (200, b"", "without valid JSON"), + ): + try: + publisher.response_json(status, payload, operation="upload") + except RuntimeError as error: + assert expected in str(error) + else: + raise AssertionError("invalid response must fail") + + +def test_public_share_must_be_same_origin_and_token_route() -> None: + base = "https://artifact-hub.app.lexmount.com" + assert publisher.public_share_path(base, base + "/v/token") == "/v/token" + for url in ( + "https://other.example/v/token", + base + "/s/slug", + base + "/v/token?owner=true", + ): + try: + publisher.public_share_path(base, url) + except ValueError: + pass + else: + raise AssertionError("unsafe public share URL must be rejected") + + +def test_bind_publication_updates_profile_and_attestation(monkeypatch, tmp_path: Path) -> None: + profile = tmp_path / "profile.json" + profile.write_text( + json.dumps({"publication": {"base_url": "https://app.example"}}), + encoding="utf-8", + ) + attestation = tmp_path / "attestation.json" + attestation.write_text(json.dumps({"profile_sha256": "old"}), encoding="utf-8") + monkeypatch.setattr(publisher, "PROFILE", profile) + monkeypatch.setattr(publisher, "ATTESTATION", attestation) + + publisher.bind_publication( + json.loads(profile.read_text()), + slug="abc", + share_url="https://app.example/v/token", + version="ver_new", + ) + + publication = json.loads(profile.read_text())["publication"] + assert publication == { + "base_url": "https://app.example", + "site_slug": "abc", + "share_url": "https://app.example/v/token", + "expected_version": "ver_new", + "state": "verified", + } + assert json.loads(attestation.read_text())["profile_sha256"] == _sha256(profile) + + +def test_package_binds_report_evidence_and_release_gate(monkeypatch, tmp_path: Path) -> None: + report = tmp_path / "report" + report.mkdir() + (report / "index.html").write_text("", encoding="utf-8") + evidence = tmp_path / "effective-results.json" + evidence.write_text("{}", encoding="utf-8") + profile = tmp_path / "profile.json" + dataset = tmp_path / "dataset.json" + review = tmp_path / "review.json" + for path in (profile, dataset, review): + path.write_text("{}", encoding="utf-8") + output = tmp_path / "report.zip" + attestation = tmp_path / "attestation.json" + monkeypatch.setattr(packager, "PROFILE", profile) + monkeypatch.setattr(packager, "DATASET", dataset) + monkeypatch.setattr(packager, "REVIEW", review) + monkeypatch.setattr(packager, "ATTESTATION", attestation) + monkeypatch.setattr( + packager, + "validate_release", + lambda _report, _receipt: {"html_pages": 1, "checked_links": 0}, + ) + + result = packager.package(report, evidence, output, tmp_path / "visual.json") + + assert result["archive_sha256"] == _sha256(output) + assert result["evidence_sha256"] == _sha256(evidence) + assert result["file_count"] == 1 From 3c7fe3cea15367e40a890977ead99f3fbe896f6b Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 15 Sep 2026 11:39:51 +0800 Subject: [PATCH 118/127] Resolve final replay review findings --- harness/browser_use/runner.py | 2 +- .../trajectory_replay/webarena_corrections.py | 7 ++- scripts/check_webarena_replay_report.py | 51 +++++++++++++++++++ tests/browser_use/test_runner.py | 6 ++- .../trajectory_replay/test_webarena_report.py | 30 +++++++++++ 5 files changed, 92 insertions(+), 4 deletions(-) diff --git a/harness/browser_use/runner.py b/harness/browser_use/runner.py index 0284ea66c..a3300b25e 100644 --- a/harness/browser_use/runner.py +++ b/harness/browser_use/runner.py @@ -401,7 +401,7 @@ def _wait_magento_dependencies_ready( ) output = result.output.decode("utf-8", errors="replace") last_detail = output[-500:] or f"exit {result.exit_code}" - if int(result.exit_code) == 0: + if result.exit_code == 0: try: status = str(json.loads(output).get("status") or "") except json.JSONDecodeError: diff --git a/harness/trajectory_replay/webarena_corrections.py b/harness/trajectory_replay/webarena_corrections.py index cc07725ad..208c97b6c 100644 --- a/harness/trajectory_replay/webarena_corrections.py +++ b/harness/trajectory_replay/webarena_corrections.py @@ -273,10 +273,15 @@ def evaluate_corrected_contract( evaluator_context = context if ordinary_network_filtering and config.evaluator == "NetworkEventEvaluator": task_data = task.model_dump(mode="json") + evaluations = task_data.get("eval") or [] + if not evaluations or not isinstance(evaluations[0].get("expected"), dict): + raise ValueError( + "ordinary network filtering requires eval[0].expected" + ) # webarena-verified 1.2.3 reads the task type used by # NetworkEventEvaluator from eval[0].expected, regardless of the # network evaluator's own position in the task configuration. - task_data["eval"][0]["expected"]["task_type"] = "MUTATE" + evaluations[0]["expected"]["task_type"] = "MUTATE" filtering_task = type(task).model_validate(task_data) evaluator_context = context.model_copy(update={"task": filtering_task}) evaluator_results.append( diff --git a/scripts/check_webarena_replay_report.py b/scripts/check_webarena_replay_report.py index 461f16ea5..8c50cb5b5 100644 --- a/scripts/check_webarena_replay_report.py +++ b/scripts/check_webarena_replay_report.py @@ -6,10 +6,58 @@ import argparse import json import re +from html.parser import HTMLParser from pathlib import Path from urllib.parse import unquote, urlsplit +class _VisibleTextParser(HTMLParser): + def __init__(self) -> None: + super().__init__() + self.hidden_depth = 0 + self.parts: list[str] = [] + + def handle_starttag(self, tag: str, attrs: list[tuple[str, str | None]]) -> None: + if tag.lower() in {"script", "style", "pre", "code"}: + self.hidden_depth += 1 + + def handle_endtag(self, tag: str) -> None: + if tag.lower() in {"script", "style", "pre", "code"} and self.hidden_depth: + self.hidden_depth -= 1 + + def handle_data(self, data: str) -> None: + if not self.hidden_depth: + self.parts.append(data) + + +def _visible_text(html: str) -> str: + parser = _VisibleTextParser() + parser.feed(html) + return " ".join(parser.parts) + + +def _check_english_localization(pages: list[Path]) -> None: + cjk = re.compile(r"[\u3400-\u9fff]") + localized_attribute = re.compile( + r'(?:aria-label|placeholder|title)=["\']([^"\']*)', + flags=re.I, + ) + for page in pages: + text = page.read_text(encoding="utf-8") + title = re.search(r"(.*?)", text, flags=re.I | re.S) + if title and cjk.search(title.group(1)): + raise ValueError(f"English report title is not localized: {page}") + if any(cjk.search(value) for value in localized_attribute.findall(text)): + raise ValueError(f"English report accessibility text is not localized: {page}") + visible = _visible_text(text).replace("中文", "") + match = cjk.search(visible) + if match: + excerpt = visible[max(0, match.start() - 40) : match.start() + 80] + raise ValueError( + f"English reader-facing page contains Chinese prose: {page}: {excerpt}" + ) + + def _local_target(page: Path, raw: str) -> Path | None: parsed = urlsplit(raw) if parsed.scheme or raw.startswith(("#", "mailto:", "javascript:")): @@ -108,6 +156,9 @@ def check(report_dir: Path) -> dict[str, int]: text = page.read_text(encoding="utf-8") if any(marker in text for marker in untranslated_zh_markers): raise ValueError(f"Chinese report contains untranslated interface text: {page}") + _check_english_localization( + [*core[len(core_names) :], *(root / "en/result").glob("*.html")] + ) checked_links = 0 for page in root.rglob("*.html"): diff --git a/tests/browser_use/test_runner.py b/tests/browser_use/test_runner.py index 088536a44..4a30cd59e 100644 --- a/tests/browser_use/test_runner.py +++ b/tests/browser_use/test_runner.py @@ -281,7 +281,7 @@ def test_environment_control_wait_covers_slow_official_image_restore() -> None: def test_magento_readiness_requires_non_red_elasticsearch(monkeypatch) -> None: class Result: - def __init__(self, exit_code: int, output: bytes) -> None: + def __init__(self, exit_code: int | None, output: bytes) -> None: self.exit_code = exit_code self.output = output @@ -293,13 +293,15 @@ def exec_run(self, command): self.calls += 1 assert command[-1].endswith("wait_for_status=yellow&timeout=5s") if self.calls == 1: + return Result(None, b"") + if self.calls == 2: return Result(7, b"connection refused") return Result(0, b'{"status":"yellow"}') monkeypatch.setattr("harness.browser_use.runner.time.sleep", lambda _: None) container = Container() _wait_magento_dependencies_ready(container, timeout_seconds=1) - assert container.calls == 2 + assert container.calls == 3 def test_physical_execution_lock_path_is_user_global_not_evidence_scoped( diff --git a/tests/trajectory_replay/test_webarena_report.py b/tests/trajectory_replay/test_webarena_report.py index 8aebf367b..d52f9d441 100644 --- a/tests/trajectory_replay/test_webarena_report.py +++ b/tests/trajectory_replay/test_webarena_report.py @@ -1,4 +1,9 @@ +from pathlib import Path + +import pytest + from harness.trajectory_replay.webarena_report import _metrics_table +from scripts.check_webarena_replay_report import _check_english_localization def _method(name: str, pass_rate: float, base: float) -> dict: @@ -28,3 +33,28 @@ def test_core_metrics_are_registry_driven_and_mark_only_unique_winners() -> None assert all(name in rendered for name in ("Browser A", "Browser B", "Browser C")) assert rendered.count(">Best") == 14 assert "median" not in rendered.lower() + + +def test_english_release_gate_rejects_reader_facing_chinese(tmp_path: Path) -> None: + page = tmp_path / "index.html" + page.write_text( + 'Replay中文' + '
    {"evidence":"原样保留"}
    ', + encoding="utf-8", + ) + _check_english_localization([page]) + + page.write_text( + 'Replay

    未翻译正文

    ', + encoding="utf-8", + ) + with pytest.raises(ValueError, match="Chinese prose"): + _check_english_localization([page]) + + page.write_text( + 'Replay' + '', + encoding="utf-8", + ) + with pytest.raises(ValueError, match="accessibility text"): + _check_english_localization([page]) From 1e02984c8db9caa153c2c6dd6b36529243e09323 Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 15 Sep 2026 12:59:49 +0800 Subject: [PATCH 119/127] Localize replay release and recover uploads --- .../webarena-verified/current-run.json | 2 +- harness/trajectory_replay/webarena_report.py | 35 ++++++++++++----- research/report-changes.json | 2 +- scripts/check_webarena_replay_report.py | 8 ++++ .../publish_webarena_replay_artifact_site.py | 39 ++++++++++++------- .../test_webarena_publish.py | 8 ++++ .../trajectory_replay/test_webarena_report.py | 9 ++++- 7 files changed, 76 insertions(+), 27 deletions(-) diff --git a/evaluation/trajectory-replay/webarena-verified/current-run.json b/evaluation/trajectory-replay/webarena-verified/current-run.json index e9fca59b8..15f690c8b 100644 --- a/evaluation/trajectory-replay/webarena-verified/current-run.json +++ b/evaluation/trajectory-replay/webarena-verified/current-run.json @@ -60,6 +60,6 @@ "state": "verified", "site_slug": "7dF7uNK5SKIx", "share_url": "https://artifact-hub.app.lexmount.com/v/hkZt38QFBpNIKtThKnqjpnBQk8At19PM", - "expected_version": "ver_f4cebfc3cc804813bf40cb1deeeb4498" + "expected_version": "ver_e92fe2d3c5a24001a36593fe6ca6fb4e" } } diff --git a/harness/trajectory_replay/webarena_report.py b/harness/trajectory_replay/webarena_report.py index 71416c99c..606240258 100644 --- a/harness/trajectory_replay/webarena_report.py +++ b/harness/trajectory_replay/webarena_report.py @@ -133,7 +133,12 @@ def _nav(lang: str, active: str, *, depth: int) -> str: def _shell(lang: str, active: str, title: str, body: str, *, depth: int = 0) -> str: prefix = "../" * depth asset_prefix = prefix if lang == "zh" else "../" + prefix - return f'''{_e(title)}{_nav(lang, active, depth=depth)}
    {body}
    WebArena-Verified 1.2.3 · Browser Trajectory Replay
    ''' + footer = ( + "WebArena-Verified 1.2.3 · 浏览器轨迹重放" + if lang == "zh" + else "WebArena-Verified 1.2.3 · Browser Trajectory Replay" + ) + return f'''{_e(title)}{_nav(lang, active, depth=depth)}
    {body}
    {footer}
    ''' def _winner(value: str, won: bool, *, lang: str) -> str: @@ -291,7 +296,12 @@ def _detail_page(

    {"固定操作轨迹" if zh else "Fixed action trajectory"}

    {''.join(rows)}
    #{"动作" if zh else "Action"}{"目标" if zh else "Target"}{"耗时" if zh else "Time"}{"状态" if zh else "Status"}{"结果" if zh else "Result"}

    {"日志与证据" if zh else "Logs and evidence"}

    {screenshot}
    ''' - return _shell(lang, "results", f"{method['label']} Task {attempt['task_id']}", body, depth=1) + title = ( + f"{method['label']} · 任务 #{attempt['task_id']}" + if zh + else f"{method['label']} · Task #{attempt['task_id']}" + ) + return _shell(lang, "results", title, body, depth=1) def _assets(output: Path) -> None: @@ -505,8 +515,13 @@ def build_webarena_replay_report( ) task_count = len(tasks) shared_count = len(passing_tasks) - summary_body = f'''

    WebArena-Verified · {task_count} tasks · model calls 0

    {"浏览器轨迹重放评测" if zh else "Browser Trajectory Replay"}

    {"同一组经 Chromium 资格验证的固定 BrowserTool 轨迹,依次在各注册浏览器中执行。Agent 与模型不参与测试。" if zh else "The same Chromium-qualified BrowserTool trajectories run on every registered browser. No Agent or model participates in the benchmark."}

    {"核心指标" if zh else "Core metrics"}

    {f"正确率覆盖全部 {task_count} 题;端到端耗时、CPU 和内存只比较所有浏览器共同通过的 {shared_count} 题,避免提前失败缩短工作量。单次动作统计包含重试中的实际调用;调用数不判优。" if zh else f"Correctness covers all {task_count} tasks. End-to-end time, CPU, and memory use the {shared_count} tasks passed by every browser so early failure cannot reduce workload. Per-action latency includes actual retry calls; call counts are not ranked."}

    {_metrics_table(method_rows, lang)}

    {"结论" if zh else "Findings"}

    {"".join(method_cards)}
    {"执行隔离" if zh else "Execution isolation"}{"串行" if zh else "Serial"}

    {"每题重置;方法之间销毁浏览器和站点。" if zh else "Reset per task; browser and sites torn down between methods."}

    {"浏览器动作耗时" if zh else "Browser action latency"}

    {"每格给出均值、P50、P95 和调用次数;耗时在所有注册浏览器间判优,调用次数只反映工作量。" if zh else "Each cell shows mean, P50, P95, and call count. Latency is ranked across every registered browser; counts only describe workload."}

    {action_table}
    ''' - (root / "index.html").write_text(_shell(lang, "index", "Browser Trajectory Replay", summary_body), encoding="utf-8") + eyebrow = ( + f"WebArena-Verified · {task_count} 个任务 · 模型调用 0 次" + if zh + else f"WebArena-Verified · {task_count} tasks · model calls 0" + ) + summary_body = f'''

    {eyebrow}

    {"浏览器轨迹重放评测" if zh else "Browser Trajectory Replay"}

    {"同一组经 Chromium 资格验证的固定 BrowserTool 轨迹,依次在各注册浏览器中执行。Agent 与模型不参与测试。" if zh else "The same Chromium-qualified BrowserTool trajectories run on every registered browser. No Agent or model participates in the benchmark."}

    {"核心指标" if zh else "Core metrics"}

    {f"正确率覆盖全部 {task_count} 题;端到端耗时、CPU 和内存只比较所有浏览器共同通过的 {shared_count} 题,避免提前失败缩短工作量。单次动作统计包含重试中的实际调用;调用数不判优。" if zh else f"Correctness covers all {task_count} tasks. End-to-end time, CPU, and memory use the {shared_count} tasks passed by every browser so early failure cannot reduce workload. Per-action latency includes actual retry calls; call counts are not ranked."}

    {_metrics_table(method_rows, lang)}

    {"结论" if zh else "Findings"}

    {"".join(method_cards)}
    {"执行隔离" if zh else "Execution isolation"}{"串行" if zh else "Serial"}

    {"每题重置;方法之间销毁浏览器和站点。" if zh else "Reset per task; browser and sites torn down between methods."}

    {"浏览器动作耗时" if zh else "Browser action latency"}

    {"每格给出均值、P50、P95 和调用次数;耗时在所有注册浏览器间判优,调用次数只反映工作量。" if zh else "Each cell shows mean, P50, P95, and call count. Latency is ranked across every registered browser; counts only describe workload."}

    {action_table}
    ''' + (root / "index.html").write_text(_shell(lang, "index", "浏览器轨迹重放" if zh else "Browser Trajectory Replay", summary_body), encoding="utf-8") headers = ["Task", "Site", *[item["label"] for item in methods.values()]] matrix_rows = [] @@ -518,7 +533,7 @@ def build_webarena_replay_report( values.append(f'{label}') matrix_rows.append(f'{task_id}{_e(", ".join(task["task"].get("sites") or []))}{"".join(f"{v}" for v in values)}') dataset_body = f'

    {f"{task_count} 条标准轨迹" if zh else f"{task_count} qualified trajectories"}

    {f"每题一条固定操作轨迹。{browser_use_source_count} 条来自干净的 Browser Use 成功执行,{repaired_source_count} 条经修复后重新在 {reference_label} 验证。" if zh else f"One fixed trajectory per task: {browser_use_source_count} clean Browser Use successes and {repaired_source_count} repaired trajectories re-qualified on {reference_label}."}

    {"逐题结果矩阵" if zh else "Per-task result matrix"}

    {"".join(f"" for h in headers)}{"".join(matrix_rows)}
    {_e(h)}
    ' - (root / "dataset.html").write_text(_shell(lang, "dataset", "Dataset", dataset_body), encoding="utf-8") + (root / "dataset.html").write_text(_shell(lang, "dataset", "评测集" if zh else "Dataset", dataset_body), encoding="utf-8") result_rows = [] for row in public_rows: @@ -527,8 +542,8 @@ def build_webarena_replay_report( status = ("通过" if row["status"] == "pass" else "失败") if zh else row["status"].title() result_rows.append(f'{_e(row["browser"])} · {row["task_id"]}{_e(row["sites"][0] if row["sites"] else "")}{status}{row["completed_actions"]}/{row["action_count"]}{_e(row["failure_stage"])}{_e(row["program_error"])}{_e(cause)}
    {_e(reason)}') browser_options = "".join(f'' for key, item in methods.items()) - results_body = f'''

    {"结果与失败诊断" if zh else "Results and failure diagnosis"}

    {"默认显示失败。点击任一行先看诊断弹窗,再进入详情页查看轨迹、日志和最后页面。" if zh else "Failures are shown by default. Open a row for the diagnosis dialog, then continue to the detail page for trajectory, logs, and last page."}

    {''.join(result_rows)}
    {"结果" if zh else "Result"}{"站点" if zh else "Site"}{"判定" if zh else "Verdict"}{"动作" if zh else "Actions"}{"失败阶段" if zh else "Failure stage"}{"程序报错" if zh else "Program error"}{"复核根因" if zh else "Reviewed root cause"}

    {"失败阶段" if zh else "Failure stage"}

    {"程序报错" if zh else "Program error"}

    {"复核根因" if zh else "Reviewed root cause"}

    {"打开完整详情" if zh else "Open full detail"}
    ''' - (root / "results.html").write_text(_shell(lang, "results", "Results", results_body), encoding="utf-8") + results_body = f'''

    {"结果与失败诊断" if zh else "Results and failure diagnosis"}

    {"默认显示失败。点击任一行先看诊断弹窗,再进入详情页查看轨迹、日志和最后页面。" if zh else "Failures are shown by default. Open a row for the diagnosis dialog, then continue to the detail page for trajectory, logs, and last page."}

    {''.join(result_rows)}
    {"结果" if zh else "Result"}{"站点" if zh else "Site"}{"判定" if zh else "Verdict"}{"动作" if zh else "Actions"}{"失败阶段" if zh else "Failure stage"}{"程序报错" if zh else "Program error"}{"复核根因" if zh else "Reviewed root cause"}

    {"失败阶段" if zh else "Failure stage"}

    {"程序报错" if zh else "Program error"}

    {"复核根因" if zh else "Reviewed root cause"}

    {"打开完整详情" if zh else "Open full detail"}
    ''' + (root / "results.html").write_text(_shell(lang, "results", "结果与诊断" if zh else "Results", results_body), encoding="utf-8") flow_labels = ( ("重放器", "浏览器容器", "本地站点", "逐题重置", "证据采集", "官方验收", "完全销毁") @@ -550,13 +565,13 @@ def build_webarena_replay_report( X --> T[Teardown browser and sites] T --> R''' environment_body = f'''

    {"环境与评测流程" if zh else "Environment and evaluation flow"}

    {"192.168.5.2 是 Colima 虚拟机中的宿主网关:浏览器容器通过它访问运行在同一台 Mac 虚拟化环境里的 WebArena 站点。它不是公网测试网站。" if zh else "192.168.5.2 is the host gateway inside the Colima VM. Browser containers use it to reach the WebArena sites running in the same Mac virtualization environment; it is not a public test website."}

    {"执行拓扑" if zh else "Execution topology"}

    {fallback}{topology}

    {"隔离规则" if zh else "Isolation rules"}

    1

    {"一套固定端口站点环境,因此只允许一个方法运行。" if zh else "One fixed-port site stack permits only one running method."}

    {task_count}

    {"一个浏览器完整跑完全部任务,每题前恢复官方夹具。" if zh else "A browser completes all tasks; official fixtures reset before each task."}

    0

    {"没有 Agent 决策和模型请求;输入是冻结的 BrowserTool 动作。" if zh else "No Agent decisions or model calls; inputs are frozen BrowserTool actions."}

    ''' - (root / "environment.html").write_text(_shell(lang, "environment", "Environment", environment_body), encoding="utf-8") + (root / "environment.html").write_text(_shell(lang, "environment", "环境与方法" if zh else "Environment", environment_body), encoding="utf-8") review_body = f'''

    {"核验记录" if zh else "Review history"}

    {"记录影响当前数据集与结论的核验轮次;逐题证据位于结果详情页。" if zh else "Review rounds that changed the current dataset or conclusions. Per-result evidence remains on result pages."}

    2026-09-15{f"{task_count} 条轨迹资格核验" if zh else f"{task_count}-trajectory qualification"}

    {f"修复已确认的非浏览器问题,重新验证后 {reference_label} {reference_row['passed']}/{reference_row['total']}。" if zh else f"Confirmed non-browser defects were repaired; {reference_label} then qualified {reference_row['passed']}/{reference_row['total']}."}

    2026-09-15{f"{reviewed_failure_count} 条失败根因复核" if zh else f"{reviewed_failure_count}-failure root-cause review"}

    {f"自动失败位置与复核根因分离;{reviewed_failure_count}/{reviewed_failure_count} 结果绑定当前证据哈希,无未解决项。" if zh else f"Programmatic locations remain separate from reviewed causes; {reviewed_failure_count}/{reviewed_failure_count} are bound to the current evidence hash with no unresolved items."}

    ''' - (root / "review.html").write_text(_shell(lang, "review", "Review history", review_body), encoding="utf-8") + (root / "review.html").write_text(_shell(lang, "review", "核验记录" if zh else "Review history", review_body), encoding="utf-8") viewer_body = f'

    {"证据查看器" if zh else "Evidence viewer"}

    {"网页直接加载选中的净化证据;也可下载原始 JSON。" if zh else "Loads the selected sanitized evidence directly in the page; raw JSON remains downloadable."}

    ' - (root / "evidence-viewer.html").write_text(_shell(lang, "results", "Evidence viewer", viewer_body), encoding="utf-8") + (root / "evidence-viewer.html").write_text(_shell(lang, "results", "证据查看器" if zh else "Evidence viewer", viewer_body), encoding="utf-8") manifest = { "schema": "lex_webarena_trajectory_replay_report/1", diff --git a/research/report-changes.json b/research/report-changes.json index 4fecc7f55..b47eb0b43 100644 --- a/research/report-changes.json +++ b/research/report-changes.json @@ -14,7 +14,7 @@ "service": "Artifact Site", "site_slug": "7dF7uNK5SKIx", "share_url": "https://artifact-hub.app.lexmount.com/v/hkZt38QFBpNIKtThKnqjpnBQk8At19PM", - "version": "ver_f4cebfc3cc804813bf40cb1deeeb4498" + "version": "ver_e92fe2d3c5a24001a36593fe6ca6fb4e" }, "effects": { "score": "首次发布;Chromium 90/90,Moli 54/90。", diff --git a/scripts/check_webarena_replay_report.py b/scripts/check_webarena_replay_report.py index 8c50cb5b5..6732a4712 100644 --- a/scripts/check_webarena_replay_report.py +++ b/scripts/check_webarena_replay_report.py @@ -145,6 +145,14 @@ def check(report_dir: Path) -> dict[str, int]: raise ValueError(f"report source contains a hand-authored SVG: {page}") untranslated_zh_markers = ( + "tasks · model calls", + 'aria-label="Close"', + ">Browser Trajectory Replay", + ">Dataset", + ">Results", + ">Environment", + ">Review history", + ">Evidence viewer", ">Action", ">Target", ">Time", diff --git a/scripts/publish_webarena_replay_artifact_site.py b/scripts/publish_webarena_replay_artifact_site.py index e47b6621f..a9cd5e949 100644 --- a/scripts/publish_webarena_replay_artifact_site.py +++ b/scripts/publish_webarena_replay_artifact_site.py @@ -23,7 +23,10 @@ PROFILE = Path("evaluation/trajectory-replay/webarena-verified/current-run.json") DATASET = Path("evaluation/trajectory-replay/webarena-verified/trajectories.json") REVIEW = Path("research/trajectory-replay-failure-review.json") -PUBLIC_REPORT_MARKER = "Browser Trajectory Replay" +PUBLIC_REPORT_MARKERS = ( + "浏览器轨迹重放", + "Browser Trajectory Replay", +) def sha256(path: Path) -> str: @@ -159,6 +162,11 @@ def public_share_path(base: str, share_url: str) -> str: return target.path +def preview_has_report_marker(body: bytes) -> bool: + text = body.decode("utf-8", errors="replace") + return any(marker in text for marker in PUBLIC_REPORT_MARKERS) + + def archive_hashes(path: Path) -> dict[str, str]: with zipfile.ZipFile(path) as archive: return { @@ -238,16 +246,21 @@ def main(*, create: bool) -> None: share_url = str(publication.get("share_url") or "") status, raw, _ = request(base, f"/api/sites/{urllib.parse.quote(slug)}") before = response_json(status, raw, operation="site lookup") - if current_version(before) != expected_version: - raise ValueError("Artifact Site version differs from current profile authority") - query = urllib.parse.urlencode({"expected_version": expected_version}) - status, raw, _ = request( - base, - f"/api/sites/{urllib.parse.quote(slug)}/versions?{query}", - payload, - content_type, - ) - response_json(status, raw, operation="site version upload") + remote_version = current_version(before) + if remote_version != expected_version: + # A previous upload may have committed before a post-upload check + # failed. Resume only when the exported tree proves that the + # remote version is byte-identical to the current package. + verify_export(base, slug) + else: + query = urllib.parse.urlencode({"expected_version": expected_version}) + status, raw, _ = request( + base, + f"/api/sites/{urllib.parse.quote(slug)}/versions?{query}", + payload, + content_type, + ) + response_json(status, raw, operation="site version upload") status, raw, _ = request(base, f"/api/sites/{urllib.parse.quote(slug)}") metadata = response_json(status, raw, operation="site lookup after upload") version = current_version(metadata) @@ -264,9 +277,7 @@ def main(*, create: bool) -> None: f"/api/preview/{urllib.parse.quote(slug)}", authenticated=False, ) - if preview_status != 200 or PUBLIC_REPORT_MARKER not in preview_body.decode( - "utf-8", errors="replace" - ): + if preview_status != 200 or not preview_has_report_marker(preview_body): raise RuntimeError("deployed preview is unavailable or missing report marker") bind_publication(profile, slug=slug, share_url=share_url, version=version) save_receipt( diff --git a/tests/trajectory_replay/test_webarena_publish.py b/tests/trajectory_replay/test_webarena_publish.py index 818c0b531..630147aab 100644 --- a/tests/trajectory_replay/test_webarena_publish.py +++ b/tests/trajectory_replay/test_webarena_publish.py @@ -43,6 +43,14 @@ def test_public_share_must_be_same_origin_and_token_route() -> None: raise AssertionError("unsafe public share URL must be rejected") +def test_preview_marker_accepts_each_localized_home_title() -> None: + assert publisher.preview_has_report_marker(b"Browser Trajectory Replay") + assert publisher.preview_has_report_marker( + "浏览器轨迹重放".encode() + ) + assert not publisher.preview_has_report_marker(b"Unrelated site") + + def test_bind_publication_updates_profile_and_attestation(monkeypatch, tmp_path: Path) -> None: profile = tmp_path / "profile.json" profile.write_text( diff --git a/tests/trajectory_replay/test_webarena_report.py b/tests/trajectory_replay/test_webarena_report.py index d52f9d441..c6b899900 100644 --- a/tests/trajectory_replay/test_webarena_report.py +++ b/tests/trajectory_replay/test_webarena_report.py @@ -2,7 +2,7 @@ import pytest -from harness.trajectory_replay.webarena_report import _metrics_table +from harness.trajectory_replay.webarena_report import _metrics_table, _shell from scripts.check_webarena_replay_report import _check_english_localization @@ -58,3 +58,10 @@ def test_english_release_gate_rejects_reader_facing_chinese(tmp_path: Path) -> N ) with pytest.raises(ValueError, match="accessibility text"): _check_english_localization([page]) + + +def test_report_shell_localizes_chinese_title_and_footer() -> None: + rendered = _shell("zh", "index", "浏览器轨迹重放", "

    正文

    ") + assert "浏览器轨迹重放" in rendered + assert "WebArena-Verified 1.2.3 · 浏览器轨迹重放" in rendered + assert "Browser Trajectory Replay" not in rendered From 6a5bcc6d280e2179cf3d5750513cd4251b70d900 Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 15 Sep 2026 13:14:49 +0800 Subject: [PATCH 120/127] Validate replay resume receipt fields --- harness/trajectory_replay/webarena_replay.py | 28 +++++++++- .../trajectory_replay/test_webarena_replay.py | 56 +++++++++++++++++++ 2 files changed, 81 insertions(+), 3 deletions(-) diff --git a/harness/trajectory_replay/webarena_replay.py b/harness/trajectory_replay/webarena_replay.py index 41a9c6d77..40ed44764 100644 --- a/harness/trajectory_replay/webarena_replay.py +++ b/harness/trajectory_replay/webarena_replay.py @@ -695,11 +695,26 @@ def _validated_resume( for task in plan["seed_manifest"].get("tasks") or [] } allowed_keys = {(engine, task_id) for engine in engines for task_id in task_ids} + + def required_receipt_int(item: object, field: str, record: str) -> int: + if not isinstance(item, dict) or field not in item: + raise ValueError(f"existing replay receipt {record} omitted {field}") + value = item[field] + if isinstance(value, bool): + raise ValueError(f"existing replay receipt {record} has invalid {field}") + try: + return int(value) + except (TypeError, ValueError) as exc: + raise ValueError( + f"existing replay receipt {record} has invalid {field}" + ) from exc + keys = [] for item in attempts: + task_id = required_receipt_int(item, "task_id", "attempt") if item.get("schema") != "lex_webarena_trajectory_replay_attempt/1": raise ValueError("existing replay receipt contains an invalid attempt schema") - key = (str(item.get("engine")), int(item.get("task_id"))) + key = (str(item.get("engine")), task_id) if key not in allowed_keys: raise ValueError("existing replay receipt contains an attempt outside the frozen plan") if item.get("source_candidate_id") != canonical_ids[key[1]]: @@ -719,14 +734,21 @@ def _validated_resume( "restart the complete formal scope in a new output directory" ) physical_keys = [ - (str(item.get("engine")), int(item.get("task_id")), int(item.get("physical_execution"))) + ( + str(item.get("engine")), + required_receipt_int(item, "task_id", "physical execution"), + required_receipt_int(item, "physical_execution", "physical execution"), + ) for item in physical_executions ] if len(physical_keys) != len(set(physical_keys)): raise ValueError("existing replay receipt contains duplicate physical executions") physical_by_logical: dict[tuple[str, int], list[dict[str, Any]]] = {} for item in physical_executions: - key = (str(item.get("engine")), int(item.get("task_id"))) + key = ( + str(item.get("engine")), + required_receipt_int(item, "task_id", "physical execution"), + ) if key not in allowed_keys: raise ValueError( "existing replay receipt contains a physical execution outside the plan" diff --git a/tests/trajectory_replay/test_webarena_replay.py b/tests/trajectory_replay/test_webarena_replay.py index baa7ae81e..294688e4a 100644 --- a/tests/trajectory_replay/test_webarena_replay.py +++ b/tests/trajectory_replay/test_webarena_replay.py @@ -465,3 +465,59 @@ def test_resume_rejects_interrupted_physical_execution(tmp_path: Path) -> None: scope="formal", runtime=runtime, ) + + +@pytest.mark.parametrize("corrupt_section", ["attempts", "physical_executions"]) +def test_resume_reports_missing_task_id_as_invalid_receipt( + tmp_path: Path, corrupt_section: str +) -> None: + runtime = {"resources": {"cpus": 2.0}} + plan = { + "profile_sha256": "profile", + "seed_manifest_sha256": "seeds", + "seed_manifest": { + "tasks": [{"task_id": 1, "canonical_candidate_id": "canonical-1"}] + }, + } + receipt = { + "schema": RECEIPT_SCHEMA, + "execution_scope": "formal", + "profile_sha256": "profile", + "source_seed_manifest_sha256": "seeds", + "task_ids": [1], + "engines": ["chromium"], + "runtime": runtime, + "run_id": "run", + "attempts": [], + "method_teardown": {"chromium": True}, + "sessions": [], + "physical_executions": [], + } + if corrupt_section == "attempts": + receipt["attempts"] = [ + { + "schema": "lex_webarena_trajectory_replay_attempt/1", + "engine": "chromium", + "source_candidate_id": "canonical-1", + } + ] + else: + receipt["physical_executions"] = [ + { + "engine": "chromium", + "physical_execution": 1, + "status": "complete", + } + ] + receipt_path = tmp_path / "receipt.json" + receipt_path.write_text(json.dumps(receipt), encoding="utf-8") + + with pytest.raises(ValueError, match="omitted task_id"): + _validated_resume( + receipt_path, + plan=plan, + task_ids=[1], + engines=["chromium"], + scope="formal", + runtime=runtime, + ) From 240f23339435326556dd556df7acb63a4152707c Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 15 Sep 2026 13:40:35 +0800 Subject: [PATCH 121/127] Harden replay cleanup and publication recovery --- harness/trajectory_replay/webarena_replay.py | 52 ++++++++++++++++-- harness/trajectory_replay/webarena_report.py | 4 +- scripts/check_webarena_replay_report.py | 6 ++- .../publish_webarena_replay_artifact_site.py | 53 ++++++++++++++++--- .../test_webarena_publish.py | 45 ++++++++++++++++ .../trajectory_replay/test_webarena_replay.py | 49 +++++++++++++++++ .../trajectory_replay/test_webarena_report.py | 13 ++++- 7 files changed, 209 insertions(+), 13 deletions(-) diff --git a/harness/trajectory_replay/webarena_replay.py b/harness/trajectory_replay/webarena_replay.py index 40ed44764..c226a2e83 100644 --- a/harness/trajectory_replay/webarena_replay.py +++ b/harness/trajectory_replay/webarena_replay.py @@ -592,6 +592,41 @@ def _teardown_method(client: Any, run_id: str, engine: str) -> None: raise RuntimeError("method teardown failed: " + "; ".join(errors)) +def _finalize_method_teardown( + client: Any, + run_id: str, + engine: str, + method_teardown: dict[str, bool], + receipt: dict[str, Any], + receipt_path: Path, + primary_error: BaseException | None, +) -> None: + """Persist cleanup outcome without replacing an in-flight task failure.""" + + cleanup_errors = [] + try: + _teardown_method(client, run_id, engine) + except Exception as exc: + cleanup_errors.append(str(exc)) + method_teardown[engine] = False + receipt.setdefault("method_teardown_errors", {})[engine] = str(exc) + else: + method_teardown[engine] = True + (receipt.get("method_teardown_errors") or {}).pop(engine, None) + receipt["method_teardown"] = method_teardown + try: + _write_receipt(receipt_path, receipt) + except Exception as exc: + cleanup_errors.append(f"receipt update failed: {exc}") + if not cleanup_errors: + return + message = "method finalization failed: " + "; ".join(cleanup_errors) + if primary_error is not None: + primary_error.add_note(message) + return + raise RuntimeError(message) + + def _remove_orphan_replay_runtimes(client: Any, current_run_id: str) -> None: """Remove stale replay containers and networks while the global stack lock is held.""" @@ -947,6 +982,7 @@ def run_replay_suite( # noqa: PLR0913 continue network = None container = None + primary_error: BaseException | None = None try: _stop_all_sites() method_teardown[engine] = False @@ -1073,11 +1109,19 @@ def run_replay_suite( # noqa: PLR0913 ), flush=True, ) + except BaseException as exc: + primary_error = exc + raise finally: - _teardown_method(client, run_id, engine) - method_teardown[engine] = True - receipt["method_teardown"] = method_teardown - _write_receipt(receipt_path, receipt) + _finalize_method_teardown( + client, + run_id, + engine, + method_teardown, + receipt, + receipt_path, + primary_error, + ) expected = {(engine, task_id) for engine in engines for task_id in task_ids} actual = {(str(item["engine"]), int(item["task_id"])) for item in attempts} diff --git a/harness/trajectory_replay/webarena_report.py b/harness/trajectory_replay/webarena_report.py index 606240258..dc0ff7f4c 100644 --- a/harness/trajectory_replay/webarena_report.py +++ b/harness/trajectory_replay/webarena_report.py @@ -18,6 +18,8 @@ programmatic_failure, ) +LOCAL_PATH_RE = re.compile(r"/(?:Users|home|private|tmp|var|opt)/[^\s\"']+") + def _e(value: Any) -> str: return html.escape(str(value if value is not None else "")) @@ -52,7 +54,7 @@ def _clean(value: Any) -> Any: if isinstance(value, list): return [_clean(item) for item in value] if isinstance(value, str): - value = re.sub(r"/Users/[^\s\"']+", "[local path removed]", value) + value = LOCAL_PATH_RE.sub("[local path removed]", value) value = re.sub(r"sk-[A-Za-z0-9_-]{12,}", "[secret removed]", value) return value diff --git a/scripts/check_webarena_replay_report.py b/scripts/check_webarena_replay_report.py index 6732a4712..75394fa6f 100644 --- a/scripts/check_webarena_replay_report.py +++ b/scripts/check_webarena_replay_report.py @@ -10,6 +10,8 @@ from pathlib import Path from urllib.parse import unquote, urlsplit +from harness.trajectory_replay.webarena_report import LOCAL_PATH_RE + class _VisibleTextParser(HTMLParser): def __init__(self) -> None: @@ -186,7 +188,9 @@ def check(report_dir: Path) -> dict[str, int]: for path in root.rglob("*") if path.is_file() and path.suffix in {".html", ".json", ".js", ".css"} ) - if "/Users/" in distributable or re.search(r"sk-[A-Za-z0-9_-]{12,}", distributable): + if LOCAL_PATH_RE.search(distributable) or re.search( + r"sk-[A-Za-z0-9_-]{12,}", distributable + ): raise ValueError("report contains a local path or credential-like token") css = (root / "assets/report.css").read_text(encoding="utf-8") if ".inventory{height:" not in css or "overflow:auto" not in css: diff --git a/scripts/publish_webarena_replay_artifact_site.py b/scripts/publish_webarena_replay_artifact_site.py index a9cd5e949..25a7d5725 100644 --- a/scripts/publish_webarena_replay_artifact_site.py +++ b/scripts/publish_webarena_replay_artifact_site.py @@ -23,6 +23,7 @@ PROFILE = Path("evaluation/trajectory-replay/webarena-verified/current-run.json") DATASET = Path("evaluation/trajectory-replay/webarena-verified/trajectories.json") REVIEW = Path("research/trajectory-replay-failure-review.json") +BINDING_JOURNAL = Path("publish/trajectory-replay-artifact-site-binding-pending.json") PUBLIC_REPORT_MARKERS = ( "浏览器轨迹重放", "Browser Trajectory Replay", @@ -37,6 +38,42 @@ def sha256(path: Path) -> str: return digest.hexdigest() +def json_bytes(value: dict) -> bytes: + return (json.dumps(value, ensure_ascii=False, indent=2) + "\n").encode() + + +def atomic_write(path: Path, payload: bytes) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + descriptor, temporary_name = tempfile.mkstemp(prefix=f".{path.name}.", dir=path.parent) + temporary = Path(temporary_name) + try: + with os.fdopen(descriptor, "wb") as stream: + stream.write(payload) + stream.flush() + os.fsync(stream.fileno()) + os.replace(temporary, path) + finally: + temporary.unlink(missing_ok=True) + + +def recover_publication_binding() -> None: + if not BINDING_JOURNAL.is_file(): + return + pending = json.loads(BINDING_JOURNAL.read_text(encoding="utf-8")) + if pending.get("schema") != "lex_webarena_replay_publication_binding/1": + raise ValueError("invalid pending publication binding") + profile = pending.get("profile") + attestation = pending.get("attestation") + if not isinstance(profile, dict) or not isinstance(attestation, dict): + raise ValueError("invalid pending publication binding payload") + profile_payload = json_bytes(profile) + if hashlib.sha256(profile_payload).hexdigest() != attestation.get("profile_sha256"): + raise ValueError("pending publication binding has an invalid profile hash") + atomic_write(PROFILE, profile_payload) + atomic_write(ATTESTATION, json_bytes(attestation)) + BINDING_JOURNAL.unlink() + + def tree_sha256(root: Path) -> str: digest = hashlib.sha256() for path in sorted(item for item in root.rglob("*") if item.is_file()): @@ -98,6 +135,7 @@ def request( def validate_package() -> dict: + recover_publication_binding() if not ARCHIVE.is_file() or not ATTESTATION.is_file(): raise ValueError("missing validated replay package or attestation") attestation = json.loads(ATTESTATION.read_text(encoding="utf-8")) @@ -197,13 +235,16 @@ def bind_publication(profile: dict, *, slug: str, share_url: str, version: str) "state": "verified", } ) - PROFILE.write_text(json.dumps(profile, ensure_ascii=False, indent=2) + "\n", encoding="utf-8") attestation = json.loads(ATTESTATION.read_text(encoding="utf-8")) - attestation["profile_sha256"] = sha256(PROFILE) - ATTESTATION.write_text( - json.dumps(attestation, ensure_ascii=False, indent=2) + "\n", - encoding="utf-8", - ) + profile_payload = json_bytes(profile) + attestation["profile_sha256"] = hashlib.sha256(profile_payload).hexdigest() + pending = { + "schema": "lex_webarena_replay_publication_binding/1", + "profile": profile, + "attestation": attestation, + } + atomic_write(BINDING_JOURNAL, json_bytes(pending)) + recover_publication_binding() def save_receipt(value: dict) -> None: diff --git a/tests/trajectory_replay/test_webarena_publish.py b/tests/trajectory_replay/test_webarena_publish.py index 630147aab..4b6e319b3 100644 --- a/tests/trajectory_replay/test_webarena_publish.py +++ b/tests/trajectory_replay/test_webarena_publish.py @@ -4,6 +4,8 @@ import json from pathlib import Path +import pytest + import scripts.package_webarena_replay_artifact_site as packager import scripts.publish_webarena_replay_artifact_site as publisher @@ -61,6 +63,7 @@ def test_bind_publication_updates_profile_and_attestation(monkeypatch, tmp_path: attestation.write_text(json.dumps({"profile_sha256": "old"}), encoding="utf-8") monkeypatch.setattr(publisher, "PROFILE", profile) monkeypatch.setattr(publisher, "ATTESTATION", attestation) + monkeypatch.setattr(publisher, "BINDING_JOURNAL", tmp_path / "binding-pending.json") publisher.bind_publication( json.loads(profile.read_text()), @@ -80,6 +83,48 @@ def test_bind_publication_updates_profile_and_attestation(monkeypatch, tmp_path: assert json.loads(attestation.read_text())["profile_sha256"] == _sha256(profile) +def test_binding_journal_recovers_interrupted_two_file_update( + monkeypatch, tmp_path: Path +) -> None: + profile = tmp_path / "profile.json" + profile.write_text( + json.dumps({"publication": {"base_url": "https://app.example"}}), + encoding="utf-8", + ) + attestation = tmp_path / "attestation.json" + attestation.write_text(json.dumps({"profile_sha256": "old"}), encoding="utf-8") + journal = tmp_path / "binding-pending.json" + monkeypatch.setattr(publisher, "PROFILE", profile) + monkeypatch.setattr(publisher, "ATTESTATION", attestation) + monkeypatch.setattr(publisher, "BINDING_JOURNAL", journal) + original_atomic_write = publisher.atomic_write + interrupted = False + + def interrupt_attestation(path: Path, payload: bytes) -> None: + nonlocal interrupted + if path == attestation and not interrupted: + interrupted = True + raise OSError("simulated interruption") + original_atomic_write(path, payload) + + monkeypatch.setattr(publisher, "atomic_write", interrupt_attestation) + with pytest.raises(OSError, match="simulated interruption"): + publisher.bind_publication( + json.loads(profile.read_text()), + slug="abc", + share_url="https://app.example/v/token", + version="ver_new", + ) + assert journal.is_file() + + monkeypatch.setattr(publisher, "atomic_write", original_atomic_write) + publisher.recover_publication_binding() + + assert not journal.exists() + assert json.loads(profile.read_text())["publication"]["expected_version"] == "ver_new" + assert json.loads(attestation.read_text())["profile_sha256"] == _sha256(profile) + + def test_package_binds_report_evidence_and_release_gate(monkeypatch, tmp_path: Path) -> None: report = tmp_path / "report" report.mkdir() diff --git a/tests/trajectory_replay/test_webarena_replay.py b/tests/trajectory_replay/test_webarena_replay.py index 294688e4a..d985784b1 100644 --- a/tests/trajectory_replay/test_webarena_replay.py +++ b/tests/trajectory_replay/test_webarena_replay.py @@ -11,6 +11,7 @@ _click_was_dispatched_before_navigation_timeout, _embedded_observation_error, _execute_replay_call, + _finalize_method_teardown, _origin, _rebind_url, _run_one, @@ -68,6 +69,54 @@ def test_replay_cleanup_and_locking_preserve_run_isolation() -> None: assert '"cleanup_errors": cleanup_errors' in run_one_source +def test_method_teardown_failure_does_not_mask_primary_error( + monkeypatch, tmp_path: Path +) -> None: + primary = RuntimeError("task failed") + receipt = {} + state = {"chromium": False} + monkeypatch.setattr( + "harness.trajectory_replay.webarena_replay._teardown_method", + lambda *_args: (_ for _ in ()).throw(RuntimeError("cleanup failed")), + ) + monkeypatch.setattr( + "harness.trajectory_replay.webarena_replay._write_receipt", + lambda *_args: None, + ) + + _finalize_method_teardown( + object(), "run", "chromium", state, receipt, tmp_path / "receipt.json", primary + ) + + assert state == {"chromium": False} + assert receipt["method_teardown_errors"]["chromium"] == "cleanup failed" + assert "cleanup failed" in " ".join(primary.__notes__) + + +def test_method_teardown_failure_is_raised_without_primary_error( + monkeypatch, tmp_path: Path +) -> None: + monkeypatch.setattr( + "harness.trajectory_replay.webarena_replay._teardown_method", + lambda *_args: (_ for _ in ()).throw(RuntimeError("cleanup failed")), + ) + monkeypatch.setattr( + "harness.trajectory_replay.webarena_replay._write_receipt", + lambda *_args: None, + ) + + with pytest.raises(RuntimeError, match="cleanup failed"): + _finalize_method_teardown( + object(), + "run", + "chromium", + {"chromium": False}, + {}, + tmp_path / "receipt.json", + None, + ) + + def test_formal_dataset_binding_requires_all_canonical_trajectories(tmp_path: Path) -> None: plan = { "seed_manifest_sha256": "seed-hash", diff --git a/tests/trajectory_replay/test_webarena_report.py b/tests/trajectory_replay/test_webarena_report.py index c6b899900..faf541add 100644 --- a/tests/trajectory_replay/test_webarena_report.py +++ b/tests/trajectory_replay/test_webarena_report.py @@ -2,7 +2,7 @@ import pytest -from harness.trajectory_replay.webarena_report import _metrics_table, _shell +from harness.trajectory_replay.webarena_report import _clean, _metrics_table, _shell from scripts.check_webarena_replay_report import _check_english_localization @@ -65,3 +65,14 @@ def test_report_shell_localizes_chinese_title_and_footer() -> None: assert "浏览器轨迹重放" in rendered assert "WebArena-Verified 1.2.3 · 浏览器轨迹重放" in rendered assert "Browser Trajectory Replay" not in rendered + + +def test_report_removes_macos_and_linux_local_paths() -> None: + for path in ( + "/Users/example/run/trace.json", + "/home/runner/work/trace.json", + "/private/tmp/run/trace.json", + "/var/lib/docker/trace.json", + "/opt/hostedtoolcache/trace.json", + ): + assert _clean(f"failure at {path}") == "failure at [local path removed]" From 4fcfb0ce1bf6f26bda1b975fd2477b47735df102 Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 15 Sep 2026 13:58:52 +0800 Subject: [PATCH 122/127] Recover first replay site publication --- .../publish_webarena_replay_artifact_site.py | 73 +++++++++++++----- .../test_webarena_publish.py | 76 +++++++++++++++++++ 2 files changed, 128 insertions(+), 21 deletions(-) diff --git a/scripts/publish_webarena_replay_artifact_site.py b/scripts/publish_webarena_replay_artifact_site.py index 25a7d5725..7f383ea07 100644 --- a/scripts/publish_webarena_replay_artifact_site.py +++ b/scripts/publish_webarena_replay_artifact_site.py @@ -256,33 +256,33 @@ def save_receipt(value: dict) -> None: os.chmod(RECEIPT, 0o600) +def pending_created_site(base: str) -> str: + if not RECEIPT.is_file(): + return "" + receipt = json.loads(RECEIPT.read_text(encoding="utf-8")) + if receipt.get("verified"): + return "" + if ( + receipt.get("base_url") != base + or receipt.get("archive_sha256") != sha256(ARCHIVE) + or not isinstance(receipt.get("slug"), str) + or not receipt["slug"] + ): + raise ValueError("pending site receipt differs from the current release package") + return receipt["slug"] + + def main(*, create: bool) -> None: validate_package() - profile, base, bound_slug, expected_version = publication_target(require_bound=not create) + profile, base, bound_slug, expected_version = publication_target(require_bound=False) publication = profile["publication"] title = str(publication.get("title") or "Browser Trajectory Replay") payload, content_type = multipart_archive(title) - if create: - if bound_slug or expected_version: + if bound_slug or expected_version: + if create: raise ValueError("replay report is already bound; update it without --create") - status, raw, _ = request(base, "/api/sites", payload, content_type) - created = response_json(status, raw, operation="site creation") - slug = str(created.get("slug") or "") - if not slug: - raise RuntimeError("site creation response omitted slug") - save_receipt(created) - share_status, share_raw, _ = request( - base, - f"/api/sites/{urllib.parse.quote(slug)}/shares", - json.dumps({"policy": "public"}).encode(), - "application/json", - method="POST", - ) - share = response_json(share_status, share_raw, operation="public share creation") - share_url = str(share.get("url") or "") - if not share_url.startswith("https://"): - raise RuntimeError("share creation response omitted public URL") - else: + if not bound_slug or not expected_version.startswith("ver_"): + raise ValueError("current replay profile has no complete Artifact Site binding") slug = bound_slug share_url = str(publication.get("share_url") or "") status, raw, _ = request(base, f"/api/sites/{urllib.parse.quote(slug)}") @@ -302,6 +302,37 @@ def main(*, create: bool) -> None: content_type, ) response_json(status, raw, operation="site version upload") + else: + slug = pending_created_site(base) + if slug: + verify_export(base, slug) + elif not create: + raise ValueError("no bound site or matching pending creation receipt") + else: + status, raw, _ = request(base, "/api/sites", payload, content_type) + created = response_json(status, raw, operation="site creation") + slug = str(created.get("slug") or "") + if not slug: + raise RuntimeError("site creation response omitted slug") + save_receipt( + { + "base_url": base, + "slug": slug, + "archive_sha256": sha256(ARCHIVE), + "verified": False, + } + ) + share_status, share_raw, _ = request( + base, + f"/api/sites/{urllib.parse.quote(slug)}/shares", + json.dumps({"policy": "public"}).encode(), + "application/json", + method="POST", + ) + share = response_json(share_status, share_raw, operation="public share creation") + share_url = str(share.get("url") or "") + if not share_url.startswith("https://"): + raise RuntimeError("share creation response omitted public URL") status, raw, _ = request(base, f"/api/sites/{urllib.parse.quote(slug)}") metadata = response_json(status, raw, operation="site lookup after upload") version = current_version(metadata) diff --git a/tests/trajectory_replay/test_webarena_publish.py b/tests/trajectory_replay/test_webarena_publish.py index 4b6e319b3..9d53a7292 100644 --- a/tests/trajectory_replay/test_webarena_publish.py +++ b/tests/trajectory_replay/test_webarena_publish.py @@ -125,6 +125,82 @@ def interrupt_attestation(path: Path, payload: bytes) -> None: assert json.loads(attestation.read_text())["profile_sha256"] == _sha256(profile) +def test_pending_created_site_is_bound_to_release_package(monkeypatch, tmp_path: Path) -> None: + archive = tmp_path / "package.zip" + archive.write_bytes(b"package") + receipt = tmp_path / "receipt.json" + monkeypatch.setattr(publisher, "ARCHIVE", archive) + monkeypatch.setattr(publisher, "RECEIPT", receipt) + base = "https://app.example" + publisher.save_receipt( + { + "base_url": base, + "slug": "site-1", + "archive_sha256": publisher.sha256(archive), + "verified": False, + } + ) + + assert publisher.pending_created_site(base) == "site-1" + with pytest.raises(ValueError, match="differs"): + publisher.pending_created_site("https://other.example") + archive.write_bytes(b"different") + with pytest.raises(ValueError, match="differs"): + publisher.pending_created_site(base) + + +@pytest.mark.parametrize("retry_create", [True, False]) +def test_share_failure_resumes_same_site_without_second_creation( + monkeypatch, tmp_path: Path, retry_create: bool +) -> None: + archive = tmp_path / "package.zip" + archive.write_bytes(b"package") + receipt = tmp_path / "receipt.json" + monkeypatch.setattr(publisher, "ARCHIVE", archive) + monkeypatch.setattr(publisher, "RECEIPT", receipt) + monkeypatch.setattr(publisher, "validate_package", lambda: {}) + monkeypatch.setattr( + publisher, + "publication_target", + lambda **_kwargs: ( + {"publication": {"title": "Replay"}}, + "https://app.example", + "", + "", + ), + ) + monkeypatch.setattr(publisher, "multipart_archive", lambda _title: (b"zip", "zip")) + monkeypatch.setattr(publisher, "verify_export", lambda *_args: None) + calls = [] + fail_share_once = True + + def fake_request(_base, path, *_args, **_kwargs): + nonlocal fail_share_once + calls.append(path) + if path == "/api/sites": + return 200, b'{"slug":"site-1"}', {} + if path.endswith("/shares"): + if fail_share_once: + fail_share_once = False + return 503, b'{"error":"temporary"}', {} + return 200, b'{"url":"https://app.example/v/token"}', {} + if path == "/api/sites/site-1": + return 200, b'{"site":{"currentVersionId":"ver_1"}}', {} + if path == "/v/token" or path == "/api/preview/site-1": + return 200, "浏览器轨迹重放".encode(), {} + raise AssertionError(path) + + monkeypatch.setattr(publisher, "request", fake_request) + monkeypatch.setattr(publisher, "bind_publication", lambda *_args, **_kwargs: None) + with pytest.raises(RuntimeError, match="public share creation"): + publisher.main(create=True) + + publisher.main(create=retry_create) + + assert calls.count("/api/sites") == 1 + assert calls.count("/api/sites/site-1/shares") == 2 + + def test_package_binds_report_evidence_and_release_gate(monkeypatch, tmp_path: Path) -> None: report = tmp_path / "report" report.mkdir() From 272264974b65a70d3a036679c624b453f6825b39 Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 15 Sep 2026 15:41:33 +0800 Subject: [PATCH 123/127] Improve trajectory replay report drilldown and hosted navigation --- AGENTS.md | 2 + .../webarena-verified/current-run.json | 2 +- harness/trajectory_replay/webarena_report.py | 405 +++++++++++++++--- research/report-changes.json | 32 ++ scripts/check_webarena_replay_report.py | 6 +- scripts/check_webarena_replay_visual.py | 62 ++- .../trajectory_replay/test_webarena_report.py | 47 +- 7 files changed, 483 insertions(+), 73 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 66401f3c4..42b855130 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,6 +15,8 @@ - Maintain `research/report-changes.json` for substantive releases and `research/provider-pricing.json` for public pricing. Release notes explain reader-visible or result-visible effects without narrating assistant workflow; prices retain verification dates, commitments, billing units and unknowns. - Use `Browser Use` for the autonomous track and `Browser Trajectory Replay` (Chinese: `轨迹重放`) for the fixed BrowserTool-action track. Their implementation roots are `harness/browser_use/` and `harness/trajectory_replay/`; do not add aliases or compatibility paths using the retired names. - Browser Trajectory Replay canonical inputs must not contain run-local snapshot refs. Compile each ref to a hash-bound stable selector while preserving the original ref action semantics; do not silently replace a DOM ref click/fill/select with BrowserTool's Playwright selector fallback because that adds different viewport, overlay and navigation-wait behavior. +- Browser Trajectory Replay reports keep the dataset separate from measured verdicts. Every task needs a contract-and-qualified-trajectory page; every result needs a same-task all-browser comparison, a reader-facing failure locator, the exact failed-action or evaluator error, reviewed cause, complete expandable tool responses, and on-demand public evidence. The result inventory stays compact, fixed-height, filterable and linked to a diagnosis dialog; generate participant columns from the registry and keep Chinese and English task/result routes aligned. +- Trajectory Replay release checks exercise Sites-style tokenized preview navigation from a nested result back to the report root; local relative-link checks alone do not prove hosted drilldown works. - Browser Use reports expose one manifest-authoritative logical result per browser and task. Dataset routes contain only frozen task definitions and acceptance contracts; result routes own verdicts, timings and trajectories. Keep one filterable result inventory instead of adding duplicate success/failure tables. - Keep automatic evaluator failure location separate from reviewed root cause. A root-cause subtype and reason must come from a per-browser, per-task review bound to the current attempts hash; render both fields blank while that result is unverified, and require complete verified coverage before a formal release when the current profile enables the gate. - Generate browser rows and comparisons from the participant registry. The same-task explorer uses one baseline browser, multi-select comparison browsers and explicit outcome relations, with browser labels linking to full results; never add one fixed table column per future browser. List Agent, model, prompt/tool contract, resources, environment and evaluator as shared controls. Standard summaries use mean, P50 and P95; compare every registered model-by-browser configuration together and mark only globally unique winners on directionally comparable metrics. For action latency, compare all configurations by per-call latency and keep unequal-workload totals and call counts neutral. diff --git a/evaluation/trajectory-replay/webarena-verified/current-run.json b/evaluation/trajectory-replay/webarena-verified/current-run.json index 15f690c8b..4c0519fac 100644 --- a/evaluation/trajectory-replay/webarena-verified/current-run.json +++ b/evaluation/trajectory-replay/webarena-verified/current-run.json @@ -60,6 +60,6 @@ "state": "verified", "site_slug": "7dF7uNK5SKIx", "share_url": "https://artifact-hub.app.lexmount.com/v/hkZt38QFBpNIKtThKnqjpnBQk8At19PM", - "expected_version": "ver_e92fe2d3c5a24001a36593fe6ca6fb4e" + "expected_version": "ver_0f4f162b973a4ba7afba2fb4e18d6173" } } diff --git a/harness/trajectory_replay/webarena_report.py b/harness/trajectory_replay/webarena_report.py index dc0ff7f4c..50cb35958 100644 --- a/harness/trajectory_replay/webarena_report.py +++ b/harness/trajectory_replay/webarena_report.py @@ -113,13 +113,13 @@ def _unique_winners( return winners if len(winners) == 1 else set() -def _nav(lang: str, active: str, *, depth: int) -> str: +def _nav(lang: str, active: str, *, depth: int, current_route: str = "index.html") -> str: labels = { "zh": (("index", "结论"), ("dataset", "评测集"), ("results", "结果与诊断"), ("environment", "环境与方法"), ("review", "核验记录")), "en": (("index", "Summary"), ("dataset", "Dataset"), ("results", "Results & diagnosis"), ("environment", "Environment & method"), ("review", "Review history")), }[lang] local_prefix = "../" * depth - switch = local_prefix + ("../index.html" if lang == "en" else "en/index.html") + switch = ("../../" if lang == "en" and depth else "../" if lang == "en" else "../en/" if depth else "en/") + current_route switch_label = "中文" if lang == "en" else "English" brand = "Trajectory Replay" if lang == "en" else "轨迹重放" theme_label = "Toggle theme" if lang == "en" else "切换主题" @@ -132,7 +132,7 @@ def _nav(lang: str, active: str, *, depth: int) -> str: return f'
    {brand}{switch_label}
    ' -def _shell(lang: str, active: str, title: str, body: str, *, depth: int = 0) -> str: +def _shell(lang: str, active: str, title: str, body: str, *, depth: int = 0, current_route: str = "index.html") -> str: prefix = "../" * depth asset_prefix = prefix if lang == "zh" else "../" + prefix footer = ( @@ -140,7 +140,18 @@ def _shell(lang: str, active: str, title: str, body: str, *, depth: int = 0) -> if lang == "zh" else "WebArena-Verified 1.2.3 · Browser Trajectory Replay" ) - return f'''{_e(title)}{_nav(lang, active, depth=depth)}
    {body}
    {footer}
    ''' + preview_bootstrap = r'''''' + return f'''{_e(title)}{preview_bootstrap}{_nav(lang, active, depth=depth, current_route=current_route)}
    {body}
    {footer}
    ''' def _winner(value: str, won: bool, *, lang: str) -> str: @@ -202,6 +213,24 @@ def time_value(value: float | None) -> str: return f'
    {"".join(f"" for h in headers)}{"".join(body)}
    {_e(h)}
    ' +def _resource_table(methods: list[dict[str, Any]], lang: str) -> str: + zh = lang == "zh" + metrics = ( + ("memory_peak", "峰值内存", "Peak memory", _format_mib), + ("network_rx", "接收数据", "Network received", _format_mib), + ("network_tx", "发送数据", "Network sent", _format_mib), + ("cpu_utilization", "CPU 平均使用率", "Mean CPU utilization", lambda value: "—" if value is None else f"{value:.1f}%"), + ) + rows = [] + for key, label_zh, label_en, formatter in metrics: + cells = [_e(label_zh if zh else label_en)] + for method in methods: + cells.append(_metric_lines(method[key], formatter, method["id"], {stat: set() for stat in ("mean", "p50", "p95")}, lang=lang)) + rows.append("" + "".join(f"{cell}" for cell in cells) + "") + headers = ("资源指标" if zh else "Resource metric", *(method["label"] for method in methods)) + return f'
    {"".join(f"" for h in headers)}{"".join(rows)}
    {_e(h)}
    ' + + def _copy_failure_screenshot(attempt: dict[str, Any], output: Path, key: str) -> str | None: ref = (attempt.get("evidence") or {}).get("failure_screenshot") if not ref or not ref.get("path"): @@ -263,52 +292,265 @@ def _detail_page( task: dict[str, Any], method: dict[str, Any], evidence: dict[str, Any], + peers: list[dict[str, Any]], ) -> str: failed = evidence["programmatic_failure"] review = evidence.get("review") zh = lang == "zh" status = ("通过" if attempt["status"] == "pass" else "失败") if zh else attempt["status"].title() - reason = (review.get("reason_zh") if zh else review.get("reason_en")) if review else ("—") - label = (review.get("label_zh") if zh else review.get("label_en")) if review else "—" + reason = (review.get("reason_zh") if zh else review.get("reason_en")) if review else "" + label = (review.get("label_zh") if zh else review.get("label_en")) if review else "" key = f'{attempt["engine"]}-{attempt["task_id"]}' report_root = "../" if zh else "../../" - rows = [] + stage_labels = { + "browser_tool_execution": ("浏览器动作执行", "Browser action execution"), + "official_evaluation": ("官方验收", "Official evaluation"), + "pass": ("无", "None"), + } + stage = stage_labels.get(failed["stage"], (failed["stage"], failed["stage"]))[0 if zh else 1] + failed_index = failed.get("step") + failed_action = next((str((step.get("arguments") or {}).get("action") or "") for step in evidence["trajectory"] if step.get("index") == failed_index), "") + peer_cells = [] + for peer in peers: + peer_key = f'{peer["engine"]}-{peer["task_id"]}' + peer_status = ("通过" if peer["status"] == "pass" else "失败") if zh else peer["status"].title() + peer_cells.append( + f'{_e(peer["browser"])}{peer_status}' + ) + steps = [] for step in evidence["trajectory"]: arguments = step.get("arguments") or {} action = arguments.get("action") or "" target = arguments.get("selector") or arguments.get("url") or arguments.get("key") or "" - result_lines = str(step.get("result") or "").splitlines() - result = (result_lines[0] if result_lines else "")[:220] - failed_step = failed.get("step") == step.get("index") - rows.append( - f'{step.get("index")}{_e(action)}{_e(target)}{_e(step.get("duration_ms"))} ms{_e(step.get("status"))}{_e(result)}' + number = step.get("index") + failed_step = failed_index == number + result = str(step.get("result") or "") + first_line = result.splitlines()[0] if result else "" + steps.append( + f'
    ' + f'
    {number}{_e(action)}' + f'{_e(step.get("status"))}' + f'{_e(round(float(step.get("duration_ms") or 0) / 1000, 2))} s
    ' + f'

    {_e(target)}

    {_e(first_line[:240])}

    ' + f'
    {"完整动作参数与返回" if zh else "Full action arguments and response"}' + f'

    {"动作参数" if zh else "Arguments"}

    {_e(json.dumps(_clean(arguments), ensure_ascii=False, indent=2))}
    ' + f'

    {"完整返回与报错" if zh else "Full response and error"}

    {_e(_clean(result))}
    ' ) screenshot = ( - f'
    {
    {"失败后保存的最后页面;不等同于错误发生瞬间。" if zh else "Last page saved after failure; not necessarily the exact error instant."}
    ' + f'
    {
    {"失败后保存的最后页面;不等同于错误发生瞬间。点击查看原图。" if zh else "Last page saved after failure, not necessarily the error instant. Open the full image."}
    ' if evidence.get("screenshot") else "" ) + failure_panel = "" + if attempt["status"] != "pass": + step_link = f'{"跳到失败动作" if zh else "Go to failed action"} #{failed_index}' if failed_index else f'{"查看已完成动作" if zh else "View completed actions"}' + action_locator = f'{failed_action} #{failed_index}' if failed_index else ("所有动作执行后" if zh else "After all actions") + failure_panel = f'''

    {"失败定位与处理" if zh else "Failure diagnosis and treatment"}

    {"失败环节" if zh else "Failure stage"}{_e(stage)}
    {"失败位置" if zh else "Failure location"}{_e(action_locator)}
    {"复核分类" if zh else "Reviewed category"}{_e(label)}

    {"原始报错" if zh else "Exact error"}

    {_e(failed["message"])}

    {"复核判断与依据" if zh else "Reviewed cause and basis"}

    {_e(reason)}

    {"处理方" if zh else "Owner"}: {_e(review.get("remediation_owner") if review else "—")} · {"状态" if zh else "Status"}: {_e(review.get("remediation_status") if review else "—")}

    {step_link}
    ''' + facts = [ + (("完成动作", "Actions completed"), f'{attempt["completed_action_count"]}/{attempt["action_count"]}'), + (("端到端耗时", "End-to-end time"), _format_ms(attempt.get("duration_ms")) if zh else f'{attempt.get("duration_ms", 0) / 1000:.1f} s'), + (("CPU 时间", "CPU time"), _format_ms((attempt.get("resource") or {}).get("cpu_ms")) if zh else f'{((attempt.get("resource") or {}).get("cpu_ms") or 0) / 1000:.1f} s'), + (("平均内存", "Mean memory"), _format_mib((attempt.get("resource") or {}).get("memory_mean_bytes"))), + ] + fact_html = "".join(f'
    {_e(label[0 if zh else 1])}{_e(value)}
    ' for label, value in facts) + viewer_file = ("data/evidence/" if zh else "../data/evidence/") + key + ".json" body = f''' -

    {_e(key)}

    {_e(method["label"])} · Task #{attempt["task_id"]}

    {_e(task["task"]["intent"])}

    -

    {"结论与定位" if zh else "Verdict and failure location"}

    -
    {"结果" if zh else "Verdict"}{status}{attempt["completed_action_count"]}/{attempt["action_count"]} {"个动作完成" if zh else "actions completed"}
    -
    {"程序定位" if zh else "Programmatic location"}{_e(failed["stage"])}

    {_e(failed["message"])}

    -
    {"复核根因" if zh else "Reviewed root cause"}{_e(label)}

    {_e(reason)}

    -
    {"处理" if zh else "Remediation"}{_e(review.get("remediation_status") if review else "—")}

    {_e(review.get("remediation_owner") if review else "—")}

    -
    -

    {"固定操作轨迹" if zh else "Fixed action trajectory"}

    {''.join(rows)}
    #{"动作" if zh else "Action"}{"目标" if zh else "Target"}{"耗时" if zh else "Time"}{"状态" if zh else "Status"}{"结果" if zh else "Result"}
    -

    {"日志与证据" if zh else "Logs and evidence"}

    {screenshot}
    ''' +

    {_e(key)}

    {_e(method["label"])} · {"任务" if zh else "Task"} #{attempt["task_id"]}

    {_e(task["task"]["intent"])}

    +

    {"同题浏览器结果" if zh else "All browser results for this task"}

    {''.join(peer_cells)}
    +
    {status}{_e(stage) if attempt["status"] != "pass" else ("固定轨迹及官方验收均通过" if zh else "Qualified trajectory and official evaluation passed")}
    {fact_html}
    + {failure_panel} +

    {"本浏览器的动作执行记录" if zh else "Executed actions in this browser"}

    {"逐项展开可查看完整参数、页面快照、工具返回和原始报错;红色标注的是实际失败动作。" if zh else "Expand an action for its full arguments, page snapshot, tool response, and exact error. Red marks the failed action."}

    {''.join(steps)}
    +

    {"验收、资源与页面证据" if zh else "Evaluation, resources, and page evidence"}

    {screenshot}
    ''' title = ( f"{method['label']} · 任务 #{attempt['task_id']}" if zh else f"{method['label']} · Task #{attempt['task_id']}" ) - return _shell(lang, "results", title, body, depth=1) + return _shell(lang, "results", title, body, depth=1, current_route=f"result/{key}.html") + + +def _task_page(lang: str, task: dict[str, Any], peers: list[dict[str, Any]]) -> str: + zh = lang == "zh" + task_id = int(task["task_id"]) + source_kind = str((task.get("source") or {}).get("kind") or "") + source_label = ( + ("来自已通过的自主浏览器执行", "From a successful autonomous browser run") + if source_kind == "authoritative_successful_browser_use_execution" + else ("修复后在基准浏览器重新验证", "Repaired and re-qualified on the reference browser") + )[0 if zh else 1] + peer_rows = "".join( + f'{_e(peer["browser"])}' + f'{("通过" if peer["status"] == "pass" else "失败") if zh else peer["status"].title()}' + f'{peer["completed_actions"]}/{peer["action_count"]}' + f'{"查看诊断与执行轨迹" if zh else "Diagnosis and executed actions"}' + for peer in peers + ) + planned_rows = [] + for index, call in enumerate(task.get("tool_calls") or [], 1): + action = str(call.get("action") or "") + target = str(call.get("selector") or call.get("url") or call.get("key") or "") + planned_rows.append( + f'{index}{_e(action)}{_e(target)}' + f'
    {"查看完整参数" if zh else "Full arguments"}
    {_e(json.dumps(_clean(call), ensure_ascii=False, indent=2))}
    ' + ) + contract = task.get("evaluation_input") or {} + contract_type = str(contract.get("task_type") or "") + contract_summary = ({"RETRIEVE": "信息提取", "NAVIGATE": "页面导航", "MUTATE": "页面操作"}.get(contract_type, contract_type) if zh else contract_type) or "—" + body = f'''

    WebArena-Verified · #{task_id}

    {"任务定义与标准轨迹" if zh else "Task contract and qualified trajectory"}

    {_e(task["task"]["intent"])}

    +
    {"站点" if zh else "Sites"}{_e(", ".join(task["task"].get("sites") or []))}
    {"轨迹来源" if zh else "Trajectory source"}{source_label}
    {"计划动作" if zh else "Planned actions"}{len(task.get("tool_calls") or [])}
    {"任务类型" if zh else "Task type"}{contract_summary}
    +

    {"同题所有浏览器结果" if zh else "All browser outcomes on this task"}

    {peer_rows}
    {"浏览器" if zh else "Browser"}{"结果" if zh else "Verdict"}{"完成动作" if zh else "Completed actions"}{"证据" if zh else "Evidence"}
    +

    {"冻结的操作轨迹" if zh else "Frozen action trajectory"}

    {"这是所有浏览器共用的输入;下方记录的是预定动作,不是任一浏览器的实测执行结果。" if zh else "The same input is used for every browser. These are planned actions, not any browser's measured execution."}

    {''.join(planned_rows)}
    #{"动作" if zh else "Action"}{"目标" if zh else "Target"}{"参数" if zh else "Arguments"}
    +

    {"验收输入" if zh else "Evaluation input"}

    {"供官方 evaluator 判分;不是 Agent 提示词,也不是唯一正确路径。" if zh else "Used by the official evaluator, not an Agent prompt or the only correct path."}

    {"查看答案与验收字段" if zh else "Inspect answer and acceptance fields"}
    {_e(json.dumps(_clean(contract), ensure_ascii=False, indent=2))}
    ''' + title = f"任务 #{task_id} · 评测集" if zh else f"Task #{task_id} · Dataset" + return _shell(lang, "dataset", title, body, depth=1, current_route=f"task/{task_id}.html") def _assets(output: Path) -> None: css = r''':root{--bg:#f4f7fb;--panel:#fff;--ink:#102235;--muted:#60748a;--line:#d4e0ec;--accent:#0969da;--good:#087f4f;--bad:#c52c45;--wash:#eaf3ff}html[data-theme=dark]{--bg:#0c1520;--panel:#121f2d;--ink:#eef5fb;--muted:#9db0c2;--line:#2b4054;--accent:#62a8ff;--good:#4ed29b;--bad:#ff7d91;--wash:#142d49}*{box-sizing:border-box}[hidden]{display:none!important}body{margin:0;background:var(--bg);color:var(--ink);font:16px/1.6 Inter,ui-sans-serif,system-ui,-apple-system,"PingFang SC","Microsoft YaHei",sans-serif}.top{height:70px;display:flex;align-items:center;gap:28px;padding:0 max(24px,calc((100% - 1440px)/2));background:var(--panel);border-bottom:1px solid var(--line);position:sticky;top:0;z-index:10}.brand{font-weight:900;color:var(--ink);text-decoration:none;white-space:nowrap}.top nav{display:flex;gap:22px;overflow:auto;flex:1}.top nav a,.language{color:var(--accent);text-decoration:none;white-space:nowrap}.top nav a[aria-current=page]{color:var(--ink);font-weight:800;border-bottom:3px solid var(--ink)}.theme{border:1px solid var(--line);background:var(--panel);color:var(--ink);border-radius:50%;width:38px;height:38px}main,footer{width:min(1440px,calc(100% - 48px));margin:auto}footer{padding:36px 0;color:var(--muted);border-top:1px solid var(--line)}section{padding:34px 0}.hero{padding:46px 0 28px}.hero h1,.detail-hero h1{font-size:clamp(36px,5vw,68px);line-height:1.08;margin:8px 0 14px;letter-spacing:-.04em}.hero p,.detail-hero p{max-width:880px;color:var(--muted);font-size:18px}.eyebrow{text-transform:uppercase;letter-spacing:.14em;color:var(--accent)!important;font-size:13px!important;font-weight:800}h2{font-size:30px;margin:0 0 16px}.table-scroll{overflow:auto;border:1px solid var(--line);border-radius:16px;background:var(--panel)}table{border-collapse:collapse;width:100%;min-width:900px}th,td{text-align:left;vertical-align:top;padding:14px 16px;border-bottom:1px solid var(--line)}th{background:color-mix(in srgb,var(--wash) 70%,var(--panel));font-size:14px;position:sticky;top:0}td{overflow-wrap:anywhere}.metrics td{min-width:150px}.metric-label{display:inline-block;width:42px;color:var(--muted);font-size:13px}.winner{color:var(--good)}.winner>span{display:inline-grid;place-items:center;margin-left:7px;min-width:24px;height:24px;padding:0 6px;border-radius:12px;background:var(--good);color:white;font-size:12px}.callout{padding:16px 18px;border-left:4px solid var(--accent);background:var(--panel);border-radius:0 12px 12px 0}.summary-grid,.diagnosis-grid{display:grid;grid-template-columns:repeat(auto-fit,minmax(220px,1fr));gap:14px}.summary-grid article,.diagnosis-grid article{background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:18px;min-height:150px}.summary-grid strong,.diagnosis-grid strong{display:block;font-size:24px;margin:8px 0}.summary-grid span,.diagnosis-grid span{color:var(--muted);font-size:13px}.verdict.fail{border-left:5px solid var(--bad)}.verdict.pass{border-left:5px solid var(--good)}.filters{display:flex;gap:10px;flex-wrap:wrap;margin:16px 0}.filters input,.filters select{padding:10px 12px;border:1px solid var(--line);background:var(--panel);color:var(--ink);border-radius:9px;min-width:170px}.inventory{height:620px;overflow:auto}.inventory table{min-width:1500px}.inventory tbody tr{cursor:pointer}.inventory tbody tr:hover{background:var(--wash)}.status-pass{color:var(--good);font-weight:800}.status-fail{color:var(--bad);font-weight:800}.failed-step{background:color-mix(in srgb,var(--bad) 10%,var(--panel))}.failed-step td:first-child{border-left:4px solid var(--bad)}pre{white-space:pre-wrap;overflow-wrap:anywhere;max-height:420px;overflow:auto;background:#071923;color:#dff4ff;padding:14px;border-radius:10px;font:13px/1.55 ui-monospace,SFMono-Regular,Menlo,monospace}figure{margin:22px 0}figure img{display:block;max-width:100%;border:1px solid var(--line);border-radius:12px}figcaption{color:var(--muted);font-size:13px}.evidence-actions{display:flex;gap:12px;flex-wrap:wrap}.evidence-actions a{padding:10px 14px;border:1px solid var(--line);border-radius:9px;background:var(--panel);color:var(--accent);text-decoration:none}.detail-hero{padding-top:40px}.viewer{height:75vh}.viewer pre{height:100%;max-height:none}dialog{width:min(850px,calc(100% - 32px));border:1px solid var(--line);border-radius:16px;background:var(--panel);color:var(--ink);padding:0}dialog::backdrop{background:#07111bbf}.dialog-head{display:flex;justify-content:space-between;padding:18px;border-bottom:1px solid var(--line)}.dialog-body{padding:18px}.dialog-head button{border:0;background:none;color:var(--ink);font-size:24px}.mermaid{width:100%;max-width:100%;background:var(--panel);border:1px solid var(--line);border-radius:14px;padding:24px;overflow:auto}.flow-fallback{display:flex;align-items:stretch;gap:10px;overflow:auto;padding:18px;background:var(--panel);border:1px solid var(--line);border-radius:14px}.flow-node{min-width:150px;padding:14px;border-radius:10px;background:var(--wash)}.flow-node strong{display:block;color:var(--accent);margin-bottom:6px}.flow-arrow{display:grid;place-items:center;color:var(--muted);font-size:24px}@media(max-width:720px){.top{padding:0 16px;gap:14px}.top nav{gap:14px}main,footer{width:calc(100% - 28px)}section{padding:26px 0}.hero h1,.detail-hero h1{font-size:36px}.inventory{height:560px}.language{display:none}}''' - js = r'''document.querySelector('.theme')?.addEventListener('click',()=>{const root=document.documentElement;root.dataset.theme=root.dataset.theme==='dark'?'light':'dark';localStorage.setItem('report-theme',root.dataset.theme)});document.documentElement.dataset.theme=localStorage.getItem('report-theme')||'light';const table=document.querySelector('[data-results]');if(table){const rows=[...table.querySelectorAll('tbody tr')],controls=[...document.querySelectorAll('[data-filter]')],dialog=document.querySelector('#diagnosis-dialog');function filter(){const q=(document.querySelector('[data-filter=q]')?.value||'').toLowerCase();rows.forEach(row=>{const ok=controls.every(c=>!c.value||c.dataset.filter==='q'||row.dataset[c.dataset.filter]===c.value)&&(!q||row.textContent.toLowerCase().includes(q));row.hidden=!ok})}controls.forEach(c=>c.addEventListener('input',filter));filter();const resultsUrl=document.documentElement.lang==='zh'?'data/results.json':'../data/results.json';fetch(resultsUrl).then(r=>r.json()).then(data=>{const byKey=Object.fromEntries(data.map(x=>[x.key,x]));rows.forEach(row=>row.addEventListener('click',e=>{if(e.target.closest('a'))return;const x=byKey[row.dataset.key];if(!x||!dialog)return;dialog.querySelector('[data-dialog-title]').textContent=x.browser+' · Task #'+x.task_id;dialog.querySelector('[data-dialog-stage]').textContent=x.failure_stage;dialog.querySelector('[data-dialog-error]').textContent=x.program_error||'—';dialog.querySelector('[data-dialog-cause]').textContent=(document.documentElement.lang==='zh'?x.reason_zh:x.reason_en)||'—';dialog.querySelector('[data-dialog-link]').href='result/'+x.key+'.html';dialog.showModal()}))});dialog?.querySelector('button')?.addEventListener('click',()=>dialog.close())}const viewer=document.querySelector('[data-evidence-viewer]');if(viewer){const file=new URLSearchParams(location.search).get('file');const safe=/^(?:\.\.\/)?data\/evidence\/[a-z0-9.-]+\.json$/.test(file||'');if(safe)fetch(file).then(r=>{if(!r.ok)throw Error(r.status);return r.json()}).then(x=>viewer.textContent=JSON.stringify(x,null,2)).catch(e=>viewer.textContent='Evidence could not be loaded: '+e.message);else viewer.textContent='No evidence file selected.'}''' + css += r''' +html{scroll-padding-top:88px}a{color:var(--accent)}button,summary{cursor:pointer} +.top nav a{padding:20px 0 15px}.top nav a:hover{text-decoration:underline} +.hero{padding:54px 0 38px;border-bottom:1px solid var(--line)}.hero h1{max-width:1100px} +.compact-section{padding:22px 0}.detail-hero{padding:40px 0 28px;border-bottom:1px solid var(--line)} +.detail-hero h1{font-size:clamp(34px,4vw,56px)}.task-intent{max-width:1040px!important;color:var(--ink)!important;line-height:1.55} +.detail-links,.evidence-actions{display:flex;gap:12px;flex-wrap:wrap;margin-top:16px} +.detail-links a,.evidence-actions a,.dialog-body .evidence-actions a{display:inline-flex;align-items:center;min-height:42px;padding:9px 13px;border:1px solid var(--line);background:var(--panel);border-radius:9px;text-decoration:none;font-weight:700} +.detail-links a:hover,.evidence-actions a:hover{border-color:var(--accent);background:var(--wash)} +.peer-list{display:flex;gap:12px;overflow:auto;padding:3px 1px}.peer{display:flex;flex-direction:column;gap:3px;min-width:170px;padding:12px 16px;border:1px solid var(--line);border-radius:12px;background:var(--panel);text-decoration:none;color:var(--ink)} +.peer strong{color:var(--good)}.peer.fail strong{color:var(--bad)}.peer[aria-current=page]{border:2px solid var(--accent);background:var(--wash)} +.verdict-line{display:flex;align-items:center;gap:14px;padding:15px 18px;border-radius:12px;background:color-mix(in srgb,var(--good) 8%,var(--panel));border-left:5px solid var(--good)} +.verdict-line.fail{background:color-mix(in srgb,var(--bad) 9%,var(--panel));border-color:var(--bad)}.verdict-line strong{font-size:25px;color:var(--good)}.verdict-line.fail strong{color:var(--bad)} +.fact-strip{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:10px;margin-top:12px}.fact-strip>div{padding:13px 16px;background:var(--panel);border:1px solid var(--line);border-radius:11px;min-height:84px} +.fact-strip span,.diagnosis-facts span{display:block;color:var(--muted);font-size:13px}.fact-strip strong,.diagnosis-facts strong{display:block;margin-top:7px;font-size:16px;line-height:1.35;overflow-wrap:anywhere} +.diagnosis-panel{background:var(--panel);border:1px solid var(--line);border-left:5px solid var(--bad);border-radius:14px;padding:20px 24px;max-width:1120px} +.diagnosis-panel h3{margin:20px 0 8px}.diagnosis-panel p{max-width:1000px}.diagnosis-panel pre,.dialog-body pre{max-height:360px;white-space:pre-wrap} +.diagnosis-facts{display:grid;grid-template-columns:repeat(auto-fit,minmax(170px,1fr));gap:10px}.diagnosis-facts>div{padding:12px;background:var(--wash);border-radius:10px} +.trace-list{display:grid;grid-template-columns:minmax(0,1fr);gap:9px;max-width:1220px}.trace-step{min-width:0;padding:13px 18px;background:var(--panel);border:1px solid var(--line);border-radius:12px} +.trace-step.failed-step{border:2px solid var(--bad);background:color-mix(in srgb,var(--bad) 6%,var(--panel))}.step-heading{display:flex;align-items:center;gap:12px;flex-wrap:wrap} +.step-number{display:grid;place-items:center;width:29px;height:29px;background:var(--wash);border-radius:50%;font-weight:800}.step-heading strong{min-width:90px}.step-heading>span:last-child{margin-left:auto;color:var(--muted)} +.step-target{margin:7px 0 0 41px;color:var(--muted);font:13px/1.55 ui-monospace,SFMono-Regular,monospace;overflow-wrap:anywhere}.step-preview{margin:6px 0 0 41px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap} +.trace-step details{margin:10px 0 0 41px}.trace-step summary,.matrix-disclosure summary{color:var(--accent);font-weight:700}.step-data{display:grid;grid-template-columns:minmax(260px,1fr) minmax(340px,2fr);gap:12px;min-width:0}.step-data>div{min-width:0}.step-data pre{max-width:100%;max-height:380px} +.dataset-table table{min-width:600px}.dataset-table td,.dataset-table th{overflow-wrap:normal;white-space:nowrap}.task-peer-table{min-width:650px}.planned-table{min-width:900px}.planned-table td:nth-child(3){min-width:320px;max-width:500px;overflow-wrap:anywhere} +.matrix-disclosure{background:var(--panel);border:1px solid var(--line);border-radius:12px;padding:14px 18px}.matrix-disclosure[open] summary{margin-bottom:13px}.matrix-table{max-height:570px}.matrix-table table{min-width:700px} +.filters{align-items:end;gap:12px}.filters label{display:flex;flex-direction:column;gap:5px;font-size:13px;font-weight:700;color:var(--muted)}.filters input,.filters select{min-width:140px;max-width:260px;font-size:15px} +.inventory{height:610px}.inventory table{min-width:1060px}.inventory th,.inventory td{white-space:nowrap;overflow-wrap:normal;vertical-align:middle}.inventory th:nth-child(7),.inventory td:nth-child(7){min-width:230px;max-width:280px;white-space:normal;overflow-wrap:anywhere}.inventory tbody tr:hover{background:var(--wash)} +.inventory button{border:1px solid var(--accent);color:var(--accent);background:var(--panel);padding:6px 10px;border-radius:7px;font-weight:700;margin-left:10px}.inventory button:hover{background:var(--wash)}.result-count{color:var(--muted);font-size:14px} +dialog{max-height:90vh;overflow:auto}.dialog-head{position:sticky;top:0;background:var(--panel);z-index:1}.dialog-body{padding:20px 24px}.dialog-body h3{margin:18px 0 8px}.dialog-body .evidence-actions{margin-bottom:4px} +.viewer-tabs{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.viewer-tabs button{padding:9px 13px;border:1px solid var(--line);background:var(--panel);color:var(--accent);border-radius:8px}.viewer-tabs button[aria-selected=true]{background:var(--accent);color:white}.viewer{height:auto;min-height:50vh}.viewer pre{min-height:45vh;max-height:65vh} +@media(max-width:720px){.hero{padding:30px 0 24px}.top{height:60px}.top nav a{padding:15px 0}.detail-hero h1{font-size:34px}.step-data{grid-template-columns:minmax(0,1fr)}.step-heading>span:last-child{margin-left:0}.trace-step details,.step-target,.step-preview{margin-left:0}.filters label{flex:1 1 145px}.filters input,.filters select{min-width:0;width:100%}.diagnosis-panel{padding:16px}.fact-strip{grid-template-columns:repeat(2,minmax(0,1fr))}} +''' + js = r'''const lang=document.documentElement.lang; +const zh=lang==='zh'; +const base=zh?'':'../'; +document.documentElement.dataset.theme=localStorage.getItem('report-theme')||'light'; +document.querySelector('.theme')?.addEventListener('click',()=>{ + const root=document.documentElement; + root.dataset.theme=root.dataset.theme==='dark'?'light':'dark'; + localStorage.setItem('report-theme',root.dataset.theme); +}); +const fetchJson=async path=>{const response=await fetch(path);if(!response.ok)throw Error(`HTTP ${response.status}`);return response.json()}; +const matrix=document.querySelector('[data-task-matrix]'); +if(matrix){ + const matrixRows=[...matrix.querySelectorAll('tbody tr')]; + const relation=document.querySelector('[data-matrix-relation]'); + const site=document.querySelector('[data-matrix-site]'); + const count=document.querySelector('[data-matrix-count]'); + const applyMatrix=()=>{ + let shown=0; + matrixRows.forEach(row=>{ + const outcomes=JSON.parse(row.dataset.outcomes); + const passes=outcomes.filter(value=>value==='pass').length; + const matchRelation=!relation.value||(relation.value==='different'&&passes>0&&passes{ + const selected=params.get(control.dataset.filter); + if(selected!==null)control.value=selected; + }); + function applyFilters(){ + const query=(document.querySelector('[data-filter=q]')?.value||'').toLowerCase(); + let shown=0; + rows.forEach(row=>{ + const result=byKey[row.dataset.key]; + const haystack=(row.textContent+' '+(result?.intent||'')+' '+(result?.program_error||'')+' '+(result?.reason_zh||'')+' '+(result?.reason_en||'')).toLowerCase(); + const matches=controls.every(control=>!control.value||control.dataset.filter==='q'||row.dataset[control.dataset.filter]===control.value)&&(!query||haystack.includes(query)); + row.hidden=!matches; + if(matches)shown++; + }); + const counter=document.querySelector('[data-result-count]'); + if(counter)counter.textContent=zh?`显示 ${shown} / ${rows.length} 条结果`:`Showing ${shown} / ${rows.length} results`; + } + controls.forEach(control=>control.addEventListener('input',applyFilters)); + fetchJson(base+'data/results.json').then(data=>{byKey=Object.fromEntries(data.map(item=>[item.key,item]));inventory.dataset.resultsReady='true';applyFilters()}).catch(()=>applyFilters()); + inventory.addEventListener('click',async event=>{ + if(event.target.closest('a'))return; + const row=event.target.closest('tr[data-key]'); + if(!row||!dialog)return; + const result=byKey[row.dataset.key]; + if(!result)return; + const detail='result/'+result.key+'.html'; + dialog.querySelector('[data-dialog-title]').textContent=result.browser+' · '+(zh?'任务':'Task')+' #'+result.task_id; + dialog.querySelector('[data-dialog-stage]').textContent=result.failure_stage==='browser_tool_execution'?(zh?'浏览器动作':'Browser action'):result.failure_stage==='official_evaluation'?(zh?'官方验收':'Official evaluation'):(zh?'无':'None'); + dialog.querySelector('[data-dialog-error]').textContent=zh?'正在读取完整证据…':'Loading complete evidence…'; + delete dialog.dataset.loaded; + dialog.querySelector('[data-dialog-link]').href=detail; + dialog.querySelector('[data-dialog-step-link]').href=detail+'#trajectory'; + dialog.showModal(); + try{ + const evidence=await fetchJson(base+'data/evidence/'+result.key+'.json'); + const step=evidence.programmatic_failure?.step; + const failedAction=(evidence.trajectory||[]).find(action=>action.index===step); + dialog.querySelector('[data-dialog-step]').textContent=step?(failedAction?.arguments?.action||'')+' #'+step:(zh?'动作完成后':'After actions completed'); + dialog.querySelector('[data-dialog-step-link]').href=detail+(step?'#step-'+step:'#trajectory'); + dialog.querySelector('[data-dialog-category]').textContent=(zh?evidence.review?.label_zh:evidence.review?.label_en)||(zh?'未复核':'Not reviewed'); + const exact=step?(failedAction?.result||evidence.programmatic_failure?.message||''):evidence.official_evaluation?JSON.stringify(evidence.official_evaluation,null,2):(evidence.programmatic_failure?.message||''); + dialog.querySelector('[data-dialog-error]').textContent=exact; + dialog.querySelector('[data-dialog-cause]').textContent=(zh?evidence.review?.reason_zh:evidence.review?.reason_en)||(zh?'尚无复核判断':'No reviewed cause'); + dialog.querySelector('[data-dialog-remediation]').textContent=(zh?'处理方:':'Owner: ')+(evidence.review?.remediation_owner||'—')+' · '+(zh?'状态:':'Status: ')+(evidence.review?.remediation_status||'—'); + dialog.dataset.loaded='true'; + }catch(error){dialog.querySelector('[data-dialog-error]').textContent=(zh?'证据读取失败:':'Evidence loading failed: ')+error.message;dialog.dataset.loaded='error'} + }); + dialog?.querySelector('.dialog-head button')?.addEventListener('click',()=>dialog.close()); +} +const viewer=document.querySelector('[data-evidence-viewer]'); +if(viewer){ + const selected=new URLSearchParams(location.search).get('file'); + const safe=/^(?:\.\.\/)?data\/evidence\/[a-z0-9.-]+\.json$/.test(selected||''); + const tabs=[...document.querySelectorAll('[data-evidence-tab]')]; + const heading=document.querySelector('[data-evidence-heading]'); + const raw=document.querySelector('[data-evidence-raw]'); + const image=document.querySelector('[data-evidence-image]'); + const render=(evidence,section)=>{ + const labels={result:zh?'结果与资源':'Result and resources',programmatic_failure:zh?'程序定位':'Programmatic finding',review:zh?'复核原因':'Reviewed cause',trajectory:zh?'执行轨迹与完整日志':'Trajectory and full logs',official_evaluation:zh?'官方验收':'Official evaluation'}; + heading.textContent=labels[section]||section; + viewer.textContent=JSON.stringify(evidence[section]??null,null,2); + tabs.forEach(tab=>tab.setAttribute('aria-selected',String(tab.dataset.evidenceTab===section))); + }; + if(!safe){viewer.textContent=zh?'没有选中证据文件。':'No evidence file selected.'} + else fetchJson(selected).then(evidence=>{ + raw.href=selected; + if(evidence.screenshot){image.href=base+evidence.screenshot;image.textContent=zh?'查看最后页面原图':'Open last observed page';image.hidden=false} + tabs.forEach(tab=>tab.addEventListener('click',()=>render(evidence,tab.dataset.evidenceTab))); + render(evidence,'result'); + }).catch(error=>viewer.textContent=(zh?'证据读取失败:':'Evidence loading failed: ')+error.message); +}''' (output / "assets").mkdir(parents=True, exist_ok=True) (output / "assets/report.css").write_text(css, encoding="utf-8") (output / "assets/app.js").write_text(js, encoding="utf-8") @@ -382,6 +624,10 @@ def build_webarena_replay_report( for item in comparable_rows ] ) + memory_peak = _stat([float((item.get("resource") or {}).get("memory_peak_bytes") or 0) for item in comparable_rows]) + network_rx = _stat([float((item.get("resource") or {}).get("network_rx_bytes") or 0) for item in comparable_rows]) + network_tx = _stat([float((item.get("resource") or {}).get("network_tx_bytes") or 0) for item in comparable_rows]) + cpu_utilization = _stat([float((item.get("resource") or {}).get("cpu_average_utilization_pct") or 0) for item in comparable_rows]) method_rows.append( { "id": method["id"], @@ -398,6 +644,10 @@ def build_webarena_replay_report( "cpu_mean": cpu["mean"], "memory": memory, "memory_mean": memory["mean"], + "memory_peak": memory_peak, + "network_rx": network_rx, + "network_tx": network_tx, + "cpu_utilization": cpu_utilization, "physical_executions": len( [item for item in results["physical_executions"] if item["engine"] == engine] ), @@ -405,11 +655,13 @@ def build_webarena_replay_report( ) public_rows = [] + public_evidence: dict[tuple[str, int], dict[str, Any]] = {} for attempt in attempts: key = (attempt["engine"], int(attempt["task_id"])) task = tasks[key[1]] review = reviews.get(key) evidence = _public_evidence(attempt, review, output) + public_evidence[key] = evidence failure = evidence["programmatic_failure"] cause = review["cause_group"] if review else {} row = { @@ -430,12 +682,23 @@ def build_webarena_replay_report( "reason_en": review.get("reason_en", "") if review else "", } public_rows.append(row) + rows_by_task = defaultdict(list) + for row in public_rows: + rows_by_task[int(row["task_id"])].append(row) + for attempt in attempts: + key = (str(attempt["engine"]), int(attempt["task_id"])) + row = next(item for item in rows_by_task[key[1]] if item["engine"] == key[0]) for lang in ("zh", "en"): target = output / ("result" if lang == "zh" else "en/result") / f"{row['key']}.html" target.parent.mkdir(parents=True, exist_ok=True) target.write_text( - _detail_page(lang, attempt, task, methods[key[0]], evidence), encoding="utf-8" + _detail_page(lang, attempt, tasks[key[1]], methods[key[0]], public_evidence[key], rows_by_task[key[1]]), encoding="utf-8" ) + for task_id, task in tasks.items(): + for lang in ("zh", "en"): + target = output / ("task" if lang == "zh" else "en/task") / f"{task_id}.html" + target.parent.mkdir(parents=True, exist_ok=True) + target.write_text(_task_page(lang, task, rows_by_task[task_id]), encoding="utf-8") _write_json(output / "data/results.json", public_rows) _write_json(output / "data/metrics.json", {"methods": method_rows, "passing_intersection": len(passing_tasks)}) @@ -495,26 +758,6 @@ def build_webarena_replay_report( cells.append(f"{text}
    {metric['n']} {calls_label}") action_rows.append(f'{_e(action)}{"".join(cells)}') action_table = f'
    {"".join(f"" for h in action_headers)}{"".join(action_rows)}
    {_e(h)}
    ' - method_cards = [] - for row in method_rows: - failed_count = row["total"] - row["passed"] - if failed_count: - note = ( - f"{failed_count} 条最终失败已逐条复核。" - if zh - else f"{failed_count} final failures were reviewed individually." - ) - else: - note = ( - "固定轨迹与验收合同全部通过。" - if zh - else "Every fixed trajectory and acceptance contract passed." - ) - method_cards.append( - f'
    {"浏览器结果" if zh else "Browser result"}' - f'{_e(row["label"])} {row["passed"]}/{row["total"]}' - f"

    {note}

    " - ) task_count = len(tasks) shared_count = len(passing_tasks) eyebrow = ( @@ -522,30 +765,56 @@ def build_webarena_replay_report( if zh else f"WebArena-Verified · {task_count} tasks · model calls 0" ) - summary_body = f'''

    {eyebrow}

    {"浏览器轨迹重放评测" if zh else "Browser Trajectory Replay"}

    {"同一组经 Chromium 资格验证的固定 BrowserTool 轨迹,依次在各注册浏览器中执行。Agent 与模型不参与测试。" if zh else "The same Chromium-qualified BrowserTool trajectories run on every registered browser. No Agent or model participates in the benchmark."}

    {"核心指标" if zh else "Core metrics"}

    {f"正确率覆盖全部 {task_count} 题;端到端耗时、CPU 和内存只比较所有浏览器共同通过的 {shared_count} 题,避免提前失败缩短工作量。单次动作统计包含重试中的实际调用;调用数不判优。" if zh else f"Correctness covers all {task_count} tasks. End-to-end time, CPU, and memory use the {shared_count} tasks passed by every browser so early failure cannot reduce workload. Per-action latency includes actual retry calls; call counts are not ranked."}

    {_metrics_table(method_rows, lang)}

    {"结论" if zh else "Findings"}

    {"".join(method_cards)}
    {"执行隔离" if zh else "Execution isolation"}{"串行" if zh else "Serial"}

    {"每题重置;方法之间销毁浏览器和站点。" if zh else "Reset per task; browser and sites torn down between methods."}

    {"浏览器动作耗时" if zh else "Browser action latency"}

    {"每格给出均值、P50、P95 和调用次数;耗时在所有注册浏览器间判优,调用次数只反映工作量。" if zh else "Each cell shows mean, P50, P95, and call count. Latency is ranked across every registered browser; counts only describe workload."}

    {action_table}
    ''' - (root / "index.html").write_text(_shell(lang, "index", "浏览器轨迹重放" if zh else "Browser Trajectory Replay", summary_body), encoding="utf-8") - - headers = ["Task", "Site", *[item["label"] for item in methods.values()]] + stage_rows = [] + for engine, method in methods.items(): + for code, label_pair in (("browser_tool_execution", ("浏览器动作", "Browser action")), ("official_evaluation", ("官方验收", "Official evaluation"))): + count = sum(row["engine"] == engine and row["status"] != "pass" and row["failure_stage"] == code for row in public_rows) + if count: + stage_rows.append(f'{_e(method["label"])}{label_pair[0 if zh else 1]}{count}{"下钻到失败结果" if zh else "Inspect failures"} →') + stage_table = f'
    {"".join(stage_rows)}
    {"浏览器" if zh else "Browser"}{"失败环节" if zh else "Failure stage"}{"结果数" if zh else "Outcomes"}{"逐题证据" if zh else "Per-task evidence"}
    ' + summary_body = f'''

    {eyebrow}

    {"浏览器轨迹重放评测" if zh else "Browser Trajectory Replay"}

    {"90 条经过资格验证的固定操作轨迹依次在全部注册浏览器上执行。Agent 与模型不参与;每题重置站点,浏览器方法之间完全销毁环境。" if zh else "The same 90 qualified fixed action trajectories ran serially on every registered browser. No Agent or model participated. Sites were reset per task and torn down between browser methods."}

    +

    {"核心指标" if zh else "Core metrics"}

    {f"通过率覆盖全部 {task_count} 题;端到端耗时、CPU 和内存只比较所有浏览器共同通过的 {shared_count} 题,避免提前失败缩短工作量。单次动作耗时含实际工具调用,调用次数不判优。" if zh else f"Pass rate covers all {task_count} tasks. End-to-end time, CPU, and memory use the {shared_count} tasks passed by every browser so early failure cannot shorten workload. Per-action latency covers actual tool calls; call counts are not ranked."}

    {_metrics_table(method_rows, lang)}
    +

    {"资源与网络" if zh else "Resources and network"}

    {f"下表也只统计共同通过的 {shared_count} 题;每个数值按单题采样。网络传输量和 CPU 使用率不直接判优,低数值可能来自缓存或等待,而非浏览器更快。" if zh else f"These per-task samples also cover the {shared_count} shared passes. Network bytes and CPU utilization are not ranked: lower values can reflect caching or waiting, not a faster browser."}

    {"展开资源与网络统计" if zh else "Open resource and network statistics"}{_resource_table(method_rows, lang)}
    +

    {"失败发生在哪个环节" if zh else "Where failures occurred"}

    {"浏览器动作失败是轨迹没有完整执行;官方验收失败是动作执行结束但任务合同未通过。点击行末入口可看到失败动作、原始报错和复核根因。" if zh else "Browser-action failures stop the trajectory before completion; official-evaluation failures complete actions but fail the task contract. Follow a row to the action, exact error, and reviewed cause."}

    {stage_table}
    +

    {"浏览器动作耗时" if zh else "Browser action latency"}

    {"均值、P50、P95 按单次调用统计;调用次数只说明工作量。下钻到结果详情可查看具体动作耗时和工具返回。" if zh else "Mean, P50, and P95 are per-call statistics; call counts only describe workload. Open a task result for the individual action timing and full tool response."}

    {"展开各类动作耗时" if zh else "Open latency by action type"}{action_table}
    ''' + (root / "index.html").write_text(_shell(lang, "index", "浏览器轨迹重放" if zh else "Browser Trajectory Replay", summary_body, current_route="index.html"), encoding="utf-8") + + headers = ["任务" if zh else "Task", "站点" if zh else "Site", "来源" if zh else "Source", "标准动作" if zh else "Qualified actions"] matrix_rows = [] for task_id, task in tasks.items(): - values = [] - for engine in methods: - row = next(item for item in public_rows if item["engine"] == engine and item["task_id"] == task_id) - label = ("通过" if row["status"] == "pass" else "失败") if zh else row["status"].title() - values.append(f'{label}') - matrix_rows.append(f'{task_id}{_e(", ".join(task["task"].get("sites") or []))}{"".join(f"{v}" for v in values)}') - dataset_body = f'

    {f"{task_count} 条标准轨迹" if zh else f"{task_count} qualified trajectories"}

    {f"每题一条固定操作轨迹。{browser_use_source_count} 条来自干净的 Browser Use 成功执行,{repaired_source_count} 条经修复后重新在 {reference_label} 验证。" if zh else f"One fixed trajectory per task: {browser_use_source_count} clean Browser Use successes and {repaired_source_count} repaired trajectories re-qualified on {reference_label}."}

    {"逐题结果矩阵" if zh else "Per-task result matrix"}

    {"".join(f"" for h in headers)}{"".join(matrix_rows)}
    {_e(h)}
    ' - (root / "dataset.html").write_text(_shell(lang, "dataset", "评测集" if zh else "Dataset", dataset_body), encoding="utf-8") + source = str((task.get("source") or {}).get("kind") or "") + source_text = ("自主执行成功" if zh else "Autonomous success") if source == "authoritative_successful_browser_use_execution" else ("修复后验证" if zh else "Repaired and qualified") + matrix_rows.append(f'#{task_id}{_e(", ".join(task["task"].get("sites") or []))}{source_text}{len(task.get("tool_calls") or [])}') + dataset_body = f'

    WebArena-Verified · {task_count}

    {"评测集与标准轨迹" if zh else "Dataset and qualified trajectories"}

    {f"每题一条固定操作轨迹。{browser_use_source_count} 条来自成功的自主浏览器执行,{repaired_source_count} 条经修复并在 {reference_label} 重新验证。点击任务查看目标、验收输入和全部预定动作。" if zh else f"One qualified path per task: {browser_use_source_count} from successful autonomous runs and {repaired_source_count} repaired and re-qualified on {reference_label}. Open a task for its goal, evaluation input, and full planned actions."}

    {"90 题任务定义" if zh else "Task definitions"}

    {"".join(f"" for h in headers)}{"".join(matrix_rows)}
    {_e(h)}

    {"查看同题实测结果 →" if zh else "Compare measured outcomes →"}

    ' + (root / "dataset.html").write_text(_shell(lang, "dataset", "评测集" if zh else "Dataset", dataset_body, current_route="dataset.html"), encoding="utf-8") result_rows = [] for row in public_rows: cause = row["cause_zh"] if zh else row["cause_en"] - reason = row["reason_zh"] if zh else row["reason_en"] status = ("通过" if row["status"] == "pass" else "失败") if zh else row["status"].title() - result_rows.append(f'{_e(row["browser"])} · {row["task_id"]}{_e(row["sites"][0] if row["sites"] else "")}{status}{row["completed_actions"]}/{row["action_count"]}{_e(row["failure_stage"])}{_e(row["program_error"])}{_e(cause)}
    {_e(reason)}') + stage_label = {"browser_tool_execution": "浏览器动作" if zh else "Browser action", "official_evaluation": "官方验收" if zh else "Official evaluation", "pass": "—"}.get(row["failure_stage"], row["failure_stage"]) + result_rows.append(f'#{row["task_id"]}{_e(row["browser"])}{_e(row["sites"][0] if row["sites"] else "")}{status}{row["completed_actions"]}/{row["action_count"]}{_e(stage_label)}{_e(cause) if cause else "—"}{"证据页" if zh else "Evidence page"}') browser_options = "".join(f'' for key, item in methods.items()) - results_body = f'''

    {"结果与失败诊断" if zh else "Results and failure diagnosis"}

    {"默认显示失败。点击任一行先看诊断弹窗,再进入详情页查看轨迹、日志和最后页面。" if zh else "Failures are shown by default. Open a row for the diagnosis dialog, then continue to the detail page for trajectory, logs, and last page."}

    {''.join(result_rows)}
    {"结果" if zh else "Result"}{"站点" if zh else "Site"}{"判定" if zh else "Verdict"}{"动作" if zh else "Actions"}{"失败阶段" if zh else "Failure stage"}{"程序报错" if zh else "Program error"}{"复核根因" if zh else "Reviewed root cause"}

    {"失败阶段" if zh else "Failure stage"}

    {"程序报错" if zh else "Program error"}

    {"复核根因" if zh else "Reviewed root cause"}

    {"打开完整详情" if zh else "Open full detail"}
    ''' - (root / "results.html").write_text(_shell(lang, "results", "结果与诊断" if zh else "Results", results_body), encoding="utf-8") + site_options = "".join(f'' for site in sorted({s for row in public_rows for s in row["sites"]})) + stage_options = '' + causes = {row["cause_zh"]: row["cause_zh"] if zh else row["cause_en"] for row in public_rows if row["cause_zh"]} + cause_options = "".join(f'' for code, label in sorted(causes.items())) + result_matrix_rows = [] + for task_id, task in tasks.items(): + values = [] + outcomes = [] + for engine in methods: + row = next(item for item in rows_by_task[task_id] if item["engine"] == engine) + outcomes.append(row["status"]) + label = ("通过" if row["status"] == "pass" else "失败") if zh else row["status"].title() + values.append(f'{label}') + site = (task["task"].get("sites") or [""])[0] + result_matrix_rows.append(f'#{task_id}{_e(", ".join(task["task"].get("sites") or []))}{"".join(f"{v}" for v in values)}') + matrix_headers = "".join(f'{_e(item["label"])}' for item in methods.values()) + matrix_sites = "".join(f'' for site in sorted({(task["task"].get("sites") or [""])[0] for task in tasks.values()})) + result_matrix = f'
    {"同题全部浏览器结果" if zh else "Same-task outcomes across all browsers"}
    {matrix_headers}{"".join(result_matrix_rows)}
    {"任务" if zh else "Task"}{"站点" if zh else "Site"}
    ' + results_body = f'''

    {len(public_rows)} {"条逻辑结果" if zh else "logical outcomes"}

    {"结果与失败诊断" if zh else "Results and failure diagnosis"}

    {"逐题矩阵比较全部浏览器;筛选表默认显示失败,点击“查看诊断”读取完整报错、复核判断和执行证据。" if zh else "The same-task matrix compares every browser. The inventory starts with failures; open a diagnosis for the full error, reviewed cause, and execution evidence."}

    {result_matrix}

    {''.join(result_rows)}
    {"任务" if zh else "Task"}{"浏览器" if zh else "Browser"}{"站点" if zh else "Site"}{"判定" if zh else "Verdict"}{"动作" if zh else "Actions"}{"失败环节" if zh else "Failure stage"}{"复核分类" if zh else "Reviewed category"}{"详情" if zh else "Detail"}
    {"失败环节" if zh else "Failure stage"}
    {"失败动作" if zh else "Failed action"}
    {"复核分类" if zh else "Reviewed category"}

    {"原始报错与官方验收" if zh else "Exact error and official evaluation"}

    {"复核原因与处理" if zh else "Reviewed cause and treatment"}

    ''' + (root / "results.html").write_text(_shell(lang, "results", "结果与诊断" if zh else "Results", results_body, current_route="results.html"), encoding="utf-8") flow_labels = ( ("重放器", "浏览器容器", "本地站点", "逐题重置", "证据采集", "官方验收", "完全销毁") @@ -567,13 +836,15 @@ def build_webarena_replay_report( X --> T[Teardown browser and sites] T --> R''' environment_body = f'''

    {"环境与评测流程" if zh else "Environment and evaluation flow"}

    {"192.168.5.2 是 Colima 虚拟机中的宿主网关:浏览器容器通过它访问运行在同一台 Mac 虚拟化环境里的 WebArena 站点。它不是公网测试网站。" if zh else "192.168.5.2 is the host gateway inside the Colima VM. Browser containers use it to reach the WebArena sites running in the same Mac virtualization environment; it is not a public test website."}

    {"执行拓扑" if zh else "Execution topology"}

    {fallback}{topology}

    {"隔离规则" if zh else "Isolation rules"}

    1

    {"一套固定端口站点环境,因此只允许一个方法运行。" if zh else "One fixed-port site stack permits only one running method."}

    {task_count}

    {"一个浏览器完整跑完全部任务,每题前恢复官方夹具。" if zh else "A browser completes all tasks; official fixtures reset before each task."}

    0

    {"没有 Agent 决策和模型请求;输入是冻结的 BrowserTool 动作。" if zh else "No Agent decisions or model calls; inputs are frozen BrowserTool actions."}

    ''' - (root / "environment.html").write_text(_shell(lang, "environment", "环境与方法" if zh else "Environment", environment_body), encoding="utf-8") + (root / "environment.html").write_text(_shell(lang, "environment", "环境与方法" if zh else "Environment", environment_body, current_route="environment.html"), encoding="utf-8") - review_body = f'''

    {"核验记录" if zh else "Review history"}

    {"记录影响当前数据集与结论的核验轮次;逐题证据位于结果详情页。" if zh else "Review rounds that changed the current dataset or conclusions. Per-result evidence remains on result pages."}

    2026-09-15{f"{task_count} 条轨迹资格核验" if zh else f"{task_count}-trajectory qualification"}

    {f"修复已确认的非浏览器问题,重新验证后 {reference_label} {reference_row['passed']}/{reference_row['total']}。" if zh else f"Confirmed non-browser defects were repaired; {reference_label} then qualified {reference_row['passed']}/{reference_row['total']}."}

    2026-09-15{f"{reviewed_failure_count} 条失败根因复核" if zh else f"{reviewed_failure_count}-failure root-cause review"}

    {f"自动失败位置与复核根因分离;{reviewed_failure_count}/{reviewed_failure_count} 结果绑定当前证据哈希,无未解决项。" if zh else f"Programmatic locations remain separate from reviewed causes; {reviewed_failure_count}/{reviewed_failure_count} are bound to the current evidence hash with no unresolved items."}

    ''' - (root / "review.html").write_text(_shell(lang, "review", "核验记录" if zh else "Review history", review_body), encoding="utf-8") + review_body = f'''

    {"核验记录" if zh else "Review history"}

    {"记录影响当前数据集与结论的核验轮次;逐题证据位于结果详情页。" if zh else "Review rounds that changed the current dataset or conclusions. Per-result evidence remains on result pages."}

    2026-09-15{f"{task_count} 条轨迹资格核验" if zh else f"{task_count}-trajectory qualification"}

    {f"修复已确认的非浏览器问题,重新验证后 {reference_label} {reference_row['passed']}/{reference_row['total']}。" if zh else f"Confirmed non-browser defects were repaired; {reference_label} then qualified {reference_row['passed']}/{reference_row['total']}."}

    2026-09-15{f"{reviewed_failure_count} 条失败根因复核" if zh else f"{reviewed_failure_count}-failure root-cause review"}

    {f"自动失败位置与复核根因分离;{reviewed_failure_count}/{reviewed_failure_count} 个失败结果绑定当前证据哈希,没有未复核的失败。后续修复状态见结果详情。" if zh else f"Programmatic locations remain separate from reviewed causes; {reviewed_failure_count}/{reviewed_failure_count} failures are bound to the current evidence hash. No failures remain unreviewed; remediation states are shown on result pages."}

    ''' + (root / "review.html").write_text(_shell(lang, "review", "核验记录" if zh else "Review history", review_body, current_route="review.html"), encoding="utf-8") - viewer_body = f'

    {"证据查看器" if zh else "Evidence viewer"}

    {"网页直接加载选中的净化证据;也可下载原始 JSON。" if zh else "Loads the selected sanitized evidence directly in the page; raw JSON remains downloadable."}

    ' - (root / "evidence-viewer.html").write_text(_shell(lang, "results", "证据查看器" if zh else "Evidence viewer", viewer_body), encoding="utf-8") + viewer_tabs = (("result", "结果与资源", "Result and resources"), ("programmatic_failure", "程序定位", "Programmatic finding"), ("review", "复核原因", "Reviewed cause"), ("trajectory", "动作与日志", "Actions and logs"), ("official_evaluation", "官方验收", "Official evaluation")) + tab_buttons = "".join(f'' for key, label_zh, label_en in viewer_tabs) + viewer_body = f'

    {"证据查看器" if zh else "Evidence viewer"}

    {"按结果、动作、程序判定和复核原因查看完整净化证据;原始 JSON 仍可下载。" if zh else "Inspect full sanitized evidence by outcome, action, programmatic finding, and review. The raw JSON remains downloadable."}

    {tab_buttons}

    ' + (root / "evidence-viewer.html").write_text(_shell(lang, "results", "证据查看器" if zh else "Evidence viewer", viewer_body, current_route="evidence-viewer.html"), encoding="utf-8") manifest = { "schema": "lex_webarena_trajectory_replay_report/1", @@ -586,7 +857,7 @@ def build_webarena_replay_report( "logical_results": len(public_rows), "failed_results": sum(row["status"] != "pass" for row in public_rows), "reviewed_failures": len(reviews), - "routes": 12 + 2 * len(public_rows), + "routes": 12 + 2 * len(public_rows) + 2 * len(tasks), } _write_json(output / "report-manifest.json", manifest) return manifest diff --git a/research/report-changes.json b/research/report-changes.json index b47eb0b43..1bca9aa5a 100644 --- a/research/report-changes.json +++ b/research/report-changes.json @@ -36,6 +36,38 @@ } } } + }, + { + "id": "2026-09-15-trajectory-replay-drilldown", + "date": "2026-09-15", + "title": "补齐轨迹重放报告的逐题诊断与站点导航", + "summary": "新增每题的目标、验收输入和标准动作页;结果页可筛选同题浏览器成败关系,并从失败结果下钻到确切报错、失败动作、复核原因、完整工具返回、页面截图和公开证据。核心指标增加资源与网络统计。", + "publication": { + "service": "Artifact Site", + "site_slug": "7dF7uNK5SKIx", + "share_url": "https://artifact-hub.app.lexmount.com/v/hkZt38QFBpNIKtThKnqjpnBQk8At19PM", + "version": "ver_0f4f162b973a4ba7afba2fb4e18d6173" + }, + "effects": { + "score": "Chromium 90/90、Moli 54/90,分数未变。", + "denominator": "仍为每浏览器 90 题、共 180 个逻辑结果。", + "latency": "原有耗时未变;补充共同通过 54 题的资源与网络统计。", + "evidence": "771 个公开文件、552 个 HTML 页面、10,154 条本地链接和 72 个视觉状态通过检查;Sites 嵌套详情导航已实测恢复。", + "limitations": "逐题证据用于定位本轮失败,不能从单次结果外推总体稳定性。" + }, + "i18n": { + "en": { + "title": "Add per-task replay diagnosis and repair hosted navigation", + "summary": "Added task pages for goals, evaluation input, and qualified actions. Results now filter same-task outcome relations and drill down to the exact error, failed action, reviewed cause, full tool response, last page, and public evidence. Core metrics add resource and network statistics.", + "effects": { + "score": "Scores remain Chromium 90/90 and Moli 54/90.", + "denominator": "Still 90 tasks per browser and 180 logical outcomes.", + "latency": "Measured timings are unchanged; resource and network statistics use the 54 shared passes.", + "evidence": "Checks covered 771 public files, 552 HTML pages, 10,154 local links, and 72 visual states. Hosted navigation from nested result pages was verified.", + "limitations": "Per-task evidence locates failures in this run; one outcome per task does not establish general reliability." + } + } + } } ], "browser_use_reviews": [ diff --git a/scripts/check_webarena_replay_report.py b/scripts/check_webarena_replay_report.py index 75394fa6f..d22d7fa13 100644 --- a/scripts/check_webarena_replay_report.py +++ b/scripts/check_webarena_replay_report.py @@ -118,6 +118,10 @@ def check(report_dir: Path) -> dict[str, int]: en_details = {path.name for path in (root / "en/result").glob("*.html")} if zh_details != en_details or len(zh_details) != expected_results: raise ValueError("Chinese and English result routes must match one-to-one") + zh_tasks = {path.name for path in (root / "task").glob("*.html")} + en_tasks = {path.name for path in (root / "en/task").glob("*.html")} + if zh_tasks != en_tasks or zh_tasks != {f"{task_id}.html" for task_id in task_ids}: + raise ValueError("every task needs matching Chinese and English contract routes") core_names = ( "index.html", "dataset.html", @@ -167,7 +171,7 @@ def check(report_dir: Path) -> dict[str, int]: if any(marker in text for marker in untranslated_zh_markers): raise ValueError(f"Chinese report contains untranslated interface text: {page}") _check_english_localization( - [*core[len(core_names) :], *(root / "en/result").glob("*.html")] + [*core[len(core_names) :], *(root / "en/result").glob("*.html"), *(root / "en/task").glob("*.html")] ) checked_links = 0 diff --git a/scripts/check_webarena_replay_visual.py b/scripts/check_webarena_replay_visual.py index 1c4a7b840..45b595b3e 100644 --- a/scripts/check_webarena_replay_visual.py +++ b/scripts/check_webarena_replay_visual.py @@ -43,6 +43,29 @@ class _QuietHandler(http.server.SimpleHTTPRequestHandler): def log_message(self, *_args: object) -> None: return + def do_GET(self) -> None: + for prefix in ("/api/preview/replay~fixture/", "/api/preview/replay/"): + if not self.path.startswith(prefix): + continue + route = self.path[len(prefix):].split("?", 1)[0] + target = (Path(self.directory) / route).resolve() + if not target.is_relative_to(Path(self.directory).resolve()) or not target.is_file(): + self.send_error(404) + return + if prefix.endswith("~fixture/") and target.suffix == ".html": + content = target.read_text(encoding="utf-8") + content = content.replace("", '', 1) + encoded = content.encode("utf-8") + self.send_response(200) + self.send_header("Content-Type", "text/html; charset=utf-8") + self.send_header("Content-Length", str(len(encoded))) + self.end_headers() + self.wfile.write(encoded) + return + self.path = "/" + route + return super().do_GET() + return super().do_GET() + def _routes(report: Path) -> list[str]: results = json.loads((report / "data/results.json").read_text(encoding="utf-8")) @@ -50,6 +73,11 @@ def _routes(report: Path) -> list[str]: (item["key"] for item in results if item["status"] == "fail"), None, ) + passed_key = next( + (item["key"] for item in results if item["status"] == "pass"), + None, + ) + task_id = next((item.get("task_id") for item in results if item["status"] == "fail"), None) fixed = [ "index.html", "dataset.html", @@ -58,6 +86,8 @@ def _routes(report: Path) -> list[str]: "review.html", ] routes = [*fixed, *(f"en/{item}" for item in fixed)] + if task_id is not None and (report / "task" / f"{task_id}.html").is_file(): + routes.extend((f"task/{task_id}.html", f"en/task/{task_id}.html")) if failed_key is not None: routes.extend( ( @@ -67,6 +97,8 @@ def _routes(report: Path) -> list[str]: f"en/evidence-viewer.html?file=../data/evidence/{failed_key}.json", ) ) + if passed_key is not None and (report / "result" / f"{passed_key}.html").is_file(): + routes.extend((f"result/{passed_key}.html", f"en/result/{passed_key}.html")) return routes @@ -91,12 +123,26 @@ def check(report: Path, receipt: Path) -> None: headless=True, ) try: + failed_key = next(item["key"] for item in json.loads((report / "data/results.json").read_text(encoding="utf-8")) if item["status"] == "fail") + route_page = browser.new_page() + route_page.goto(f"http://127.0.0.1:{server.server_port}/api/preview/replay~fixture/result/{failed_key}.html", wait_until="domcontentloaded") + route_page.wait_for_function("() => document.baseURI.includes('/api/preview/replay/result/')") + route_page.wait_for_function("() => !!document.documentElement.dataset.theme") + route_page.wait_for_function("() => getComputedStyle(document.body).backgroundColor !== 'rgba(0, 0, 0, 0)'") + assert "/api/preview/replay/result/" in route_page.locator('.peer-list a').first.evaluate("element => element.href") + assert "/api/preview/replay/evidence-viewer.html" in route_page.locator('#evidence .evidence-actions a').first.evaluate("element => element.href") + route_page.locator('nav a[href="../index.html"]').first.click() + route_page.wait_for_url(f"http://127.0.0.1:{server.server_port}/api/preview/replay/index.html") + route_page.locator('h1').wait_for() + route_page.close() for index, route in enumerate(_routes(report)): for width, height in ((1440, 1000), (390, 844)): for theme in ("light", "dark"): page = browser.new_page(viewport={"width": width, "height": height}) url = f"http://127.0.0.1:{server.server_port}/{route}" - page.goto(url, wait_until="load", timeout=10_000) + # The optional Mermaid module is hosted remotely; core report + # content and its local interactions are ready at DOMContentLoaded. + page.goto(url, wait_until="domcontentloaded", timeout=10_000) if "evidence-viewer" in route: page.wait_for_function( """() => { @@ -105,6 +151,16 @@ def check(report: Path, receipt: Path) -> None: }""", timeout=5_000, ) + if route.endswith("results.html"): + page.wait_for_function("() => document.querySelector('[data-matrix-count]')?.textContent.includes('/')", timeout=5_000) + page.locator('[data-matrix-relation]').select_option('all-pass') + page.wait_for_function("() => [...document.querySelectorAll('[data-task-matrix] tbody tr:not([hidden])')].every(row => JSON.parse(row.dataset.outcomes).every(value => value === 'pass'))") + page.locator('[data-matrix-relation]').select_option('different') + page.wait_for_function("() => [...document.querySelectorAll('[data-task-matrix] tbody tr:not([hidden])')].every(row => new Set(JSON.parse(row.dataset.outcomes)).size > 1)") + page.locator('[data-results-ready="true"]').wait_for(timeout=5_000) + page.locator('.inventory tbody tr:not([hidden]) [data-open-diagnosis]').first.click() + page.locator('#diagnosis-dialog[data-loaded]').wait_for(timeout=5_000) + page.locator('.dialog-head button').click() page.evaluate( "value => document.documentElement.dataset.theme=value", theme, @@ -114,7 +170,7 @@ def check(report: Path, receipt: Path) -> None: const root=document.documentElement; const clipped=[]; const layoutSelector='h1,h2,.filters input,.filters select,'+ - '.diagnosis-grid article,.summary-grid article'; + '.diagnosis-grid article,.summary-grid article,.fact-strip>div'; for(const element of document.querySelectorAll(layoutSelector)){ if(!element.getClientRects().length)continue; const box=element.getBoundingClientRect(); @@ -171,7 +227,7 @@ def check(report: Path, receipt: Path) -> None: "results.html", "environment.html", } - or route.startswith("result/") + or route.startswith(("result/", "task/")) ) ): shot = screenshot_dir / f"{index}-{width}.png" diff --git a/tests/trajectory_replay/test_webarena_report.py b/tests/trajectory_replay/test_webarena_report.py index faf541add..faf07f620 100644 --- a/tests/trajectory_replay/test_webarena_report.py +++ b/tests/trajectory_replay/test_webarena_report.py @@ -2,7 +2,13 @@ import pytest -from harness.trajectory_replay.webarena_report import _clean, _metrics_table, _shell +from harness.trajectory_replay.webarena_report import ( + _clean, + _detail_page, + _metrics_table, + _shell, + _task_page, +) from scripts.check_webarena_replay_report import _check_english_localization @@ -65,6 +71,45 @@ def test_report_shell_localizes_chinese_title_and_footer() -> None: assert "浏览器轨迹重放" in rendered assert "WebArena-Verified 1.2.3 · 浏览器轨迹重放" in rendered assert "Browser Trajectory Replay" not in rendered + detail = _shell("zh", "results", "结果", "

    内容

    ", depth=1, current_route="result/moli-29.html") + assert 'href="../en/result/moli-29.html"' in detail + english = _shell("en", "results", "Result", "

    Content

    ", depth=1, current_route="result/moli-29.html") + assert 'href="../../result/moli-29.html"' in english + + +def test_replay_task_and_result_have_connected_drilldown() -> None: + task = { + "task_id": 29, + "task": {"intent": "Find the latest post", "sites": ["reddit"]}, + "source": {"kind": "expert_repair_spec"}, + "tool_calls": [{"action": "click", "selector": "#latest"}], + "evaluation_input": {"task_type": "RETRIEVE", "status": "SUCCESS"}, + } + peers = [ + {"engine": "chromium", "browser": "Chromium", "task_id": 29, "status": "pass", "completed_actions": 1, "action_count": 1}, + {"engine": "moli", "browser": "Moli", "task_id": 29, "status": "fail", "completed_actions": 0, "action_count": 1}, + ] + task_html = _task_page("zh", task, peers) + assert 'href="../result/moli-29.html#diagnosis"' in task_html + assert 'href="../result/chromium-29.html#trajectory"' in task_html + assert 'href="../en/task/29.html"' in task_html + assert "冻结的操作轨迹" in task_html + assert "信息提取" in task_html + + attempt = {"engine": "moli", "task_id": 29, "status": "fail", "completed_action_count": 0, "action_count": 1, "duration_ms": 1200, "resource": {"cpu_ms": 100, "memory_mean_bytes": 10_000_000}} + evidence = { + "programmatic_failure": {"stage": "browser_tool_execution", "step": 1, "message": "Click failed: timed out"}, + "review": {"label_zh": "目标不可操作", "label_en": "Target not actionable", "reason_zh": "目标没有出现", "reason_en": "Target did not appear", "remediation_owner": "Browser", "remediation_status": "open"}, + "trajectory": [{"index": 1, "arguments": {"action": "click", "selector": "#latest"}, "duration_ms": 1000, "status": "error", "result": "Click failed: timed out\nCall log: #latest"}], + "screenshot": None, + } + detail = _detail_page("zh", attempt, task, {"label": "Moli"}, evidence, peers) + assert 'id="step-1"' in detail + assert 'href="#step-1"' in detail + assert "Call log: #latest" in detail + assert "目标没有出现" in detail + assert 'href="../task/29.html"' in detail + assert 'href="../evidence-viewer.html?file=data/evidence/moli-29.json"' in detail def test_report_removes_macos_and_linux_local_paths() -> None: From 832b7c6f343b32d21ec6d9064d92beab11cff601 Mon Sep 17 00:00:00 2001 From: kevin Date: Tue, 15 Sep 2026 16:31:26 +0800 Subject: [PATCH 124/127] Clarify replay submission failures and scale browser comparison --- AGENTS.md | 3 +- harness/trajectory_replay/webarena_report.py | 105 +++++++++++++----- scripts/check_webarena_replay_report.py | 11 ++ scripts/check_webarena_replay_visual.py | 5 +- .../trajectory_replay/test_webarena_report.py | 41 +++++++ 5 files changed, 137 insertions(+), 28 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 42b855130..caf5f3b2b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,7 +15,8 @@ - Maintain `research/report-changes.json` for substantive releases and `research/provider-pricing.json` for public pricing. Release notes explain reader-visible or result-visible effects without narrating assistant workflow; prices retain verification dates, commitments, billing units and unknowns. - Use `Browser Use` for the autonomous track and `Browser Trajectory Replay` (Chinese: `轨迹重放`) for the fixed BrowserTool-action track. Their implementation roots are `harness/browser_use/` and `harness/trajectory_replay/`; do not add aliases or compatibility paths using the retired names. - Browser Trajectory Replay canonical inputs must not contain run-local snapshot refs. Compile each ref to a hash-bound stable selector while preserving the original ref action semantics; do not silently replace a DOM ref click/fill/select with BrowserTool's Playwright selector fallback because that adds different viewport, overlay and navigation-wait behavior. -- Browser Trajectory Replay reports keep the dataset separate from measured verdicts. Every task needs a contract-and-qualified-trajectory page; every result needs a same-task all-browser comparison, a reader-facing failure locator, the exact failed-action or evaluator error, reviewed cause, complete expandable tool responses, and on-demand public evidence. The result inventory stays compact, fixed-height, filterable and linked to a diagnosis dialog; generate participant columns from the registry and keep Chinese and English task/result routes aligned. +- Browser Trajectory Replay reports keep the dataset separate from measured verdicts. Every task needs a contract-and-qualified-trajectory page; every result needs a same-task all-browser comparison, a reader-facing failure locator, the exact failed-action or evaluator error, reviewed cause, complete expandable tool responses, and on-demand public evidence. The result inventory stays compact, fixed-height, filterable and linked to a diagnosis dialog. The same-task explorer uses a registry-driven baseline, multi-select comparison browsers and explicit outcome relations; group pass/fail links instead of adding one result column per future browser. Keep Chinese and English task/result routes aligned. +- A BrowserTool call that returns success does not prove its intended site mutation occurred. When the official evaluator rejects a completed trajectory for an absent required network event, locate the last plausible trigger action and publish the exact missing-event assertion, but do not mark that action as failed or change the official verdict. Repair asynchronous or position-dependent trajectory inputs only through new qualification evidence and a frozen full-scope comparison rerun. - Trajectory Replay release checks exercise Sites-style tokenized preview navigation from a nested result back to the report root; local relative-link checks alone do not prove hosted drilldown works. - Browser Use reports expose one manifest-authoritative logical result per browser and task. Dataset routes contain only frozen task definitions and acceptance contracts; result routes own verdicts, timings and trajectories. Keep one filterable result inventory instead of adding duplicate success/failure tables. - Keep automatic evaluator failure location separate from reviewed root cause. A root-cause subtype and reason must come from a per-browser, per-task review bound to the current attempts hash; render both fields blank while that result is unverified, and require complete verified coverage before a formal release when the current profile enables the gate. diff --git a/harness/trajectory_replay/webarena_report.py b/harness/trajectory_replay/webarena_report.py index 50cb35958..1efa36d0e 100644 --- a/harness/trajectory_replay/webarena_report.py +++ b/harness/trajectory_replay/webarena_report.py @@ -251,6 +251,37 @@ def _public_evidence( trace = _load_artifact((attempt.get("evidence") or {}).get("trace")) or [] evaluation = _load_artifact((attempt.get("evidence") or {}).get("evaluation")) failed = programmatic_failure(attempt) + if failed["stage"] == "official_evaluation" and evaluation: + missing_events = [ + result for result in evaluation.get("evaluators_results") or [] + if result.get("evaluator_name") == "NetworkEventEvaluator" + and any( + assertion.get("assertion_name") == "missing_navigation_event" + for assertion in result.get("assertions") or [] + ) + ] + if missing_events: + assertion_messages = [ + str(message) + for result in missing_events + for assertion in result.get("assertions") or [] + if assertion.get("assertion_name") == "missing_navigation_event" + for message in assertion.get("assertion_msgs") or [] + ] + trigger = next( + ( + step for step in reversed(trace) + if (step.get("arguments") or {}).get("action") in {"click", "press", "evaluate"} + ), + None, + ) + failed = { + "stage": "side_effect_not_observed", + "step": None, + "trigger_step": trigger.get("index") if trigger else None, + "message": "NetworkEventEvaluator: missing_navigation_event" + (" — " + " | ".join(assertion_messages) if assertion_messages else ""), + "expected_network": [result.get("expected") for result in missing_events], + } screenshot = _copy_failure_screenshot(attempt, output, key) public = { "result": { @@ -304,11 +335,13 @@ def _detail_page( report_root = "../" if zh else "../../" stage_labels = { "browser_tool_execution": ("浏览器动作执行", "Browser action execution"), + "side_effect_not_observed": ("提交请求未出现", "Submission request not observed"), "official_evaluation": ("官方验收", "Official evaluation"), "pass": ("无", "None"), } stage = stage_labels.get(failed["stage"], (failed["stage"], failed["stage"]))[0 if zh else 1] failed_index = failed.get("step") + trigger_index = failed.get("trigger_step") failed_action = next((str((step.get("arguments") or {}).get("action") or "") for step in evidence["trajectory"] if step.get("index") == failed_index), "") peer_cells = [] for peer in peers: @@ -345,9 +378,10 @@ def _detail_page( ) failure_panel = "" if attempt["status"] != "pass": - step_link = f'{"跳到失败动作" if zh else "Go to failed action"} #{failed_index}' if failed_index else f'{"查看已完成动作" if zh else "View completed actions"}' - action_locator = f'{failed_action} #{failed_index}' if failed_index else ("所有动作执行后" if zh else "After all actions") - failure_panel = f'''

    {"失败定位与处理" if zh else "Failure diagnosis and treatment"}

    {"失败环节" if zh else "Failure stage"}{_e(stage)}
    {"失败位置" if zh else "Failure location"}{_e(action_locator)}
    {"复核分类" if zh else "Reviewed category"}{_e(label)}

    {"原始报错" if zh else "Exact error"}

    {_e(failed["message"])}

    {"复核判断与依据" if zh else "Reviewed cause and basis"}

    {_e(reason)}

    {"处理方" if zh else "Owner"}: {_e(review.get("remediation_owner") if review else "—")} · {"状态" if zh else "Status"}: {_e(review.get("remediation_status") if review else "—")}

    {step_link}
    ''' + step_link = f'{"跳到失败动作" if zh else "Go to failed action"} #{failed_index}' if failed_index else f'{"查看触发动作" if zh else "Inspect trigger action"} #{trigger_index}' if trigger_index else f'{"查看已完成动作" if zh else "View completed actions"}' + action_locator = f'{failed_action} #{failed_index}' if failed_index else ((f'{("触发动作" if zh else "Trigger action")} #{trigger_index}; {("请求未出现" if zh else "request absent")}') if trigger_index else ("所有动作执行后" if zh else "After all actions")) + expected_network = f'

    {"未出现的请求" if zh else "Missing required request"}

    {_e(json.dumps(failed["expected_network"], ensure_ascii=False, indent=2))}
    ' if failed.get("expected_network") else "" + failure_panel = f'''

    {"失败定位与处理" if zh else "Failure diagnosis and treatment"}

    {"失败环节" if zh else "Failure stage"}{_e(stage)}
    {"失败位置" if zh else "Failure location"}{_e(action_locator)}
    {"复核分类" if zh else "Reviewed category"}{_e(label)}

    {"程序原始判定" if zh else "Exact programmatic finding"}

    {_e(failed["message"])}
    {expected_network}

    {"复核判断与依据" if zh else "Reviewed cause and basis"}

    {_e(reason)}

    {"处理方" if zh else "Owner"}: {_e(review.get("remediation_owner") if review else "—")} · {"状态" if zh else "Status"}: {_e(review.get("remediation_status") if review else "—")}

    {step_link}
    ''' facts = [ (("完成动作", "Actions completed"), f'{attempt["completed_action_count"]}/{attempt["action_count"]}'), (("端到端耗时", "End-to-end time"), _format_ms(attempt.get("duration_ms")) if zh else f'{attempt.get("duration_ms", 0) / 1000:.1f} s'), @@ -440,6 +474,10 @@ def _assets(output: Path) -> None: dialog{max-height:90vh;overflow:auto}.dialog-head{position:sticky;top:0;background:var(--panel);z-index:1}.dialog-body{padding:20px 24px}.dialog-body h3{margin:18px 0 8px}.dialog-body .evidence-actions{margin-bottom:4px} .viewer-tabs{display:flex;gap:8px;flex-wrap:wrap;margin:14px 0}.viewer-tabs button{padding:9px 13px;border:1px solid var(--line);background:var(--panel);color:var(--accent);border-radius:8px}.viewer-tabs button[aria-selected=true]{background:var(--accent);color:white}.viewer{height:auto;min-height:50vh}.viewer pre{min-height:45vh;max-height:65vh} @media(max-width:720px){.hero{padding:30px 0 24px}.top{height:60px}.top nav a{padding:15px 0}.detail-hero h1{font-size:34px}.step-data{grid-template-columns:minmax(0,1fr)}.step-heading>span:last-child{margin-left:0}.trace-step details,.step-target,.step-preview{margin-left:0}.filters label{flex:1 1 145px}.filters input,.filters select{min-width:0;width:100%}.diagnosis-panel{padding:16px}.fact-strip{grid-template-columns:repeat(2,minmax(0,1fr))}} +:root{--bg:#f6f8fb;--ink:#17212b;--muted:#5d6b78;--line:#d9e2ea;--accent:#1268d8;--good:#117a47;--bad:#c22e43;--wash:#e9f2ff;--panel2:#eef3f8;--shadow:0 8px 28px rgba(30,55,80,.08)}html[data-theme=dark]{--bg:#071015;--panel:#0d1a21;--ink:#edf6f7;--muted:#9aadb4;--line:#263c46;--accent:#49d6ed;--good:#67df9f;--bad:#ff6b7a;--wash:#10303a;--panel2:#122630;--shadow:none} +body{background:linear-gradient(180deg,var(--wash) 0,transparent 240px),var(--bg);font-size:15px}.top{height:58px;gap:17px;padding:0 max(20px,calc((100% - 1180px)/2));background:color-mix(in srgb,var(--panel) 92%,transparent);backdrop-filter:blur(14px)}.top nav{gap:17px}.top nav a,.language{font-size:14px}.theme{border-radius:999px;width:auto;height:auto;padding:6px 12px}main,footer{width:min(1180px,calc(100% - 40px))}section{padding:38px 0;border-top:1px solid var(--line)}.hero{padding:38px 0 30px;border:0}.hero h1{font-size:clamp(34px,5vw,58px)}h2{font-size:clamp(25px,3vw,36px);line-height:1.16}.table-scroll{border-radius:13px;box-shadow:var(--shadow)}th{background:var(--panel2);font-size:12px;white-space:nowrap}tbody tr:hover{background:var(--wash)} +.method-comparison{padding:24px;border:1px solid var(--line);border-radius:18px;background:var(--panel);box-shadow:var(--shadow)}.comparison-heading{display:flex;justify-content:space-between;align-items:start;gap:24px;margin-bottom:20px}.comparison-heading h2{margin-bottom:6px;font-size:25px}.comparison-heading p{margin:0;color:var(--muted)}.comparison-count{white-space:nowrap;padding:7px 11px;border-radius:999px;background:var(--wash);color:var(--accent);font-size:13px}.comparison-controls{display:grid;grid-template-columns:repeat(3,minmax(0,1fr));gap:14px}.comparison-controls label{display:grid;gap:5px;font-size:13px;font-weight:700}.comparison-controls select{width:100%;padding:10px 12px;border:1px solid var(--line);background:var(--bg);color:var(--ink);border-radius:9px;font:inherit}.comparison-methods{border:0;padding:0;margin:18px 0}.comparison-methods legend{font-weight:700}.method-checks,.outcome-list{display:flex;gap:7px;flex-wrap:wrap}.compare-chip{display:flex;align-items:center;gap:6px;padding:7px 11px;border:1px solid var(--line);border-radius:999px;background:var(--bg);cursor:pointer}.compare-chip.disabled{opacity:.45;cursor:not-allowed}.compare-chip input{accent-color:var(--accent)}.outcome-pill{display:inline-block;padding:5px 9px;border-radius:8px;font-size:13px;white-space:nowrap;text-decoration:none}.outcome-pill.pass{background:color-mix(in srgb,var(--good) 12%,var(--panel));color:var(--good)}.outcome-pill.fail{background:color-mix(in srgb,var(--bad) 12%,var(--panel));color:var(--bad)}.matrix-table table{min-width:760px}.matrix-table th:nth-child(n+3),.matrix-table td:nth-child(n+3){min-width:270px}.matrix-table td{vertical-align:middle;overflow-wrap:normal} +@media(max-width:760px){.top{padding:0 14px}.comparison-heading{display:block}.comparison-count{display:inline-block;margin-top:10px}.comparison-controls{grid-template-columns:1fr}.method-comparison{padding:16px}main,footer{width:calc(100% - 28px)}} ''' js = r'''const lang=document.documentElement.lang; const zh=lang==='zh'; @@ -456,21 +494,27 @@ def _assets(output: Path) -> None: const matrixRows=[...matrix.querySelectorAll('tbody tr')]; const relation=document.querySelector('[data-matrix-relation]'); const site=document.querySelector('[data-matrix-site]'); + const baseline=document.querySelector('[data-matrix-baseline]'); + const boxes=[...document.querySelectorAll('[data-matrix-method]')]; const count=document.querySelector('[data-matrix-count]'); const applyMatrix=()=>{ + boxes.forEach(box=>{box.disabled=box.value===baseline.value;box.closest('label')?.classList.toggle('disabled',box.disabled);if(box.disabled)box.checked=false}); + const others=boxes.filter(box=>box.checked&&!box.disabled).map(box=>box.value); + const selected=[baseline.value,...others]; let shown=0; matrixRows.forEach(row=>{ - const outcomes=JSON.parse(row.dataset.outcomes); - const passes=outcomes.filter(value=>value==='pass').length; - const matchRelation=!relation.value||(relation.value==='different'&&passes>0&&passes{link.hidden=!selected.includes(link.dataset.engine)}); + const matchRelation=others.length>0&&(!relation.value||(relation.value==='different'&&selected.some(engine=>pass.has(engine))&&selected.some(engine=>fail.has(engine)))||(relation.value==='baseline-pass-others-fail'&&pass.has(baseline.value)&&others.every(engine=>fail.has(engine)))||(relation.value==='baseline-fail-others-pass'&&fail.has(baseline.value)&&others.every(engine=>pass.has(engine)))||(relation.value==='all-pass'&&selected.every(engine=>pass.has(engine)))||(relation.value==='all-fail'&&selected.every(engine=>fail.has(engine)))||(relation.value==='exact-pass'&&selected.every(engine=>pass.has(engine))&&pass.size===selected.length)); const matchSite=!site.value||row.dataset.site===site.value; row.hidden=!(matchRelation&&matchSite); if(!row.hidden)shown++; }); - count.textContent=zh?`找到 ${shown} / ${matrixRows.length} 个任务`:`Showing ${shown} / ${matrixRows.length} tasks`; + count.textContent=others.length?(zh?`找到 ${shown} / ${matrixRows.length} 个任务`:`Showing ${shown} / ${matrixRows.length} tasks`):(zh?'请至少选择一个对比浏览器':'Select at least one comparison browser'); }; - relation.addEventListener('change',applyMatrix); - site.addEventListener('change',applyMatrix); + baseline.addEventListener('change',()=>{boxes.forEach(box=>box.checked=box.value!==baseline.value);applyMatrix()}); + [...boxes,relation,site].forEach(control=>control.addEventListener('change',applyMatrix)); applyMatrix(); } const inventory=document.querySelector('[data-results]'); @@ -507,7 +551,7 @@ def _assets(output: Path) -> None: if(!result)return; const detail='result/'+result.key+'.html'; dialog.querySelector('[data-dialog-title]').textContent=result.browser+' · '+(zh?'任务':'Task')+' #'+result.task_id; - dialog.querySelector('[data-dialog-stage]').textContent=result.failure_stage==='browser_tool_execution'?(zh?'浏览器动作':'Browser action'):result.failure_stage==='official_evaluation'?(zh?'官方验收':'Official evaluation'):(zh?'无':'None'); + dialog.querySelector('[data-dialog-stage]').textContent=result.failure_stage==='browser_tool_execution'?(zh?'浏览器动作':'Browser action'):result.failure_stage==='side_effect_not_observed'?(zh?'提交请求未出现':'Submission request absent'):result.failure_stage==='official_evaluation'?(zh?'官方验收':'Official evaluation'):(zh?'无':'None'); dialog.querySelector('[data-dialog-error]').textContent=zh?'正在读取完整证据…':'Loading complete evidence…'; delete dialog.dataset.loaded; dialog.querySelector('[data-dialog-link]').href=detail; @@ -516,11 +560,12 @@ def _assets(output: Path) -> None: try{ const evidence=await fetchJson(base+'data/evidence/'+result.key+'.json'); const step=evidence.programmatic_failure?.step; + const trigger=evidence.programmatic_failure?.trigger_step; const failedAction=(evidence.trajectory||[]).find(action=>action.index===step); - dialog.querySelector('[data-dialog-step]').textContent=step?(failedAction?.arguments?.action||'')+' #'+step:(zh?'动作完成后':'After actions completed'); - dialog.querySelector('[data-dialog-step-link]').href=detail+(step?'#step-'+step:'#trajectory'); + dialog.querySelector('[data-dialog-step]').textContent=step?(failedAction?.arguments?.action||'')+' #'+step:trigger?(zh?'触发动作':'Trigger action')+' #'+trigger:(zh?'动作完成后':'After actions completed'); + dialog.querySelector('[data-dialog-step-link]').href=detail+(step?'#step-'+step:trigger?'#step-'+trigger:'#trajectory'); dialog.querySelector('[data-dialog-category]').textContent=(zh?evidence.review?.label_zh:evidence.review?.label_en)||(zh?'未复核':'Not reviewed'); - const exact=step?(failedAction?.result||evidence.programmatic_failure?.message||''):evidence.official_evaluation?JSON.stringify(evidence.official_evaluation,null,2):(evidence.programmatic_failure?.message||''); + const exact=step?(failedAction?.result||evidence.programmatic_failure?.message||''):evidence.programmatic_failure?.stage==='side_effect_not_observed'?(evidence.programmatic_failure.message+'\n\nExpected network event:\n'+JSON.stringify(evidence.programmatic_failure.expected_network,null,2)):evidence.official_evaluation?JSON.stringify(evidence.official_evaluation,null,2):(evidence.programmatic_failure?.message||''); dialog.querySelector('[data-dialog-error]').textContent=exact; dialog.querySelector('[data-dialog-cause]').textContent=(zh?evidence.review?.reason_zh:evidence.review?.reason_en)||(zh?'尚无复核判断':'No reviewed cause'); dialog.querySelector('[data-dialog-remediation]').textContent=(zh?'处理方:':'Owner: ')+(evidence.review?.remediation_owner||'—')+' · '+(zh?'状态:':'Status: ')+(evidence.review?.remediation_status||'—'); @@ -767,7 +812,7 @@ def build_webarena_replay_report( ) stage_rows = [] for engine, method in methods.items(): - for code, label_pair in (("browser_tool_execution", ("浏览器动作", "Browser action")), ("official_evaluation", ("官方验收", "Official evaluation"))): + for code, label_pair in (("browser_tool_execution", ("浏览器动作", "Browser action")), ("side_effect_not_observed", ("提交请求未出现", "Submission request absent")), ("official_evaluation", ("官方验收", "Official evaluation"))): count = sum(row["engine"] == engine and row["status"] != "pass" and row["failure_stage"] == code for row in public_rows) if count: stage_rows.append(f'{_e(method["label"])}{label_pair[0 if zh else 1]}{count}{"下钻到失败结果" if zh else "Inspect failures"} →') @@ -775,7 +820,7 @@ def build_webarena_replay_report( summary_body = f'''

    {eyebrow}

    {"浏览器轨迹重放评测" if zh else "Browser Trajectory Replay"}

    {"90 条经过资格验证的固定操作轨迹依次在全部注册浏览器上执行。Agent 与模型不参与;每题重置站点,浏览器方法之间完全销毁环境。" if zh else "The same 90 qualified fixed action trajectories ran serially on every registered browser. No Agent or model participated. Sites were reset per task and torn down between browser methods."}

    {"核心指标" if zh else "Core metrics"}

    {f"通过率覆盖全部 {task_count} 题;端到端耗时、CPU 和内存只比较所有浏览器共同通过的 {shared_count} 题,避免提前失败缩短工作量。单次动作耗时含实际工具调用,调用次数不判优。" if zh else f"Pass rate covers all {task_count} tasks. End-to-end time, CPU, and memory use the {shared_count} tasks passed by every browser so early failure cannot shorten workload. Per-action latency covers actual tool calls; call counts are not ranked."}

    {_metrics_table(method_rows, lang)}

    {"资源与网络" if zh else "Resources and network"}

    {f"下表也只统计共同通过的 {shared_count} 题;每个数值按单题采样。网络传输量和 CPU 使用率不直接判优,低数值可能来自缓存或等待,而非浏览器更快。" if zh else f"These per-task samples also cover the {shared_count} shared passes. Network bytes and CPU utilization are not ranked: lower values can reflect caching or waiting, not a faster browser."}

    {"展开资源与网络统计" if zh else "Open resource and network statistics"}{_resource_table(method_rows, lang)}
    -

    {"失败发生在哪个环节" if zh else "Where failures occurred"}

    {"浏览器动作失败是轨迹没有完整执行;官方验收失败是动作执行结束但任务合同未通过。点击行末入口可看到失败动作、原始报错和复核根因。" if zh else "Browser-action failures stop the trajectory before completion; official-evaluation failures complete actions but fail the task contract. Follow a row to the action, exact error, and reviewed cause."}

    {stage_table}
    +

    {"失败发生在哪个环节" if zh else "Where failures occurred"}

    {"动作报错与提交请求未出现分别统计。后者表示工具调用返回成功,但验收要求的网络请求没有发生;不能据此改判通过。逐题详情给出触发动作、验收规则和复核原因。" if zh else "Action errors and absent submission requests are counted separately. In the latter case the tool call returned success, but the required network request did not occur; this does not justify changing the verdict to pass. Task detail shows the trigger action, evaluator rule, and reviewed cause."}

    {stage_table}

    {"浏览器动作耗时" if zh else "Browser action latency"}

    {"均值、P50、P95 按单次调用统计;调用次数只说明工作量。下钻到结果详情可查看具体动作耗时和工具返回。" if zh else "Mean, P50, and P95 are per-call statistics; call counts only describe workload. Open a task result for the individual action timing and full tool response."}

    {"展开各类动作耗时" if zh else "Open latency by action type"}{action_table}
    ''' (root / "index.html").write_text(_shell(lang, "index", "浏览器轨迹重放" if zh else "Browser Trajectory Replay", summary_body, current_route="index.html"), encoding="utf-8") @@ -792,28 +837,38 @@ def build_webarena_replay_report( for row in public_rows: cause = row["cause_zh"] if zh else row["cause_en"] status = ("通过" if row["status"] == "pass" else "失败") if zh else row["status"].title() - stage_label = {"browser_tool_execution": "浏览器动作" if zh else "Browser action", "official_evaluation": "官方验收" if zh else "Official evaluation", "pass": "—"}.get(row["failure_stage"], row["failure_stage"]) + stage_label = {"browser_tool_execution": "浏览器动作" if zh else "Browser action", "side_effect_not_observed": "提交请求未出现" if zh else "Submission request absent", "official_evaluation": "官方验收" if zh else "Official evaluation", "pass": "—"}.get(row["failure_stage"], row["failure_stage"]) result_rows.append(f'#{row["task_id"]}{_e(row["browser"])}{_e(row["sites"][0] if row["sites"] else "")}{status}{row["completed_actions"]}/{row["action_count"]}{_e(stage_label)}{_e(cause) if cause else "—"}{"证据页" if zh else "Evidence page"}') browser_options = "".join(f'' for key, item in methods.items()) site_options = "".join(f'' for site in sorted({s for row in public_rows for s in row["sites"]})) - stage_options = '' + stage_labels = {"browser_tool_execution": ("浏览器动作", "Browser action"), "side_effect_not_observed": ("提交请求未出现", "Submission request absent"), "official_evaluation": ("官方验收", "Official evaluation")} + present_stages = {row["failure_stage"] for row in public_rows if row["status"] == "fail"} + stage_options = "".join(f'' for code in stage_labels if code in present_stages) causes = {row["cause_zh"]: row["cause_zh"] if zh else row["cause_en"] for row in public_rows if row["cause_zh"]} cause_options = "".join(f'' for code, label in sorted(causes.items())) result_matrix_rows = [] for task_id, task in tasks.items(): - values = [] - outcomes = [] + pass_links = [] + fail_links = [] + passed_engines = [] + failed_engines = [] for engine in methods: row = next(item for item in rows_by_task[task_id] if item["engine"] == engine) - outcomes.append(row["status"]) label = ("通过" if row["status"] == "pass" else "失败") if zh else row["status"].title() - values.append(f'{label}') + link = f'{_e(row["browser"])} · {label}' + if row["status"] == "pass": + pass_links.append(link) + passed_engines.append(engine) + else: + fail_links.append(link) + failed_engines.append(engine) site = (task["task"].get("sites") or [""])[0] - result_matrix_rows.append(f'#{task_id}{_e(", ".join(task["task"].get("sites") or []))}{"".join(f"{v}" for v in values)}') - matrix_headers = "".join(f'{_e(item["label"])}' for item in methods.values()) + result_matrix_rows.append(f'#{task_id}{_e(", ".join(task["task"].get("sites") or []))}
    {"".join(pass_links) or "—"}
    {"".join(fail_links) or "—"}
    ') matrix_sites = "".join(f'' for site in sorted({(task["task"].get("sites") or [""])[0] for task in tasks.values()})) - result_matrix = f'
    {"同题全部浏览器结果" if zh else "Same-task outcomes across all browsers"}
    {matrix_headers}{"".join(result_matrix_rows)}
    {"任务" if zh else "Task"}{"站点" if zh else "Site"}
    ' - results_body = f'''

    {len(public_rows)} {"条逻辑结果" if zh else "logical outcomes"}

    {"结果与失败诊断" if zh else "Results and failure diagnosis"}

    {"逐题矩阵比较全部浏览器;筛选表默认显示失败,点击“查看诊断”读取完整报错、复核判断和执行证据。" if zh else "The same-task matrix compares every browser. The inventory starts with failures; open a diagnosis for the full error, reviewed cause, and execution evidence."}

    {result_matrix}

    {''.join(result_rows)}
    {"任务" if zh else "Task"}{"浏览器" if zh else "Browser"}{"站点" if zh else "Site"}{"判定" if zh else "Verdict"}{"动作" if zh else "Actions"}{"失败环节" if zh else "Failure stage"}{"复核分类" if zh else "Reviewed category"}{"详情" if zh else "Detail"}
    {"失败环节" if zh else "Failure stage"}
    {"失败动作" if zh else "Failed action"}
    {"复核分类" if zh else "Reviewed category"}

    {"原始报错与官方验收" if zh else "Exact error and official evaluation"}

    {"复核原因与处理" if zh else "Reviewed cause and treatment"}

    ''' + matrix_baselines = "".join(f'' for engine, item in methods.items()) + matrix_checks = "".join(f'' for engine, item in methods.items()) + result_matrix = f'

    {"同题浏览器对比" if zh else "Same-task browser comparison"}

    {"按通过与失败分组展示所有参评浏览器;点击浏览器名称查看完整动作和验收证据。" if zh else "All registered browsers are grouped by pass and fail. Open a browser outcome for its actions and evaluator evidence."}

    {"参与比较的浏览器(至少选一个)" if zh else "Comparison browsers (select at least one)"}
    {matrix_checks}
    {"".join(result_matrix_rows)}
    {"任务" if zh else "Task"}{"站点" if zh else "Site"}{"通过的浏览器" if zh else "Passing browsers"}{"失败的浏览器" if zh else "Failing browsers"}
    ' + results_body = f'''

    {len(public_rows)} {"条逻辑结果" if zh else "logical outcomes"}

    {"结果与失败诊断" if zh else "Results and failure diagnosis"}

    {"同题对比支持选择基准和多个浏览器;下方筛选表默认只显示失败,点击“诊断”查看原始判定、复核原因与执行证据。" if zh else "Choose a baseline and multiple browsers for same-task comparison. The inventory starts with failures; open Diagnose for the exact finding, reviewed cause, and execution evidence."}

    {result_matrix}

    {''.join(result_rows)}
    {"任务" if zh else "Task"}{"浏览器" if zh else "Browser"}{"站点" if zh else "Site"}{"判定" if zh else "Verdict"}{"动作" if zh else "Actions"}{"失败环节" if zh else "Failure stage"}{"复核分类" if zh else "Reviewed category"}{"详情" if zh else "Detail"}
    {"失败环节" if zh else "Failure stage"}
    {"动作定位" if zh else "Action locator"}
    {"复核分类" if zh else "Reviewed category"}

    {"程序判定与验收规则" if zh else "Programmatic finding and evaluator rule"}

    {"复核原因与处理" if zh else "Reviewed cause and treatment"}

    ''' (root / "results.html").write_text(_shell(lang, "results", "结果与诊断" if zh else "Results", results_body, current_route="results.html"), encoding="utf-8") flow_labels = ( diff --git a/scripts/check_webarena_replay_report.py b/scripts/check_webarena_replay_report.py index d22d7fa13..24d40286c 100644 --- a/scripts/check_webarena_replay_report.py +++ b/scripts/check_webarena_replay_report.py @@ -202,6 +202,17 @@ def check(report_dir: Path) -> dict[str, int]: results_html = (root / "results.html").read_text(encoding="utf-8") if '