Reference

August 3, 2026 · View on GitHub

← back to the module README

Verification and grades

Two gates check different things. The input gate decides whether a solve is trustworthy enough to build from; the output gate decides whether the distilled skill actually runs. Each has three levels:

gatewhat it askslevels (strict → loose)what it decides
inputis this solve's answer trustworthy?gold (match a known answer) → self_verify (shape + non-empty + the agent's own "I succeeded"; passes wrong-but-plausible answers, so use gold when correctness matters) → nonewhether a solve becomes material for a skill
outputcan the skill reproduce the answer with no model?strict (give back the recorded answer, ignoring spacing and case) → shape (non-empty + schema-shaped, for drifting answers) → off (no replay)the skill's grade, below

The output gate spends two nested budgets, --draws independent candidates each repaired up to --verify-rounds rounds; if none reproduce the recorded answers, nothing lands.

Both gates judge answers. One check judges the method. A solve can hold a right answer and still be nothing to build on:

RESULT = ["UA 729", "United", "12:10 AM"]
if "UA 729" in text: return "UA 729"

It recognised its answer instead of working it out, and the input gate can't see that: the answer is right, and the answer is all it looks at. Distillation may not copy instance values, so it must invent an extractor the trace never had — and then fails replay on that instance, however many draws you spend. So a solve whose script contains every field of its answer verbatim is dropped before distilling, counted as dropped_lookup. (Every field at once — "the answer appears in the code" flags working solves too, where an airline name is a vocabulary entry.)

Whichever level the output gate ran at becomes the skill's grade:

executablereference (--on-fail reference)unverified (--verify off)
what happenedreplay ran and reproduced the answersreplay ran and it failedno replay ran
what it buysrun it directly: plain python/playwright, no model, cron-ablea prior for the agent: exact selectors, URLs, param shapes it reads and reusesthe same prior, but untested: might run standalone, might be broken
trustprovedknown not to reproduce its answersunknown
refiningincremental refines must pass regression replay (replays.json); a verified skill is never overwritten by an unverified refinerefined freelyrefined freely

unverified (--verify off) skips replay entirely. Use it when no replay could be fair: the site needs credentials the library can't store, or the training instances themselves have expired (the date has passed, the listing is gone), so the skill comes back with nothing and even shape rejects it (an empty answer fails the gate) for something that isn't the skill's fault. Drifting values are not that case; shape replays those and compares loosely. Nor is a page that moved: there the failed replay is real news, and --on-fail reference (the default) keeps the broken skill as a labelled prior; --on-fail reject instead lands nothing and leaves the runs retryable. reference isn't a grade to aim for, but it's the default so a failed replay never leaves you empty-handed; a clean run still lands executable.

Why code even at reference grade, versus a natural-language note: the selectors, URLs and param shapes are verbatim-copyable into the agent's next script, individual primitives often still run when the whole skill doesn't, and a reference skill is one repair away from executable. And a prior alone pulls its weight: the WebArena numbers in Results come from a library the agent read exactly this way. The flights skill in the Quick Start, by contrast, reruns an unseen route standalone, which is what executable buys.

All parameters

The commands fall into two modes (see Manual mode for when to use which). Quick mode covers init, build, and learn: it infers the template and parameters and gates each solve for you. Manual mode is update: you hand it a manifest and state all of that yourself. route and skill_use belong to neither: they're the out-of-loop reuse decision — resolved before/around a solve (by route, or injected into the agent's prompt by build/learn), never a call the agent makes mid-loop.

Quick mode

python -m webwright.skill_factory init "<need>"

flagdefaultmeaning
-o, --outskill.yamlwhere to write the drafted spec
--rows3how many blank instance rows to leave for you to fill (these become the instances: in the spec)

One LLM call. Drafts the template, the start_url (a guess, check it), and the verify mode it judges the task needs. Never the values.

python -m webwright.skill_factory build <spec.yaml>

Solves the spec's instances, then hands them to learn. Everything in the spec's build: block can be overridden here; machine-specific things are flags only, so the spec stays committable.

flagdefaultmeaning
--librarylibrarylibrary directory to grow
-c, --config(none)webwright model config for the AGENT (repeatable). It reads a yaml, not the env vars
--jobs1solve N instances at once (solving only; learn is serial). More than you have means all of them
--outputs<spec dir>/build_outputswhere solves are written; also what you point learn at to retry
--dry-runoffprint the plan (substituted tasks, policy, what would be solved) and stop
--yesoffskip the confirmation before spending agent time
--verify, --verify-rounds, --draws, --on-fail, --chunk, --goldsfrom the spec's build: block, else learn's defaultsoverride the spec

On --jobs: the ceiling is the site, not the flag. Too many browsers from one IP gets you throttled, which reads as your solves failing; 3 to 5 is safe. With N > 1 each solve goes to build_outputs/solve_NN.log and progress ticks every 30 s.

python -m webwright.skill_factory learn <runs_dir>

flagdefaultmeaning
--librarylibrarylibrary directory to grow
--golds(none)JSON {task_id: gold_answer} → gold gate instead of self_verify
--chunk25runs per LLM grouping call
--dry-runoffprint the grouping plan, change nothing
--verifystrictreplay bar: strict = give the recorded answers back (spacing and case folded, so a label typed AS 26 for a page that prints AS26 can't sink a working skill), shape = non-empty + schema-shaped (live data), off = skip
--verify-rounds2repair rounds within one candidate: its failures are fed back and it is re-distilled
--draws2independent candidates before giving up. A draw can simply come out brittle, and a fresh one often lands where repairing the bad one won't. Stops at the first that verifies
--on-failreferencefailed verification: reference (lands as a labeled prior; never overwrites an existing skill) or reject (lands nothing, runs stay retryable)

Manual mode

python -m webwright.skill_factory.update

The manifest-driven path (see manual.md). Use it when you're assembling batches by hand rather than from a spec.

flagdefaultmeaning
--manifestrequired{template, runs:[{dir, admit(bool, REQUIRED), params, verdict, site, output_schema, answer?, credentials?}]}
--libraryrequiredlibrary directory
--verify / --verify-rounds / --draws / --on-failoff / 2 / 2 / referenceas above. --verify is off by default here because benchmark sites may need credentials; --verify-rounds and --draws only take effect once you turn --verify on

Solve time

python -m webwright.skill_factory route

Route a task out of the agent loop: recommend decides run/adapt/skip, then route acts on it — runs a matching executable skill directly (no model), or hands the task to the agent with the skill as a prior, falling back to the agent if a direct run fails or comes back the wrong shape. Without --start-url it only prints the decision (and still runs a directly-runnable skill).

flagdefaultmeaning
--taskrequiredthe task to route
--library$SKILL_LIBRARY_ROOT, else librarylibrary to search
--start-url(none)launch a Webwright solve on this URL for the agent path; omit to only decide
-c / --config(none)agent model config for a launched solve (base.yaml is added automatically)
-o / --out, --task-id. / route_taskoutput dir and task id for a launched solve
--jsonoffprint the raw outcome as JSON

python -m webwright.tools.skill_use

The recommend decision on its own — retrieve + judge → {verdict, skill_id, source_path, how_to_reuse}, doing nothing else. Ranking is local; the verdict is one LLM round trip on the module's model (below). route and the prompt-hint injection are built on it; you rarely call it directly.

flagdefaultmeaning
--taskrequiredthe task text to match against the library
--library$SKILL_LIBRARY_ROOT, else librarylibrary directory to query
--output(none)also write the JSON verdict here; it goes to stdout either way

Environment variables

Two models, two doors

build is solve × N, then learn. Each half runs a different model:

the agent's modelthe module's model
what it doesopens the browser: looks at the page, picks the next click, and again, ~50 times per solvenever opens a browser: reads the finished transcripts and writes the skill's python
who calls itthe solves in build; any Webwright runlearn, plus init (drafts your spec) and recommend (the reuse decision behind route)
which modelmodel_name: in a yaml you pass as -c model.yamlSKILL_MODEL_NAME, else OPENAI_MODEL, else the class's fallback
what URLopenai_endpoint: in that yamlSKILL_MODEL_ENDPOINT, else OPENAI_ENDPOINT, else the class's fallback

Same class underneath (models/openai_model.py); llm.py just builds from env the config the yaml spells out by hand. So SKILL_MODEL_NAME and OPENAI_MODEL aren't two settings: one field, SKILL_MODEL_* wins. Two names exist so you can send distillation somewhere other than whatever else already reads OPENAI_*; if you don't care, set only OPENAI_*.

Set neither of those and you get that class's own fallbacks, gpt-4o at https://api.openai.com/v1/responses, and a line on stderr saying so. Those are inherited defaults, not suggestions: the Results ran on a much newer model, and every skill in your library is written by whichever one you leave it on. Name it.

The agent's model reads none of these vars; nothing outside llm.py does. On a custom gateway set both doors, or your solves go to api.openai.com while everything else uses your gateway. build warns when only one is set.

varread bymeaning
OPENAI_API_KEYboth modelsthe key, and the one genuinely shared var
OPENAI_ENDPOINTthe module's modelcustom gateway. The FULL request URL, e.g. https://gateway.example/api/responses, not .../api. A base path fails
OPENAI_MODELthe module's modelwhich model the module's calls use
SKILL_MODEL_ENDPOINTthe module's modelthe same setting as OPENAI_ENDPOINT, higher priority (above)
SKILL_MODEL_NAMEthe module's modelthe same setting as OPENAI_MODEL, higher priority (above)
SKILL_MODEL_CLASSthe module's modela non-OpenAI backend. Defaults to openai
SKILL_MODEL_TIMEOUTthe module's modelseconds per call. Defaults to 600: distilling a skill emits ~16k tokens, and the model's own 120 s default cuts it off mid-file
SKILL_LIBRARY_ROOTroute / skill_usedefault for --library, so you don't repeat the path
WORKSPACE_DIRevery generated skillwhere a skill writes agent_response.json, its log and screenshots. Defaults to the cwd, which is why the docs cd to a scratch dir before running one

Using the module as a library rather than a CLI? configure_llm(model) hands it a model object directly and every var above is ignored: that's how a running agent gives the module its own backend, with no gateway or key hardcoded anywhere.

Components

The module's files and what each one does, for anyone reading or extending the code.

The data flow between them, and the interface each one exposes:

data flow and interfaces

filerole
library.pySkill + Library(root): on-disk skills (<id>/skill.py + meta.json)
retrieve.pyretrieve(task, library) → ranked Candidates (relevance)
decide.pydecide(task, candidates)Decision(verdict, skill_id, reason) (utility verdict: use/adapt/skip)
skill_use.py (tools)recommend(task, library){verdict: run/adapt/skip, skill_id, source_path, how_to_reuse, params}: the out-of-loop decision; promotes decide's use to run when the skill is executable and every slot fills, else adapt
route.pyroute(task, library): out-of-loop orchestrator — recommend, then carry it out (run the skill directly / launch the agent / fall back)
execute.pyrun_skill(source, params): run a skill directly via a taskspec, out of the agent loop
fill.pyfill_params(task, param_names): pull each slot's value from the task for a direct run (missing → None, never invented)
entry_shim.pyprepends the CLI shim so a generated skill accepts --flags as well as a positional taskspec.json
gate.pygate(result, method=gold|self_verify|none) → admit? (keeps wrong solves out)
update.pyevolve(traces, library): grow on the existing library (add / adapt-refine / keep); _refine parameterizes and decomposes into primitives, incrementally improving an existing skill
llm.pyconfigure_llm(model) + llm(): backend-agnostic via Webwright's Model abstraction; a bare CLI builds the model from SKILL_MODEL_NAME/SKILL_MODEL_ENDPOINT (or OPENAI_*) env, no hardcoded endpoint/key
prompt.pywith_skill_hint(prompt, task, library): resolves the library lookup out of the agent loop and prepends the chosen skill to the prompt (used by build/learn; the route agent path injects the same hint)