Repository navigation
Build Demo Applied Analysis
When I first ran browser-use, every run failed at the step where the agent asks the AI model what to do, and each provider failed in a different way. browser-use's own model service returned 403 because free-tier keys aren't allowed to use it. DeepSeek returned 402 Insufficient Balance. Gemini returned 503 ("high demand"), then 429 once I went over the free tier's limit of five requests per minute. The gemini-2.5 models returned 404 because Google has retired them, even though browser-use still lists them in VerifiedGeminiModels in browser_use/llm/google/chat.py. Analysis means investigating this before deciding anything about the code. How can a call to a model provider fail, and what does each failure mean for a task that's already halfway through? The most important finding is that these failures come in two kinds. A 503 or 429 usually clears up within seconds. A 402, 403 or 404 keeps failing the same way until a person adds credit, changes plans, or picks a different model. The logs show why this matters: with DeepSeek, the agent tried six times on a call that could never succeed. Design means deciding what the software should do about those findings and which class is responsible for it. browser-use's authors made two decisions I can point to. First, each provider's adapter converts that provider's own errors into one shared ModelProviderError with a status_code (browser_use/llm/exceptions.py), so the Agent never needs to know which provider it's talking to. Second, the Agent handles recovery. _try_switch_to_fallback_llm in browser_use/agent/service.py switches to an optional backup model, fallback_llm, when the status code is in {401, 402, 429, 500, 502, 503, 504}, and keeps using it for the rest of the run. The design decision I would make is to put the two kinds of failure from my analysis into the error class itself, as an is_transient property on ModelProviderError. The error object already holds the status code, so by the GRASP Information Expert principle it is the right object to know whether that code is temporary. The Agent would then retry the same model only on 429 and 5xx errors, and would switch to the backup or stop immediately on 401 through 404. Working through that decision also exposed a bug. DeepSeek's adapter creates the error as ModelProviderError(str(e), model=self.name) without passing the real status code (browser_use/llm/deepseek/chat.py:201). So the 402 I received reached the Agent as the default 502, and no rule based on the status code could have classified it correctly.
| Domain concept | Where it lives in the code | How close is the match? |
|---|---|---|
| Step | Split across four classes in browser_use/agent/views.py: AgentStepInfo (step number and step limit, line 279), AgentOutput (evaluation, memory, next goal and the list of actions, line 388), StepMetadata (start and end time, line 362), and AgentHistory (line 488), which bundles the model's output, the action results and a browser snapshot for one finished step |
Split. Every piece of the domain Step exists, but no single class is "a step." AgentHistory comes closest, and only after the step has finished. The step number is stored twice, in AgentStepInfo and StepMetadata. |
| AI Model |
BaseChatModel in browser_use/llm/base.py (line 33), with one class per provider (ChatGoogle, ChatDeepSeek, ...) |
Close, except one missing attribute. Provider and name match (provider, model). canSeeScreenshots isn't on the model at all. Instead, the Agent constructor in browser_use/agent/service.py checks whether the model's name contains "deepseek", "grok-3" or "grok-code", and turns screenshots off if so (lines 476–485). The code's own comment on line 474 says # TODO: move this logic to the LLMs. |
| Task | Not a class. It's the task: str argument to Agent (browser_use/agent/service.py, line 138), saved as self.task. Status isn't stored anywhere. It's worked out afterwards from the last action's result (is_done() and is_successful() in views.py, lines 727 and 734), plus the paused and stopped flags on AgentState. |
Poor: buried inside the Agent. In the domain, Task is a real thing with instructions, a status, a primary and backup model, and an answer. In the code, it's one line of text inside the object doing the work, and its status is spread across three places. |
browser-use's core library is in Construction, with Transition happening alongside it, because its Elaboration already took place in mid-2025. That's when the architecture described in my earlier parts was built: the project switched to the bubus event bus on 2025-06-18, added its first watchdog service on 2025-07-31, and made 1,340 commits between version 0.5.0 (July 7, 2025) and 0.7.0 (August 27, 2025). Since then the architecture has stayed put and the work has been filling it in. Of the commits from the last 90 days that carry a type prefix, 96 are fixes, 3 are features and 2 are refactors. The folder that changed most is browser_use/llm/, where providers like OrcaRouter get added behind the existing BaseChatModel interface without changing the Agent, which is lower-risk functionality built out on a stable core. At the same time the project ships to real users constantly, which is Transition work: 13 tagged releases between May 23 and September 3, 2026, release candidates before 0.13.0, and dependency upgrades made specifically to clear security advisories (one commit clears 10 CVEs). It isn't in one phase overall, though. The newer command-line tool for coding assistants (cli.py, skill_cli/ and skills/, shipped alongside a separate Browser Harness package still at version 0.1.x) first appears in the commit history in June 2026, and it is still retiring commands and moving its docs to "CLI 3.0." That looks more like Elaboration. So the core library is in Construction and Transition, while one newer part of the project is still in Elaboration.
A single iteration is a release cycle of two to three weeks, made up of many smaller loops that each go from issue to released code in a few days. Issue #5598 is a typical loop. It was opened on 2026-08-29 by linear[bot], which suggests the team tracks work in Linear and syncs it to GitHub. A reviewer had noticed that PR #5451, which added OrcaRouter by copying ChatOpenRouter, fixed three bugs in the copy that were never fixed in the original. The same day, Magnus Müller opened PR #5599 from a branch named after the issue, agency/fix-openrouter-5598. The PR description has a Summary section and a Tests section listing the exact pytest commands run against tests/ci/models/test_llm_openrouter.py, and the test.yaml and lint.yml workflows run automatically on every pull request. After 6 commits and 2 review comments it was merged on September 1. Two days later it went out in 0.13.9, one of about 75 commits since 0.13.8 on August 16, most of them provider fixes, tests and docs. Publishing that GitHub release triggers publish.yml, which uploads the new version to PyPI. Each loop contains every UP discipline in miniature: analysis in the issue write-up, design and implementation in the PR, testing in tests/ci, and deployment in the release. The biggest constraint on an iteration is the backlog: there are currently 127 open issues and 283 open pull requests, so deciding what goes into each release is as much work as writing the code.