CHOOSEBrowser & web
pi-jev-browser
An installable browser agent that uses structured DOM observations and a Jev mechanism for decision-making, avoiding costly screenshots.
SignalJev reads a structured DOM observation, picks one action, then observes again.
I finally turned the browser agent I wanted into something installable: pi-jev-browser
It is not part of the "send a screenshot to the model every step" school. Jev reads a structured DOM observation, picks one action, then observes again.
Open source, Apache-2.0, on npm and in the official pi catalog.
TL;DR first, details after.
▍Why not a screenshot loop
The problem with screenshots is not accuracy, it is that every step is expensive. A 1280x720 screenshot is thousands of tokens before the model has done
anything, and a 20-step task pays that 20 times. DOM text is a few KB, and the model sees a list of addressable targets instead of "what the page looks
like".
The trade is real: it cannot read purely visual content. That is a trade-off, not a free lunch.
▍How it works
Two layers.
The first is Jev (TypeSafe System One). It reads a structured observation: visible text, interactive targets (role/label/href/state), controls still
offscreen, and what is already selected. It returns one operation at a time, straight to POST /v1/systemone, no gateway in between.
The second is a deterministic execution layer. Manual actions can address elements directly:
{type:"click", target:{role:"link", name:"Blog"}}
{type:"fill", target:{role:"textbox", name:"Email"}, value:"a@b.c"}
{type:"select", target:{role:"radio", name:"Nonstop only"}}
No coordinates, repeatable, and it fails loudly when nothing matches instead of clicking whatever is nearby. Jev explores and plans; selectors execute
precisely.
Seven tools:
- jev_run: start the browser, run the Jev loop, return before/after screenshots
- jev_actions: manual actions without calling Jev
- jev_extract: deterministic read (text/table/links/attributes), zero model calls
- jev_state, jev_logs, jev_stream, jev_stop
▍Measured numbers
All of these are from my own runs, not repeated from someone else's post. macOS, Chromium 1243, Jev 1.13.0.
Decision latency: median ~280ms (run medians across five independent measurements landed at 266-295ms; the slowest single call, 974ms, was the first call
of a connection)
Cost: ~US$0.001 per page, ~US$0.0003 per decision. That is input tokens only (3 System One calls per page, at TypeSafe's current $0.042/M input rate,
output free), not the whole bill.
Warm page-to-page: 2.7s median. Cold, including Chromium launch: about 4.6s.
▍What real tasks look like
Google Flights, Taipei to London round trip, cheapest fare per month:
- 2026-10 $1,026 (China Airlines + KLM, 18h55m, 1 stop AMS)
- 2026-11 $1,115 (China Airlines + KLM, 19h30m, 1 stop AMS)
- 2026-12 $928 (Etihad, 25h25m, 1 stop AUH), the cheapest of the three
Those match an earlier independent measurement I took in TWD (NT$32,604 / NT$35,408 / NT$29,482) at the prevailing rate.
A two-step filter dialog (Stops, then Nonstop): Jev completed it on its own and landed on a China Airlines nonstop, 16h10m, $1,124. I confirmed that fare
three independent ways.
Header enumeration on a real marketing site: 10 links clicked, 11 decisions, no repeats, 0 wasted retries, 8.1s.
▍Safety model
Hard-coded in Jev's rules: sending messages, posting, submitting orders, payments, bookings, deletion, permission changes, entering sensitive data, facing
a CAPTCHA or a security warning all require REVIEW first, handing control back to the human. No supported action left means BLOCKED.
Other layers:
- A new tab no longer hijacks the run silently: the default is "stay", the switch is logged and reported, and moving tabs requires an explicit
activate_tab
- A coordinate click that lands in an iframe records that frame's origin, and names known anti-bot providers (reCAPTCHA / hCaptcha / Cloudflare)
- denyOrigins is a hard block, checked before allowedOrigins and before Chromium even starts
- requireConfirmation asks per call, and fails instead of proceeding when no dialog UI exists
- Provider errors are redacted from tool output (they can quote the request body); the full text only goes to a local errors.log
One privacy fact worth stating plainly: this tool sends the visible text of the page to api.typesafe.ai for the decision. The analysis is remote. Judge
sensitive pages accordingly. Field values are also generated by the pi model in use.
▍How I verified it
Rather than writing up a demo that went well, I turned verification into a suite you can rerun (npm run benchmark). Three tiers:
- local: offline fixtures, no credential, zero model calls
- model: real Jev decisions, but only against local pages
- live: real third-party sites
The point is that every scenario has to be falsifiable: a submitted form is checked against the fixture server's request log, a clicked link against the
decision trace, a fare against three independent paths. Nothing passes by claiming success.
14 scenarios pass, and one is reported as GAP, not PASS and not ignored:
Jev does stop at the real reCAPTCHA page (it returns REVIEW), but only because that widget lives in an iframe it cannot see, not because it recognised
human verification. Given an ordinary DOM human-verification gate, it clicks straight through and reports success.
I asserted that limitation on purpose: it shows as GAP while the gap exists, and turns into FAIL the moment the guardrail starts refusing, so the
limitation cannot quietly disappear from the report.
▍Bugs the suite found
This is the part I most wanted to share. All of these surfaced only after I had a suite:
- Silent tab hijack: any popup unconditionally became the observed page, so the agent could be teleported to another site without anyone knowing
- Scroll oscillation: alternating SCROLL_UP and SCROLL_DOWN is a two-cycle that evades both the repeated-action guard (needs three identical actions) and
the no-progress guard (I had excluded SCROLL when fixing an earlier bug). It burned the entire step budget
- An event race: the click returned at +71ms, the new-tab event arrived at +73ms, so the notice missed the very call that caused it
- A selector reading an empty document mid-navigation, and a {role, text} target silently ignoring text
- A publishing hazard: "benchmarks/" in the files field overrode gitignore, and 18 files with local URLs and measurement logs nearly shipped in the
tarball
My own bugs are in that list too.
▍What it cannot do
- iframe content: structurally invisible to the automatic loop
- Date pickers and multi-step form entry: the flight scenarios used the URL shortcut and skipped the form
- No visual assertions: every conclusion comes from page text, the DOM, the trace, or server logs, never from a screenshot
- No purchase path was tested
- Everything is sequential, one browser per pi session, and there is no spending cap (only step and evaluation budgets)
- It needs a TypeSafe API key (paid, remote)
▍Install
pi install npm:pi-jev-browser
Or pin a version:
pi install git:github.com/laihenyi/pi-Jev-browser@v0.1.0
Then give it a TypeSafe key (TYPESAFE_API_KEY, or typesafe.apiKey in ~/.pi/agent/pi-jev-browser.config.json with chmod 600). Playwright downloads Chromium
itself, about 150MB.
▍This is a port, not a rewrite
The original is Cline's cline/plugins/plugins/jev-browser. The observation pipeline, Jev's prompt, the run statuses and the safety contract carry over.
What I changed: the host API, the decision transport (direct to TypeSafe), the text helper (the active pi model), plus the selector layer, a persistent
profile, per-site policy, and the fixes above.
Apache-2.0, 62 tests.
▍Links
GitHub github.com/laihenyi/pi-Jev-browser
npm npmjs.com/package/pi-jev-browser
catalog pi.dev/packages/pi-jev-browser
If you are building browser agents, or want a baseline to compare approaches against, the suite in benchmarks/ runs as-is. I would genuinely like to hear
what you measure.