Skip to content

v0.9.0: feat(release): block a release with no eval run for its build (#572)

Choose a tag to compare

@github-actions github-actions released this 29 Oct 22:55
· 64 commits to main since this release
4172c72
  • A .NET client. dotnet add package Fastbrowse runs tasks from .NET over the protocol the JavaScript SDK speaks, with the same options: structured output, secrets resolved in your process, an until check, events and frames. Cancelling the CancellationToken stops 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:fastbrowse on 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, an until check, events and frames. run returns a CompletableFuture, and cancelling it stops the run. Each platform's binary ships in a natives-<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/go runs tasks from Go over the protocol the JavaScript SDK speaks, with the same options: structured output, secrets resolved in your process, an until check, events and frames, and cancellation through context. 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 module go/vX.Y.Z.

  • fastbrowse is on crates.io. cargo add fastbrowse gives a Rust program run_task in 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-cli installs the fastbrowse command. Each release publishes the crates at the same version as PyPI and npm.

  • FASTBROWSE_LLM_PROVIDER=anthropic holds Claude to the schema. Anthropic's OpenAI-compatible endpoint ignores response_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 as x-api-key, and FASTBROWSE_LLM_REASONING as output_config.effort, low by 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=none is refused for Claude.

  • FASTBROWSE_LLM_PROVIDER sends 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, and FASTBROWSE_LLM_BASE_URL sent every one of them OpenRouter's request shape. A preset now fills those in from the provider's own documentation: OpenAI and Azure get max_completion_tokens and reasoning_effort, Azure its api-key header, and DeepSeek, whose JSON output takes no schema, gets json_object with the schema stated in the prompt. Each preset reads only its own key variable, such as OPENAI_API_KEY, sends it to its own host alone, and refuses FASTBROWSE_LLM_BASE_URL rather than carry the key elsewhere. A provider with no default model asks for FASTBROWSE_LLM_MODEL before 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=perplexity asks pplx-decider-v1.2 (or pplx-decider-flash-v1.2) with PERPLEXITY_API_KEY, priced at Perplexity's $0.02 per million input tokens ($0.01 on flash) and held to its 128 questions a request. aimlapi asks typesafe/jev-1.13 by default with AIMLAPI_API_KEY, and FASTBROWSE_DECISION_MODEL picks another of its models: liquid/d1, a later Jev, or openai/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-dollars limit refuses rather than prices as the model's list.

  • OpenAI's Decisions API can be the decision model. FASTBROWSE_DECISION_SOURCE=openai asks gpt-6-luna at api.openai.com/v1/decisions with OPENAI_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=telnyx asks Telnyx's telnyx/decision-flash (or telnyx/decision-pro with FASTBROWSE_DECISION_MODEL) with TELNYX_API_KEY. ollama asks a local Ollama at http://localhost:11434 with no key, and custom asks the server at FASTBROWSE_DECISION_BASE_URL; both need FASTBROWSE_DECISION_MODEL and take FASTBROWSE_DECISION_API_KEY when 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 no Authorization header rather than an empty one, and an HTTP 413 is read as input too large. In the Rust crates, TypeSafeJevClient is now SystemOneClient, in fastbrowse_clients::system_one.

  • The decision model's settings are named for any decision model, not for Jev. FASTBROWSE_DECISION_SOURCE, FASTBROWSE_DECISION_BASE_URL and FASTBROWSE_DECISION_MODEL replace FASTBROWSE_JEV_SOURCE, FASTBROWSE_JEV_BASE_URL and FASTBROWSE_JEV_MODEL. The former names are still read when the new one is unset. In the Rust crates, fastbrowse_core::jev is now fastbrowse_core::decision, JevClient is DecisionClient, JevError is DecisionError and FailoverJevClient is FailoverDecisionClient. What a run reports is unchanged: its cost lines and trace spans still say jev, and --max-jev-calls keeps 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_MODEL on 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 a decision_unvalidated event. Jev 1.13 runs exactly as before.

  • The Rust core is the only core: the Python one is removed. FASTBROWSE_PYTHON_CORE=1 no longer selects anything, and the modules it ran (fastbrowse.agent, fastbrowse.browser, fastbrowse.clients, fastbrowse.cli, fastbrowse.mcp_server, fastbrowse.serve and the rest) are gone with their tests, the PyInstaller build and the cdp-use, pydantic-settings and websockets dependencies. run_task, connect_cdp, resolve_cdp_port, traced and the pydantic models keep their signatures. ConfigurationError and RecordingError are now in fastbrowse.models, and every browser error connect_cdp raises is in fastbrowse.page. The kept modules lose what only the Python core used: fastbrowse.page.Page, fastbrowse.models.BrowserConnection and SecretValue, fastbrowse.jev.JEV_MODEL, fastbrowse.llm.recorded and fastbrowse.telemetry.trace_to, and fastbrowse.telemetry.Ledger is now the protocol of the ledger the core hands a caller's llm= client, not one to construct. The Arch package builds fastbrowse alone. The mcp and browser-use extras are removed, since the commands need neither: uvx --from fastbrowse fastbrowse-mcp serves 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_actions steps. 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_steps and stall.tripwires are no longer settings, and a config that sets one is refused as unknown, and RunResult.would_fire is 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, over FASTBROWSE_LLM_REASONING, as FASTBROWSE_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=none turns 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, as google/gemini-3.8-flash does, is never sent none by 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=none turns reasoning off. The setting took low, medium or high. 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. none sends OpenRouter's reasoning: {"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, skeleton or shimmer in its class or id. A guessed list matches content: Shopify themes call the add-to-cart button's label loader-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-first reads over HTTP and starts a browser only when the run needs one. --fetch is a promise that the task never needs a browser, so a step that clicks, or a page that scripts build, ends the run blocked. With --fetch-first the 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-dollars and the call and time limits; an attempt that used one up ends budget_exceeded naming it, and one with an unpriced call under --max-dollars does 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 an escalate event. It cannot be combined with --fetch, --record, --attach or --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 neither fetch nor fetch_first.

  • A browserless read decodes a gzip or deflate response instead of refusing it. --fetch asked 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 run blocked as 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-Agent stops refusing it. --fetch sent no User-Agent, and a server that refuses a client that does not name itself answered 403, as Wikipedia does. The Rust reader now sends user-agent: fastbrowse/<version>.