Repository navigation
Add bench: A/B harness for pi agents with and without xwebmcp tools - #112
Merged
Merged
Conversation
yummybomb
force-pushed
the
hypeship/ab-harness
branch
from
October 8, 2026 15:36
716302f to
8e51b1b
Compare
yummybomb
force-pushed
the
hypeship/ab-harness
branch
from
October 8, 2026 20:33
e738025 to
01262f4
Compare
bench/ measures, per user intent on a site, how much more often and how much faster the same pi agent succeeds when the domain's read-only xwebmcp tools are registered in its Kernel browser. It adds the runner (SHA-pinned registration, smoke gate, adoption trials, paired AB/BA trials, resume), the pi adapter, grading by answer shape and intent rules, the report with screening labels and rollups, and pilot and screening intents. The bench unit tests run in validate.yml.
yummybomb
force-pushed
the
hypeship/ab-harness
branch
from
October 9, 2026 06:09
6956c97 to
a6ed739
Compare
yummybomb
marked this pull request as ready for review
October 9, 2026 08:27
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds
bench/, a harness that measures, per user intent on a site, how much more often and how much faster the same agent succeeds when the domain's read-only xwebmcp tools are registered in its Kernel browser. This is the pilot phase: the runner, the smoke gate, SHA-pinned registration, the agent adapter, grading by shape and rules, the report with labels and rollups, 9 pilot intents across ipwho.is (#69), fandango.com (#58) and github.com (#55), and 17 screening intents for the domains on main. doordash.com has no read-only tools and every purelymail.com tool needs a signed-in mailbox, so neither has an intent. instacart.com's catalog tools need a signed-in session, so its intent runs on a Kernel profile (see Signed-in trials).Each trial runs pi (
pi -p --mode json) against one Kernel browser. It works with any provider pi supports, for example--model openai/gpt-5.4withOPENAI_API_KEY, oranthropic/...withANTHROPIC_API_KEY. Both arms get the same model, prompt, kernel MCP server, tool allowlist and browser settings. Treatment additionally registers the domain's read-only tools before the clock starts. Both arms' system prompt tells the agent to list the page's WebMCP tools first and fall back to Playwright or computer actions when none fits, as Kernel's docs tell customers to instruct their agents; it never mentions xwebmcp or whether tools are registered.run.mjs: resolves each intent's tool refs to commits in a bare cache repo, so the working clone is never touched. It composes the batch inxwebmcp attach's format, keeping the tools withreadOnlyHint: trueminus per-domain excludes and, once the smoke gate passes, freezes everything inrun.json, including the pi version and each pair's order, split evenly between AB and BA within each intent and shuffled.entry_toolis not live on the start tab. A pair with an aborted trial runs again whole, both trials in order, so its trials stay consecutive.rows.jsonl. Rerunning the same--run-idresumes, rerunning any pair that lacks a scored trial in either arm. One runner per run id.hypeship servicesservice under a hold. The hold is sized per stage and checked again before each job.lib/agent.mjsruns pi isolated:--no-session --no-context-files --no-skills --no-prompt-templates --no-approve --no-extensions -e builtin:mcp --no-builtin-tools, the bench system prompt, and--toolsset toallowlist.txt.PI_CODING_AGENT_DIRholding only the MCP config frozen inrun.json(the kernel server,exposure: direct), so the user's MCP servers, settings and stored logins never apply.turn_start) and the spend cap (from the cost pi reports). pi has no flags for the last two.Smoke gate: checks
KERNEL_API_KEY, the pi version on resume, that a zero-tool pi run answers and reports a cost above zero (which proves the model and its key, and that the spend cap can work; pi prices a model it has no prices for at 0), that the tools pi declares to the model exactly equalallowlist.txt, one treatment setup end to end on the first pending intent, and that the hold covers the remaining jobs.grade.mjs: extracts an answer leniently (fenced block, then trailing object, thenkey: valueprose). Nothing extractable ismalformed; a schema failure or an intent rule failure iswrong. A successful call outside the allowlist, a final message pi reports as an error, or a cost pi leaves out or reports as something other than a number iscrashed, except that a provider error pi retried and gave up on, or an error in the runner itself, isabortedand the pair runs again; hitting the time, turn or spend cap is atimeout. An agent that started without exactly the allowlisted tools isabortedand retried, because pi waits only 10s for MCP servers.report.mjs: per intent, the registered tools and drafts; per arm, over pairs where both trials were scored, n and correct with a Wilson interval, outcome counts, speed over correct trials,median_all, minutes per correct answer, and treatment tool use. A Δ row gives a Newcombe interval, and over both-correct pairs the speedup (ratio of medians) and an exact sign test, plus a screening label. Cheapest-fare intents carry anagreementcheck: when both arms are correct but their fares differ by more than $1, the higher one missed the cheapest fare, and a faster arm that did so in 2 more pairs than the other ismixed. Intents with one right answer that changes too often to pin (recent bug issues with linked PRs, AMC and Fandango showtimes) carryagreement: {same: true}: both-correct pairs whose answers differ as sets are counted, and 2 of them make a faster or slower labelmixed. A pair with an aborted or missing trial is left out of both arms and counted, with its aborts per arm, and so are correct answers given without a single tool call. It also prints per-PR rollups (worst label) and per-run rollups: spend over every attempt, an order check (control-first vs treatment-first pairs), label histogram, median speedup, the pooled gap-intent placebo check, a start delay check (pairs whose agents launched more than 1s apart after page load), cap calibration and the expected number of noise labels.Tool failures: each WebMCP invocation records the tool's own
statusand an error message, so a tool that returned{"status": "error"}no longer counts towardinvoke ok, and a call the MCP server rejected, such as one with a staletool_ref, is anerrorinstead ofunknown. The report adds a table per tool: calls, failures and the most common errors, and says how many invocations made through Playwright or the REPL have no recorded result.node --test "bench/**/*.test.mjs"is added tovalidate.yml. These tests are unit-only; trials never run in CI.Choices worth a look
Cart policy: agents may not check out, pay, place an order or complete a booking, and may not add to a cart unless the task allows it; a domain whose intents need a cart opts in through its
instructions, and no current intent does. Carts on live sites can hold real inventory: an AMC cart holds seats against other buyers, and AMC rate-limits the ticket flow after a few carts, soamctheatres.com/domain.jsonalso names the step that creates one (Continue on the ticket page). Selecting seats and ticket types to see their prices is allowed, so AMC's read-onlypreview_ticketsis registered. Registration is still limited toreadOnlyHint: truetools.Allowlist:
browser_repl,computer_action,execute_playwright_codeandwebmcp. pi's--toolsdrops every other kernel MCP tool, including any the server adds later, so there is no denylist. Which tools count as "driving the browser" is still open.Stealth is off by default (
--stealthapplies it to every browser in a run). With stealth on,github.comandgithub.com/exploreoften stayed inreadyState: "loading"; without it, every page I tried loaded in 0–2s. The three pilot domains are low bot pressure.Both arms get the same setup. The loaded page must match the entry tool's URL patterns in both arms, and both launch the agent 10s after the page loaded (
start_delay_msis recorded per row), so registration does not give the treatment's page longer to settle.Setup handles stalled start pages. It waits up to 15s for the start page to be parsed, then navigates to the start URL once more. In stealth browsers the first navigation often stalls with the document still loading (49 of 149 trials in the main run needed the second navigation, and it parsed in 1–4s). After that it gives
loadup to 15s, and a failed setup gets up to three fresh browsers before the trial is aborted. GitHub intents start on/explore.The github
prepare_*tools stay registered. They are read-only; they just have no intents.Excluded tools. A domain's
domain.jsonlistsexclude(always left out, such as a tool that does what the system prompt forbids; none currently) andneeds_sign_in(reads that need a signed-in account, left out unless the domain signs in: amc, delta, etsy, github, kroger, resy, ubereats). Every name must exist.Signed-in trials. A domain can name a Kernel
profileand asigned_in_checktool. Jobs on one profile run one at a time, so two trials never use the account at once, and the hold estimate counts them back to back. Both arms load the profile without saving changes, itsneeds_sign_intools are registered, and the smoke gate invokes the check tool and stops the run if it reports an error such asnot_logged_in. Every treatment setup invokes it again, since a session can lapse mid-run; a signed-out trial aborts without trying more browsers. A missing profile skips the domain, or stops the run when the domain was selected by name. instacart.com usesbench-instacart, which does not exist yet; the README shows how to create it with a managed auth connection on a test account..env:bench/.env.examplelists the keys; the runner loadsbench/.envwhen present, and variables already set win.Tool files are evaluated in an empty
vmcontext (nofetch,processorimport) inside a child process with an empty environment and Node's permission model (no file system or child processes), and sources must come fromkernel/xwebmcp. The definitions are reshaped and serialized inside the context, so only a JSON string comes back: handing the context any runner function, such as a.mapcallback, would give a tool file the runner'sprocess. A vm context is not a security boundary, and the permission model denies the network only from Node 25, so on Node 22 a tool file that escaped could open connections but would find no keys or files. URL patterns are matched with the extension's own matcher.Fixtures were picked from live data. The fandango dates need moving forward before the pilot. There are no intents for Fandango's
get_ticket_offersorprepare_checkoutyet: a reserved-seat showtime shows no ticket prices until seats are selected. The system prompt now allows that, so they can be added once checked on live showtimes.Reading webmcp results: pi shortens tool results over 20 KB for the model, so adoption parsing reads the full text from the result's
structuredContent. Tool names are taken from any listing an agent returns, including reshapedwebmcp.listTools()output from Playwright or the REPL.Fixtures:
{"days_from_start": n}fixture values resolve to dates at plan time and are frozen inrun.json; the flight and Ticketmaster intents use them. The film intents keep fixed dates; once one has passed, a new run skips the intent unless it was selected by name, and a resume skips its remaining trials. A source with aprwhose branch was deleted resolves torefs/pull/<pr>/head; the runner logs it and the report marks the source(branch gone, PR head), since that is the PR's last head rather than main. Stable facts (ipwho 8.8.8.8, the merged Playwright PR, Playwright's branch and license, and theheadless-chrometopic's top three repositories) are pinned in rules; the AMC answer must name the film, the release tag must look likev1.2.3, and fares are checked from $1. ipwho uses an IP the model cannot recall (41.203.18.200, ZA / AS37153), Playwright's star and release counts have floors at roughly today's values (95,000 and 160), andgithub-bug-issues-with-prslists the 10 issues it judged, which must be distinct and numbered above 36,000.Robustness:
--run-idand--thinking, are validated;FETCH_HEAD.Later review changes: the report labels each intent's answers
pinned,agreementorshape onlyand names the faster or slower labels that come from shape-only intents; when treatment used the tools in some pairs but not all, it also gives the sign test and speedup over just those pairs, noted as not randomized.runQueuemoved tolib/schedule.mjswith tests, and releases a job's profile even when the job throws.normalizenow matchestoolsource.Normalizewhen whitespace sits between trailing semicolons.run.jsonrecords the kernel CLI version.Not in this PR
Reference answers and spot checks, the no-agent baseline, the telemetry-based outcome classes (
blocked,busy,outcome_unknown,mutation_attempted,tampered), scheduled runs, and the 30-pair verdict statistics.Testing
node --test "bench/**/*.test.mjs": 70 pass on Node 26 in about 25s (the stub run tests setBENCH_START_DELAY_MSto 1s instead of waiting the real 10s) (the 59 before the tool failures change also passed on Node 22). These cover the plan's worked example (speedup 2.6×, Δ +20 pp [−16, +51], p = 0.016,faster), the label rules, pair-level scoring, answer extraction, rules, pi transcript parsing (including shortened results and tool redefinition), the outcome order, the hold estimate, the adoption gate, isolated tool evaluation (nofetch,processorimport), composition order, job planning on resume (pending pairs, scored adoption trials, stale fixtures), pair retries (a complete pair always comes from one attempt), the paired speedup, fare agreement, the PR-ref fallback only on a missing branch, and failure capture when the call, the tool or the MCP server failed.The in-context evaluator gives the same output as the previous one for all 471 tool files on main, Add GitHub reads, content preparation and guarded stars #55, Add Fandango discovery, seat previews and ticket plans #58 and Add ipwho.is lookup_ip page tool #69, on Node 22 and 26, and a tool file's custom
.mapor.thencan no longer reach the runner'sprocess(a regression test covers both).run.mjsagainst stubpi,kernelandhypeshipcommands: with every browser create failing after the smoke gate, 5 pairs stopped after 6 aborts (before this change all 10 trials aborted and the run reported done), and a resume finished all 10 trials; a control browser that already has custom tools aborts its trial with that reason.With the vm evaluator, all 26 intents register the same tools and URL patterns as before, and every tool file on main and the Add GitHub reads, content preparation and guarded stars #55, Add Fandango discovery, seat previews and ticket plans #58 and Add ipwho.is lookup_ip page tool #69 branches (471) evaluates. A run with no hold resolved every intent and stopped at the hold check without starting the smoke gate; an unknown
--intentstops with a one-line error.node scripts/validate-registry.mjs,node --test "tests/*.test.mjs"and the extension tests still pass.Stale fixtures and deleted branches, checked without trials: a deleted PR branch resolved to its
refs/pullhead; with one fixture date moved into the past, a run naming its domain stopped, a default run skipped it and planned the rest, and a resume skipped its pending pairs.A real run with pi and
openai/gpt-5.4-mini, one pair each ofipwho-country-asn,github-repo-detailsandfandango-theater-showtimes:correct, at about $0.25 total.search_theaters(completed, 584 ms), then finished with Playwright, in 2:19 against control's 2:53.Earlier, all 22 trial setups (11 intents × 2 arms) passed browser creation, load, registration and the entry-tool check.
A screening run on main (
29da25c): 17 intents × 5 pairs, pi withopenai/gpt-6-lunaat medium thinking, stealth browsers, 8 slots.faster(aa, alaskaair, amctheatres, jetblue, southwest, united), 1slower(resy), 1no adoption(ubereats), 3no signal, 6inconclusive. Median speedup was 1.5×.not_logged_in.Runs after the latest fixes, pi with
openai/gpt-6-luna, stealth browsers:run.json. A wrong--modelfailed the smoke gate with the provider's error and left norun.json.bench-instacartprofile is skipped in a default run and stops a run that names the intent; an empty throwaway profile on Instacart failed the smoke gate withnot_logged_in; a throwaway profile on ipwho (whose lookup works signed out) passed the check, and both arms of two pairs ran on it, two browsers at a time, in orders BA and AB.correctin both arms on live data.Stub runs after the latest changes (stub
kernel,piandhypeship):--adoption 3, one slot, and every github.com browser create failing: before, 6 github adoption aborts in a row stopped the whole run before ipwho.is ran. Now the adoption trials interleave, the run finished, and github.com was stopped by its own adoption verdict.signed out: get_delivery_address returned not_logged_in, ran again once, and was left out; the report showsaborted: control 0, treatment 1.preview_tickets.After review (stub
pi, no trials):pi, an assistant message without a cost, or with a null one, stops the agent asno cost reported; a normal cost does not, and $5 trips the spend cap.runQueue's loop, driven with fake jobs over 3 slots, never ran two jobs on one profile at once, while other jobs filled the free slots.registry.jsonto one named definition each.Review fixes
signed_in_checkas treatment and removes the tools before its agent starts, so a lapsed session aborts both arms instead of only failing control trials. Checked live:kernel browsers webmcp custom-tools removeclears both the registration and the page's live tools.run.mjshas an end-to-end test (bench/test/run.test.mjs) against stubpi,kernelandhypeshipcommands and a local repository standing in for kernel/xwebmcp: a signed-in pair checks sign-in in both arms and a resume has nothing to run; a signed-out control aborts its trial. Removing the control check or the tool removal fails it.Later fixes
native_tools), control fails setup if a custom tool is live (not only registered), and the report names control trials that started with the site's tools, since there control is not tool-free.trials/<intent>/<pair>-<arm>and deleted the transcript of the attempt it replaced; reruns now go to-2,-3and so on, and each row records itstrial_dir.fandango-theater-showtimesdates itself 2 days from the run's start instead of a fixed 2026-10-13. The two intents tied to a film's run keep fixed dates.invoke ok.