feat(evals): port HardBenchmark suite onto the harness wave - #2866
feat(evals): port HardBenchmark suite onto the harness wave#2866miguelg719 wants to merge 38 commits into
Conversation
…pters Phase 1 of the mastra harness: a structural runMastraSession layer over @mastra/core Agent.stream + @mastra/mcp MCPClient, an evals tool adapter that mounts stagehand_facade / playwright_mcp / chrome_devtools_mcp over stdio MCP and hosts the code surfaces as an in-process createTool backed by the existing code bridge, a fullStream -> Trajectory adapter, and a runner mirroring codexRunner. Registry/planner wiring lands in Phase 2.
…-runner skeleton Wire the Phase 1 Mastra adapters into the bench harness registry via defineExternalHarness and move mastraRunner onto runExternalHarnessTask so it shares the prompt/parse/grade lifecycle and normalized harness_* metrics with codex and claude_code. - benchHarness: add mastraHarness (stagehand_facade default surface, openai/gpt-5.4-mini default model, EVAL_MASTRA_MODELS override via the generic planner key) and register it last in harnessRegistry. - mastraToolAdapter: export MASTRA_TOOL_SURFACES and resolve surface/profile with the shared toolSurfaceResolution helpers; drop the bespoke resolvers. - mastraRunner: delegate prompt building, result parsing, status/stop-reason normalization, evidence capture and verifier grading to the skeleton; emit only harness_* metrics (no mastra_* names). Tool instructions now ride in the prompt instead of the Mastra system prompt to avoid duplication. - tests: registry/planner/runner/adapter coverage for the new harness, and update the registry-derived guidance regexes that hardcoded the old "claude_code or codex" list.
- Use a per-session MCPClient id so concurrent bench rows no longer disconnect each other's live MCP connection (@mastra/mcp caches clients by id). - Sanitize Mastra event summaries, transcripts, logs, and trajectory tool results/errors; drop the unused full process.env copy from the prepared tool adapter. - Do not score success JSON emitted before an SDK error as a pass. - Make MCP discovery abortable and bound MCP disconnect and runtime cleanup with timeouts so cancellation cannot hang or leak the temp workspace. - Add a real @mastra/mcp MCPClient test covering the unique-id contract.
…up errors, deep-sanitized tool args/results, fixed discovery-failure message
Token-level reasoning/text deltas are fragments of one block; joining them with \n garbled every step's reasoning fed to the verifier. Fragments now concatenate directly, with a newline only at the reasoning->text block boundary, matching how mastra-sdk accumulates its own textBuffer.
Adds @browserbasehq/stagehand-integrations-pi-sdk (in-process pi-coding-agent session layer: hermetic ResourceLoader, custom-tool allowlist, turn budget, abort forwarding, token/cost extraction, stdio MCP -> pi tool bridge) and wires a "pi" bench harness with defineExternalHarness + runExternalHarnessTask. - piToolAdapter: stagehand_facade (default), playwright_mcp, chrome_devtools_mcp, stagehand_code, playwright_code, cdp_code; mcp mounts bridged in-process, handles mounts hosted as the harness run tool inside pi - piAdapter: pi AgentSession events -> verifier Trajectory - piRunner: marker result contract, harness_* metrics via the skeleton, pi_turns - registry: "pi" registered after codex; EVAL_PI_MODELS derives automatically - tests: pi-sdk session/mcp, adapter, tool adapter, runner, registry; existing hardcoded harness-list assertions made registry-derived
…ering, stop reasons, and run-tool aborts - pi-sdk session: sanitize every event summary (message + detail) so logPiEvent/buildPiTranscript never leak tool args/results or provider errorMessage fields; treat pi stopReason 'aborted' (without an external signal) and 'length' as sdk_error instead of completed - pi-sdk mcp: line-buffer MCP stderr via attachPiMcpStderrLogger, sanitize complete lines (CRLF-aware), flush the trailing partial line once on end/close - evals piToolAdapter: honor the AbortSignal pi hands the run tool (reject before start when already aborted, race abort alongside the timeout), and attach a logging catch to abandoned snippets so late rejections never surface as unhandled
…MCP tool bridge - packages/integrations/eve-sdk: session layer around `eve dev --no-ui` + eve/client (startEveDevServer, runEveSession, usage/cost extraction, redacted logging) with hermetic tests - eveToolAdapter: mounts MCP surfaces (stagehand_facade, playwright_mcp, chrome_devtools_mcp) by generating an eve app whose authored tools proxy to the MCP stdio server via @modelcontextprotocol/sdk; server specs travel via STAGEHAND_EVE_MCP_SERVERS, built-in eve tools disabled - eveRunner on runExternalHarnessTask (eve_* + normalized harness_* metrics), eveAdapter trajectory mapping with image evidence - register "eve" via defineExternalHarness; EVAL_EVE_MODELS derives from the registry; update harness-list assertions; turbo/ci/vitest plumbing
…-eve review - eveToolAdapter: bound listMcpServerTools connect/listTools/close with EVAL_EVE_MCP_LIST_TOOLS_TIMEOUT_MS (default 60s), close a late-arriving client after a connect timeout, and accept an injectable connect for tests - eveToolAdapter: bound runtime.cleanup() in the normal cleanup path with EVAL_AGENT_MOUNT_CLEANUP_TIMEOUT_MS so the app dir is always removed - eveToolAdapter: writeEveAgentApp removes its mkdtemp dir when a later write or the node_modules symlink fails - eve-sdk session: caller abort now also cancels the Eve session exactly once (shared with the budget/input.requested paths); appRoot server variant accepts an injectable spawn - eve-sdk session: logEveEvent passes message and detail through sanitizeErrorMessage before persisting - tests: new eveToolAdapterPrepare.test.ts covering prepareEveToolAdapter with a faked startAgentToolRuntime (mount rejection, env payload, matcher, observations, idempotent/resilient/timed-out cleanup, catch-path cleanup) and listMcpServerTools timeouts; mkdtemp rollback test; mid-stream abort, dev-server kill, and redaction tests for the session layer
…cted transcript/fallbacks, late MCP client close, timer cleanup, lazy eve-sdk load, ai peer range
…-renamed-imports)
…nner Phase 1 of the deepagents harness (no registry/planner wiring yet): - packages/integrations/deepagents/runner: uv project + run_eval.py that drives LangChain Deep Agents over persistent stdio MCP sessions and emits JSONL events (assistant, tool_call, tool_result, final, usage, error). - packages/integrations/deepagents-sdk: runDeepagentsSession spawns the runner via `uv run --project <dir> --locked python run_eval.py`, forwards AbortSignal as SIGTERM, and normalizes status/stopReason/tokenUsage. - packages/evals/framework: deepagentsToolAdapter (MCP mounts only), harnesses/deepagentsAdapter (events -> NormalizedToolCall), and deepagentsRunner mirroring codexRunner (prompt/parse duplicated on purpose until the shared externalRunner skeleton lands). - turbo/ci/vitest/evals package wiring for the new workspace package.
…arness core - define deepagentsHarness via defineExternalHarness and register it after codex - switch deepagentsRunner onto runExternalHarnessTask (marker result contract, normalized harness_* metrics, harnessStatus/harnessStopReason) - resolve --tool/--startup through the shared toolSurfaceResolution helpers (DEEPAGENTS_TOOL_SURFACES: stagehand_facade, playwright_mcp, chrome_devtools_mcp) - move the file/todo-tool guidance into DEEPAGENTS_SYSTEM_PROMPT - tests: registry/parse coverage for deepagents; three-harness flag guidance
…group cleanup, redaction, runner tool profiles, CI checks
…lip, redacted error events/iterationError, full redaction policy in Python runner
…down - run_eval.py closes its AsyncExitStack in a suppressed finally instead of inside the try whose handler returns exit 1: a raising stdio-session close after the stream finished no longer converts a correct run into sdk_error, and no longer emits an error event that overwrote the stop classification. - Node session: terminal error kinds (tool_step_budget/recursion_limit) are no longer overwritten by later generic exception events.
…nd runner (phase 1) - New workspace package @browserbasehq/stagehand-integrations-fx-sdk: runFxSession spawns `fx ask --json` with an isolated HOME, tails the session events.jsonl for per-tool-call evidence, and normalizes status/stopReason/tokenUsage. - evals: fxToolAdapter (MCP-only mounts -> ~/.fx/mcp.json + settings.json + workspace .fx.json/AGENTS.md), fxRunner (mirrors codexRunner), harnesses/fxAdapter (tool_step events -> NormalizedToolCall), unit tests. - turbo/ci/vitest wiring for the new package. Registry/planner wiring lands in phase 2.
Define fxHarness with defineExternalHarness and add it to the bench harness registry so --harness fx plans, dry-runs, and executes like claude_code and codex. fxRunner now runs through runExternalHarnessTask (shared prompt, result parsing, normalized harness_* metrics and harnessStatus), and fxToolAdapter resolves surfaces/startup profiles through the shared registry helpers via FX_TOOL_SURFACES. EVAL_FX_MODELS overrides the default model list. Registry-derived guidance tests now include fx.
…ssions, and status - fx-sdk session: emit tool-step observations only from the live events.jsonl tail (no post-exit replay against the final browser state) and return the observed call keys so the trajectory adapter pairs evidence by key - fx-sdk session: rewrite resolveFxStatus precedence so empty stdout, ask.exit_code != 0, and failed/cancelled turn kinds are sdk_error even on OS exit 0; sanitize event summaries/transcripts; signal the fx process group on abort - fxToolAdapter: deny every non-MCP fx built-in (web_search/web_fetch/file tools included), pre-allow every discovered mcp_<server>_<tool> so --auto never adjudicates, pass the runner's HOME plus pnpm/XDG/proxy cache vars to MCP children, set startup_timeout_ms (EVAL_FX_MCP_STARTUP_TIMEOUT_MS), and log instead of swallowing cleanup timeouts while always removing the temp root
…s-group cleanup, sanitized cleanup logs
…s rejection during polls - resolveFxStatus: the bare observed-step-count heuristic is now a fallback for silent stops only. A run that succeeded outright (process exit 0, ask exit 0, no error) on exactly its last allowed step is completed, not a fabricated max_turns with a synthesized iteration error. Explicit step-limit signals (notice text, terminal_reason, error) classify as before. - runFxSession pre-attaches a no-op catch to the process promise so a spawn failure during the poll loop's sleep no longer fires unhandledRejection and kills the evals worker; the error still surfaces at the awaited join.
Adds the Cursor `agent` CLI as an external harness, built without touching the shared registry/planner files (wired in phase 2 after harness/wave-core): - packages/integrations/cursor-sdk: runCursorAgentSession spawns `agent -p --output-format stream-json --force --trust --approve-mcps`, parses NDJSON events, enforces a completed-tool-step budget (the CLI has no max-turns flag), sanitizes stop reasons, and reports zero token usage (Cursor emits none in any output format). Process runner is injectable. - evals cursorToolAdapter: MCP-only mounts (stagehand_facade, playwright_mcp, chrome_devtools_mcp) via a temp workspace `.cursor/mcp.json`; handles/cli mounts are rejected. Observation recorder wired through onToolResult. - evals cursorRunner + harnesses/cursorAdapter: TaskResult/metrics and Trajectory mapping (tool_call started/completed pairs, image evidence, ordinal observation pairing). - packages/integrations/cursor: mcp.json template, AGENTS.md, README. - turbo/ci/vitest/evals package wiring for the new workspace package.
…sor parser - cursorAdapter marks 'started' calls that never receive a 'completed' envelope (abort/SIGTERM mid-call) as ok:false 'no tool result' instead of a successful-looking step with an empty result, matching deepagents. - runExternalHarnessTask accepts an optional parseResult hook; the cursor runner passes parseCursorResult so the fenced/prose-JSON leniency its tests assert actually runs in production.
46-task residual hard core of WebTailBench + Online-Mind2Web with audited provenance (source_suite/failure_mode/capability_axis) and verbatim precomputed rubrics. Registered as agent/hardbenchmark across discovery, planner, external-harness plan, TUI shorthand (b:hardbenchmark) and config.
|
There was a problem hiding this comment.
4 issues found across 10 files
Confidence score: 3/5
packages/evals/suites/hardbenchmark.tslabels all rows asagent/hardbenchmark, dropping OnlineMind2Web site-scoping metadata; external harnesses may apply generic guidance and run those tasks incorrectly. Preserve the dataset-specific contract in the generated rows.packages/evals/evals.config.jsonexposesbenchmarks.hardbenchmark.limit, but the runner does not consume it, so configured limits have no effect. Wire the value into suite limit resolution or remove the setting.packages/evals/framework/benchPlanner.tshas no focused coverage for the newagent/hardbenchmarkroute, dataset routing, or generated testcase shape, leaving regressions able to disable the suite silently. Add planner tests for this route.packages/evals/framework/externalHarnessPlan.tsomitsagent/hardbenchmarkfrom the unsupported-task fallback, producing incomplete diagnostics when that suite is requested. Update the fallback message.
Not reviewed (too large): packages/evals/datasets/hardbenchmark/HardBenchmark_data.jsonl (~46 lines) - if these are generated or fixture files, add them to ignored paths to exclude them from future reviews.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/evals/evals.config.json">
<violation number="1" location="packages/evals/evals.config.json:22">
P2: `benchmarks.hardbenchmark.limit` is not consumed by the runner, so this setting cannot control the suite and creates a false configuration surface. Wire the config value into the suite's limit resolution, or remove this entry until benchmark limits are read from config.</violation>
</file>
<file name="packages/evals/framework/benchPlanner.ts">
<violation number="1" location="packages/evals/framework/benchPlanner.ts:303">
P2: Add a focused planner test for `agent/hardbenchmark`; this new route currently has no coverage for dataset routing or generated testcase shape, so regressions can silently disable the suite.
(Based on your team's feedback about unit tests for new behavior.)</violation>
</file>
<file name="packages/evals/framework/externalHarnessPlan.ts">
<violation number="1" location="packages/evals/framework/externalHarnessPlan.ts:51">
P3: When an unsupported external task is requested, the error now omits the newly supported `agent/hardbenchmark` suite. Add `agent/hardbenchmark` to the fallback message so the diagnostic matches this branch.</violation>
</file>
<file name="packages/evals/suites/hardbenchmark.ts">
<violation number="1" location="packages/evals/suites/hardbenchmark.ts:116">
P2: OnlineMind2Web tasks lose their site-scoping contract when run through this suite. Because every row is labeled `agent/hardbenchmark`, external harnesses use generic guidance and cannot distinguish the 13 OnlineMind2Web rows; preserve the source suite in the plan/input or add source-aware guidance, and retain the source URL when it is available.</violation>
</file>
Architecture diagram
sequenceDiagram
participant TUI as TUI/CLI
participant Discovery as Discovery
participant Planner as Bench Planner
participant Builder as HardBenchmark Builder
participant Dataset as HardBenchmark_data.jsonl
participant Config as Config/TaskConfig
participant Harness as External Harness Plan
participant Eval as Eval Runner
Note over TUI,Eval: HardBenchmark Suite Registration Flow
TUI->>Discovery: b:hardbenchmark shorthand
Discovery->>Discovery: Register agent/hardbenchmark as external_agent_benchmark
Discovery-->>TUI: Suite discovered
TUI->>Planner: Run agent/hardbenchmark
Planner->>Planner: Generate suite testcases
Planner->>Builder: buildHardBenchmarkTestcases(models)
Builder->>Dataset: Read HardBenchmark_data.jsonl
Dataset-->>Builder: 46 rows with precomputed_rubric + provenance
alt EVAL_HARDBENCHMARK_IDS set
Builder->>Builder: Select tasks by explicit IDs
else EVAL_HARDBENCHMARK_MODE set
Builder->>Dataset: Filter by audited failure_mode
Builder->>Builder: Apply sampling/limit
else Default (all 46)
Builder->>Builder: Use all tasks (EVAL_MAX_K or EVAL_HARDBENCHMARK_LIMIT)
end
Builder->>Config: Look up task categories
Config-->>Builder: categories
Builder->>Builder: Build Testcase with provenance metadata (source_suite, failure_mode, capability_axis)
Builder-->>Planner: 46 testcases
Planner->>Harness: Build external harness plan for each task
Harness->>Harness: Map agent/hardbenchmark -> dataset: "hardbenchmark"
Harness->>Eval: Execute with startUrl, instruction, precomputed_rubric
alt Task runs successfully
Eval-->>Harness: Result with rubric match
Harness-->>Planner: Testcase outcome
else Task fails (expected at ~0% pass rate)
Eval-->>Harness: Failure with trajectory
Harness-->>Planner: Failure attributed to capability_axis
end
Planner-->>TUI: Aggregate results with provenance-aware reporting
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| "limit": 25 | ||
| }, | ||
| "hardbenchmark": { | ||
| "limit": 46 |
There was a problem hiding this comment.
P2: benchmarks.hardbenchmark.limit is not consumed by the runner, so this setting cannot control the suite and creates a false configuration surface. Wire the config value into the suite's limit resolution, or remove this entry until benchmark limits are read from config.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/evals/evals.config.json, line 22:
<comment>`benchmarks.hardbenchmark.limit` is not consumed by the runner, so this setting cannot control the suite and creates a false configuration surface. Wire the config value into the suite's limit resolution, or remove this entry until benchmark limits are read from config.</comment>
<file context>
@@ -18,6 +18,9 @@
"limit": 25
},
+ "hardbenchmark": {
+ "limit": 46
+ },
"odysseysbench": {
</file context>
| "agent/webvoyager": (models) => buildWebVoyagerTestcases(models), | ||
| "agent/onlineMind2Web": (models) => buildOnlineMind2WebTestcases(models), | ||
| "agent/webtailbench": (models) => buildWebTailBenchTestcases(models), | ||
| "agent/hardbenchmark": (models) => buildHardBenchmarkTestcases(models), |
There was a problem hiding this comment.
P2: Add a focused planner test for agent/hardbenchmark; this new route currently has no coverage for dataset routing or generated testcase shape, so regressions can silently disable the suite.
(Based on your team's feedback about unit tests for new behavior.)
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/evals/framework/benchPlanner.ts, line 303:
<comment>Add a focused planner test for `agent/hardbenchmark`; this new route currently has no coverage for dataset routing or generated testcase shape, so regressions can silently disable the suite.
(Based on your team's feedback about unit tests for new behavior.) </comment>
<file context>
@@ -299,6 +300,7 @@ export function generateSuiteTestcases(
"agent/webvoyager": (models) => buildWebVoyagerTestcases(models),
"agent/onlineMind2Web": (models) => buildOnlineMind2WebTestcases(models),
"agent/webtailbench": (models) => buildWebTailBenchTestcases(models),
+ "agent/hardbenchmark": (models) => buildHardBenchmarkTestcases(models),
"agent/odysseysbench": (models) => buildOdysseysBenchTestcases(models),
};
</file context>
| for (const modelEntry of normalizeAgentModelEntries(models)) { | ||
| for (const row of rows) { | ||
| const input: EvalInput = { | ||
| name: "agent/hardbenchmark", |
There was a problem hiding this comment.
P2: OnlineMind2Web tasks lose their site-scoping contract when run through this suite. Because every row is labeled agent/hardbenchmark, external harnesses use generic guidance and cannot distinguish the 13 OnlineMind2Web rows; preserve the source suite in the plan/input or add source-aware guidance, and retain the source URL when it is available.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/evals/suites/hardbenchmark.ts, line 116:
<comment>OnlineMind2Web tasks lose their site-scoping contract when run through this suite. Because every row is labeled `agent/hardbenchmark`, external harnesses use generic guidance and cannot distinguish the 13 OnlineMind2Web rows; preserve the source suite in the plan/input or add source-aware guidance, and retain the source URL when it is available.</comment>
<file context>
@@ -0,0 +1,154 @@
+ for (const modelEntry of normalizeAgentModelEntries(models)) {
+ for (const row of rows) {
+ const input: EvalInput = {
+ name: "agent/hardbenchmark",
+ modelName: modelEntry.modelName as AvailableModel,
+ agentMode: modelEntry.mode,
</file context>
| } | ||
|
|
||
| if (input.name === "agent/webtailbench") { | ||
| if (input.name === "agent/webtailbench" || input.name === "agent/hardbenchmark") { |
There was a problem hiding this comment.
P3: When an unsupported external task is requested, the error now omits the newly supported agent/hardbenchmark suite. Add agent/hardbenchmark to the fallback message so the diagnostic matches this branch.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/evals/framework/externalHarnessPlan.ts, line 51:
<comment>When an unsupported external task is requested, the error now omits the newly supported `agent/hardbenchmark` suite. Add `agent/hardbenchmark` to the fallback message so the diagnostic matches this branch.</comment>
<file context>
@@ -48,13 +48,13 @@ export function buildExternalHarnessTaskPlan(input: EvalInput): ExternalHarnessT
}
- if (input.name === "agent/webtailbench") {
+ if (input.name === "agent/webtailbench" || input.name === "agent/hardbenchmark") {
const instruction = readString(params, "ques");
if (!instruction) {
</file context>
abb9e3c to
c20d28b
Compare
|
This work is consolidated in #2891, which adds HardBench and rubric v1.2 on the current evaluator. The replacement retains 19 active tasks from this PR; 27 previously retired or quarantined tasks are intentionally omitted. Its full corpus contains 122 tasks across core, extended and holdout, without the corpus manifest layer. |
Stacked on #2812 (top of the harness wave). Part of the harness×model campaign.
What
Ports the 46-task HardBenchmark suite — the residual hard core of WebTailBench + Online-Mind2Web (tasks both frontier models failed, audited to genuine model faults, date-rotted tasks dropped) — onto the wave head.
datasets/hardbenchmark/HardBenchmark_data.jsonl(verbatimprecomputed_rubricper row + audited provenance:source_suite,failure_mode,capability_axis)suites/hardbenchmark.tsbuilder; registered asagent/hardbenchmarkin discovery, planner, external-harness plan, TUI (b:hardbenchmark), configNotes
feat/facade-batch-surface.Summary by cubic
Adds the 46-task HardBenchmark residual suite to the eval harness to focus model evaluation on audited failures from WebTailBench and Online-Mind2Web. Unlike the parent suites, it runs all 46 cases by default and preserves each task’s precomputed rubric.
Included
agent/hardbenchmarkacross discovery, planning, external harnesses, config, and theb:hardbenchmarkTUI shorthand.Written for commit 2c93dcf. Summary will update on new commits.