Repository navigation
v0.9.0: feat(release): block a release with no eval run for its build (#572)
-
A .NET client.
dotnet add package Fastbrowseruns tasks from .NET over the protocol the JavaScript SDK speaks, with the same options: structured output, secrets resolved in your process, anuntilcheck, events and frames. Cancelling theCancellationTokenstops a run once its browser has closed. The package carries each platform's binary. .NET 10 or newer. -
A Java client, for Kotlin too.
dev.agentlabs:fastbrowseon Maven Central runs tasks from the JVM over the protocol the JavaScript SDK speaks, with the same options: structured output, secrets resolved in your process, anuntilcheck, events and frames.runreturns aCompletableFuture, and cancelling it stops the run. Each platform's binary ships in anatives-<platform>jar beside the client, unpacked once into the user cache directory. Java 17 or newer. -
A Go client.
go get github.com/agent-labs-dev/fastbrowse/goruns tasks from Go over the protocol the JavaScript SDK speaks, with the same options: structured output, secrets resolved in your process, anuntilcheck, events and frames, and cancellation throughcontext. The first start downloads the release's binary for the platform and refuses one whose SHA-256 is not the one the module's tag pins. Each release tags the modulego/vX.Y.Z. -
fastbrowse is on crates.io.
cargo add fastbrowsegives a Rust programrun_taskin its own process and the result's types, from the same core the CLI, the Python package and the JavaScript SDK run on.cargo install fastbrowse-cliinstalls thefastbrowsecommand. Each release publishes the crates at the same version as PyPI and npm. -
FASTBROWSE_LLM_PROVIDER=anthropicholds Claude to the schema. Anthropic's OpenAI-compatible endpoint ignoresresponse_format, so the preset stated the schema in the prompt and a reply that drifted cost a second call. Claude is now reached through the Messages API with the schema as structured output, the key asx-api-key, andFASTBROWSE_LLM_REASONINGasoutput_config.effort,lowby default since Claude 5 models think by default and thinking counts against the output cap. Schema keywords structured outputs refuses, such as numeric bounds, are left out of the request and still checked against the reply. The retries, repair, truncation retry and ledger are the ones every LLM call uses.FASTBROWSE_LLM_REASONING=noneis refused for Claude. -
FASTBROWSE_LLM_PROVIDERsends the LLM to OpenAI, Anthropic, Gemini, Groq, Together AI, Mistral, DeepSeek, xAI, Fireworks, Azure OpenAI, Ollama, LM Studio or vLLM by name. Pointing the LLM anywhere but OpenRouter or the gateway meant knowing each provider's address, its key, whether it takes a strict schema and where it reads a reasoning effort, andFASTBROWSE_LLM_BASE_URLsent every one of them OpenRouter's request shape. A preset now fills those in from the provider's own documentation: OpenAI and Azure getmax_completion_tokensandreasoning_effort, Azure itsapi-keyheader, and DeepSeek, whose JSON output takes no schema, getsjson_objectwith the schema stated in the prompt. Each preset reads only its own key variable, such asOPENAI_API_KEY, sends it to its own host alone, and refusesFASTBROWSE_LLM_BASE_URLrather than carry the key elsewhere. A provider with no default model asks forFASTBROWSE_LLM_MODELbefore the first request, and the run's route names the provider. With the setting unset, the LLM is chosen as before. docs/llm-providers.md lists every preset and its sources. -
Perplexity and AIMLAPI can serve the decision model.
FASTBROWSE_DECISION_SOURCE=perplexityaskspplx-decider-v1.2(orpplx-decider-flash-v1.2) withPERPLEXITY_API_KEY, priced at Perplexity's $0.02 per million input tokens ($0.01 on flash) and held to its 128 questions a request.aimlapiaskstypesafe/jev-1.13by default withAIMLAPI_API_KEY, andFASTBROWSE_DECISION_MODELpicks another of its models:liquid/d1, a later Jev, oropenai/gpt-6-luna, which is sent in OpenAI's own shape. AIMLAPI reports no cost and resells at its own rates, so its calls are an unknown cost, which a--max-dollarslimit refuses rather than prices as the model's list. -
OpenAI's Decisions API can be the decision model.
FASTBROWSE_DECISION_SOURCE=openaiasksgpt-6-lunaatapi.openai.com/v1/decisionswithOPENAI_API_KEY, and nothing else is sent that key. The run's questions are rewritten into its shape (a yes/no question becomes a predicate whose true and false descriptions join its instructions) and its answers back into the shape every other source returns, so they pass the same checks; a refused question fails the request. The response states no cost, so each call is estimated at $0.10 per million input tokens. It takes up to 400 questions a request, and its thresholds are Jev's until an eval tunes them. -
A decision model can come from Telnyx, a local Ollama, or any System One server.
FASTBROWSE_DECISION_SOURCE=telnyxasks Telnyx'stelnyx/decision-flash(ortelnyx/decision-prowithFASTBROWSE_DECISION_MODEL) withTELNYX_API_KEY.ollamaasks a local Ollama athttp://localhost:11434with no key, andcustomasks the server atFASTBROWSE_DECISION_BASE_URL; both needFASTBROWSE_DECISION_MODELand takeFASTBROWSE_DECISION_API_KEYwhen the host wants one. Each is sent only its own key, has no backup, and is held to its model's limits: 64 options a choice on Telnyx, 26 on Ollama. A System One host given no key is now sent noAuthorizationheader rather than an empty one, and an HTTP 413 is read as input too large. In the Rust crates,TypeSafeJevClientis nowSystemOneClient, infastbrowse_clients::system_one. -
The decision model's settings are named for any decision model, not for Jev.
FASTBROWSE_DECISION_SOURCE,FASTBROWSE_DECISION_BASE_URLandFASTBROWSE_DECISION_MODELreplaceFASTBROWSE_JEV_SOURCE,FASTBROWSE_JEV_BASE_URLandFASTBROWSE_JEV_MODEL. The former names are still read when the new one is unset. In the Rust crates,fastbrowse_core::jevis nowfastbrowse_core::decision,JevClientisDecisionClient,JevErrorisDecisionErrorandFailoverJevClientisFailoverDecisionClient. What a run reports is unchanged: its cost lines and trace spans still sayjev, and--max-jev-callskeeps its name. -
A decision model other than Jev 1.13 is held to its own limits. The choice ceiling, token limits and price were Jev's constants, so a model pinned with
FASTBROWSE_JEV_MODELon another System One host was sent 240-option choices and priced as Jev. Each model now has a profile of what it accepts and costs. A model fastbrowse does not know gets the tightest documented limits (26 options a choice, 64 questions and 64 KiB a request) and no price, so its unmetered calls are an unknown cost rather than Jev's. Its thresholds are still Jev's, and the trace says so with adecision_unvalidatedevent. Jev 1.13 runs exactly as before. -
The Rust core is the only core: the Python one is removed.
FASTBROWSE_PYTHON_CORE=1no longer selects anything, and the modules it ran (fastbrowse.agent,fastbrowse.browser,fastbrowse.clients,fastbrowse.cli,fastbrowse.mcp_server,fastbrowse.serveand the rest) are gone with their tests, the PyInstaller build and thecdp-use,pydantic-settingsandwebsocketsdependencies.run_task,connect_cdp,resolve_cdp_port,tracedand the pydantic models keep their signatures.ConfigurationErrorandRecordingErrorare now infastbrowse.models, and every browser errorconnect_cdpraises is infastbrowse.page. The kept modules lose what only the Python core used:fastbrowse.page.Page,fastbrowse.models.BrowserConnectionandSecretValue,fastbrowse.jev.JEV_MODEL,fastbrowse.llm.recordedandfastbrowse.telemetry.trace_to, andfastbrowse.telemetry.Ledgeris now the protocol of the ledger the core hands a caller'sllm=client, not one to construct. The Arch package builds fastbrowse alone. Themcpandbrowser-useextras are removed, since the commands need neither:uvx --from fastbrowse fastbrowse-mcpserves MCP. -
Direct TypeSafe is the recommended Jev route. It puts one service between fastbrowse and the model and keeps the direct-API price. OpenRouter still serves Jev text when it is the only Jev key, but it is not recommended, and it never receives Jev image input. A gateway key on its own also serves Jev, as a last resort; otherwise the Vercel AI Gateway is a Jev fallback for an outage, not the recommended route. Jev's Images and Box surfaces are not used by the agent yet; when they are, a run with only an OpenRouter key keeps image input off, so page pixels never travel through the router. The LLM's screenshot path is separate and still follows the LLM route.
-
A run is stuck by one rule: its page has not changed for
stall.unchanged_actionssteps. Four special cases sat beside it and are removed. A page state the run had already reached was called a loop ("the actions since undid each other") and sent to recovery, and on a six-part cart task that stopped the run at four of six, on a walk from search to product to cart that changed the page at every step. A setting put back to an earlier value, and a click repeated after one that changed nothing, were each sent to recovery the same way. The shadow tripwires for repeated actions and unchanged requirements are gone too:stall.repeated_actions,stall.stagnant_plan_stepsandstall.tripwiresare no longer settings, and a config that sets one is refused as unknown, andRunResult.would_fireis no longer reported. A page state never reached before still renews the recovery budget, and the same action taken a third time from the same page still counts as no change, so going round two pages still ends in recovery. -
FASTBROWSE_LLM_REASONING_<PURPOSE>sets one purpose's reasoning effort, overFASTBROWSE_LLM_REASONING, asFASTBROWSE_LLM_MODEL_<PURPOSE>sets its model. DeepSeek V4.1 Flash reasons at every effort and spent the completion verdict's whole output allowance thinking, so the verdict came back empty and the run ended as if it had failed;FASTBROWSE_LLM_REASONING_VERIFY=noneturns reasoning off for the verdict alone and leaves planning and recovery at their effort. Nothing changes unless it is set: a model that refuses disabled reasoning, asgoogle/gemini-3.8-flashdoes, is never sentnoneby default. -
A click the page answers over fetch is judged by the answer. A settle read the page once its DOM was quiet, so an add-to-cart that posts to the cart and redraws when the response arrives was read before it redrew. On The Pi Hut the quick-add drawer's Add to cart was recorded as a click that changed nothing, and the run added the item again from its product page, then spent its recoveries working out why the cart held two. When the page has not changed and a fetch or XHR request it started after the action is still open, the settle now waits for that response, within the same 5s deadline, then judges the page. A click whose request changes nothing is still a click that changed nothing, and a request that never answers ends the wait at the deadline as no change.
-
A visually hidden checkbox or radio is pressed through its label. A theme's variant swatch is often a 1px visually hidden radio inside a wrapper, with its
<label for>drawn as the swatch beside it. The hit test looked for a point of the radio's own box, found only the wrapper there, and refused the click as covered: on The Pi Hut the OLED display's Blue option was refused four times and the run ended stuck. The points of the control's own labels now count as its points, which is how the browser itself toggles it; anything drawn over the label still blocks the click. -
A search typed mid-run is written for the next item the task still needs. The field writer was given the search rule and the requirements still unmet only while it recovered a missing answer output. Mid-run it saw only the whole task, so on a six-part cart task it called the search box's query missing and the run ended. It now always has both, and types the next item still to add.
-
FASTBROWSE_LLM_REASONING=noneturns reasoning off. The setting tooklow,mediumorhigh. A model such as DeepSeek V4.1 Flash reasons at every effort and does not scale it to the output allowance, so a short call such as the completion verdict could spend all of it thinking and come back empty.nonesends OpenRouter'sreasoning: {"effort": "none"}. -
Settling no longer waits on anything named like a spinner. A settle waited up to 1.5s, and every observation and read up to 3s more, while any visible element had
spinner,loading,loader,skeletonorshimmerin its class or id. A guessed list matches content: Shopify themes call the add-to-cart button's labelloader-button__text, so on every product page each action paid 1.5s and each observation 3s. Through a CDP proxy on a six-part cart task, settle waits took 331s of the 363s not spent in model calls; without the list the median observation wait fell from 3.08s to 0.05s and the median action settle from 1.60s to 0.20s, and seconds per step from 7.2 to 4.0. Settling now waits for an interactive document and a quiet DOM alone. A page still loading is read as it stands, and the reader and Jev judge from its text whether to look again: on a page that shows "Loading..." for 2.5s, the answer is still right (3 of 3 runs) at about 3s more than before. -
--fetch-firstreads over HTTP and starts a browser only when the run needs one.--fetchis a promise that the task never needs a browser, so a step that clicks, or a page that scripts build, ends the runblocked. With--fetch-firstthe same run starts again in the browser instead: the run tries the cheap read, and the browser is the fallback rather than the caller's guess. Nothing is inferred from the task text; the browser is started on the fetched page's own refusal. The browser run spends from what the attempt left of--max-steps,--max-dollarsand the call and time limits; an attempt that used one up endsbudget_exceedednaming it, and one with an unpriced call under--max-dollarsdoes not start a browser it cannot bound. The result's cost lists the attempt's calls first, and its steps are the browser run's; the trace marks the hand-over with anescalateevent. It cannot be combined with--fetch,--record,--attachor--target-match, which need a browser from the first step.run_task(fetch_first=True)does the same from Python;serve --stdio, the SDK and MCP take neitherfetchnorfetch_first. -
A browserless read decodes a gzip or deflate response instead of refusing it.
--fetchasked for no compression and refused a body that arrived compressed anyway, and python.org's CDN gzips whatever it is asked, so every python.org page ended the runblockedas needing a browser. The Rust reader now asks for gzip and deflate and decodes them, as httpx does for the Python one, and the 5 MiB bound applies to what the body decodes to, so a small body that inflates past it is still refused. Brotli and zstd are still refused, being codings it never asks for. -
A browserless read names its client, so a site that requires a
User-Agentstops refusing it.--fetchsent noUser-Agent, and a server that refuses a client that does not name itself answered 403, as Wikipedia does. The Rust reader now sendsuser-agent: fastbrowse/<version>.