diff --git a/tests/tools/test_browser_lightpanda_serve.py b/tests/tools/test_browser_lightpanda_serve.py index dc9676fa12..89a5ce9965 100644 --- a/tests/tools/test_browser_lightpanda_serve.py +++ b/tests/tools/test_browser_lightpanda_serve.py @@ -43,6 +43,7 @@ def _isolate(tmp_path, monkeypatch): # Never touch the developer's real ~/.local/bin/lightpanda. monkeypatch.setattr(lp, "_home_candidates", lambda: []) monkeypatch.setattr(lp, "_safe_start_time", lambda pid: 111) + monkeypatch.setattr(lp, "_supports_http_cache", True) with lp._servers_lock: lp._servers.clear() yield state @@ -142,6 +143,39 @@ class TestLaunch: assert Path(cache).is_dir() assert not list(Path(cache).glob("*.json")) # never confused with a session record + def test_no_http_cache_flag_on_old_binary(self, monkeypatch, _isolate): + monkeypatch.setattr(lp, "_supports_http_cache", False) + _, err, calls = self._launch(monkeypatch) + assert err is None + assert "--http-cache-dir" not in calls["argv"] + assert calls["argv"][-1] == "43111" + + def test_http_cache_probe_caches_and_detects_flag(self, monkeypatch, tmp_path): + exe = _exe(tmp_path / "lightpanda") + runs = [] + + class FakeRun: + def __init__(self, stdout): + self.stdout = stdout + + def fake_run(argv, **kwargs): + runs.append(argv) + return FakeRun("--http-cache-dir " if len(runs) == 1 else "") + + monkeypatch.setattr(lp, "_supports_http_cache", None) + monkeypatch.setattr(lp.subprocess, "run", fake_run) + assert lp._binary_supports_http_cache(str(exe)) is True + assert lp._binary_supports_http_cache(str(exe)) is True # cached, single probe + assert len(runs) == 1 + + monkeypatch.setattr(lp, "_supports_http_cache", None) + + def fake_run_old(argv, **kwargs): + return FakeRun("no such flag here") + + monkeypatch.setattr(lp.subprocess, "run", fake_run_old) + assert lp._binary_supports_http_cache(str(exe)) is False + def test_block_private_networks_flag(self, monkeypatch): _, err, calls = self._launch(monkeypatch, block_private_networks=True) assert err is None diff --git a/tools/browser_lightpanda.py b/tools/browser_lightpanda.py index 9d35d7ffe3..3c1bdf8d36 100644 --- a/tools/browser_lightpanda.py +++ b/tools/browser_lightpanda.py @@ -119,15 +119,43 @@ def _http_cache_dir() -> Path: """Filesystem HTTP cache shared by every Lightpanda this Hermes spawns. Shared rather than per-session so a cached asset survives session churn. - Lightpanda holds it in sqlite (WAL); a write that loses a race is a cache - miss, never a failed page load, and ``--http-cache-entry-limit`` (default - 1000) bounds it without Hermes managing eviction. + Lightpanda holds it in sqlite (WAL) with a best-effort write path, and + ``--http-cache-entry-limit`` (upstream default 1000, not passed here) + bounds it without Hermes managing eviction. """ path = _state_dir() / "http-cache" path.mkdir(parents=True, exist_ok=True) return path +_supports_http_cache: Optional[bool] = None + + +def _binary_supports_http_cache(binary: str) -> bool: + """True if ``lightpanda serve`` accepts ``--http-cache-dir``. + + The flag landed upstream in 0.3.x; older binaries fatally reject it + ("unknown argument"), which would break every launch. Probing ``help`` + output keeps working across future flag additions without parsing + versions, and is cached for the process lifetime. + """ + global _supports_http_cache + if _supports_http_cache is None: + try: + out = subprocess.run( + [binary, "help"], capture_output=True, text=True, timeout=10 + ) + _supports_http_cache = "--http-cache-dir" in out.stdout + except Exception as e: + logger.debug("lightpanda http-cache probe failed (%s); assuming no", e) + _supports_http_cache = False + if not _supports_http_cache: + logger.debug( + "lightpanda %s predates --http-cache-dir; serving without a cache", binary + ) + return _supports_http_cache + + def _record_path(session_name: str) -> Path: return _state_dir() / f"{session_name}.json" @@ -229,10 +257,9 @@ def launch_lightpanda( ) port = _pick_free_loopback_port() - argv = [ - binary, "serve", "--host", "127.0.0.1", "--port", str(port), - "--http-cache-dir", str(_http_cache_dir()), - ] + argv = [binary, "serve", "--host", "127.0.0.1", "--port", str(port)] + if _binary_supports_http_cache(binary): + argv += ["--http-cache-dir", str(_http_cache_dir())] if block_private_networks: argv.append("--block-private-networks") log_path = str(_state_dir() / f"{session_name}.log") diff --git a/website/docs/user-guide/features/browser.md b/website/docs/user-guide/features/browser.md index 33369e1ba7..97b5e4ca4a 100644 --- a/website/docs/user-guide/features/browser.md +++ b/website/docs/user-guide/features/browser.md @@ -455,7 +455,7 @@ AGENT_BROWSER_ENGINE=lightpanda The engine works with both browser drivers: -- **Browser Use mode (the default).** Hermes launches `lightpanda serve --host 127.0.0.1 --port ` itself — one process per `browser_exec` session name (or per task) — and points the Browser Use CLI at it. No Chromium, Playwright or Node.js is needed. The process is reaped after `browser.inactivity_timeout`, on exit, and by the orphan sweep if Hermes crashes. All of these processes share one on-disk HTTP cache at `$HERMES_HOME/cache/browser-use/lightpanda/http-cache`, so repeat visits skip re-downloading assets; delete that directory to clear it. Lightpanda has no graphical renderer, so `capture_screenshot()` is unavailable and the tool description tells the model to work text-first; it also holds one page per session, so the model is told to call `new_tab()` once and `goto_url()` afterwards (tracked upstream in [lightpanda-io/browser#1962](https://github.com/lightpanda-io/browser/issues/1962)). +- **Browser Use mode (the default).** Hermes launches `lightpanda serve --host 127.0.0.1 --port ` itself — one process per `browser_exec` session name (or per task) — and points the Browser Use CLI at it. No Chromium, Playwright or Node.js is needed. The process is reaped after `browser.inactivity_timeout`, on exit, and by the orphan sweep if Hermes crashes. All of these processes share one on-disk HTTP cache at `$HERMES_HOME/cache/browser-use/lightpanda/http-cache`, so repeat visits skip re-downloading assets. Hermes passes the cache flag only when the installed Lightpanda supports it (0.3.x+); older binaries simply run without a cache. To clear it, stop your Lightpanda sessions first, then delete that directory. Lightpanda has no graphical renderer, so `capture_screenshot()` is unavailable and the tool description tells the model to work text-first; it also holds one page per session, so the model is told to call `new_tab()` once and `goto_url()` afterwards (tracked upstream in [lightpanda-io/browser#1962](https://github.com/lightpanda-io/browser/issues/1962)). - **Built-in browser tools** (`/browser use off`). Hermes drives Lightpanda through `agent-browser --engine lightpanda` over CDP, the same way it drives local Chrome, with **automatic Chrome fallback**: Lightpanda handles the actions it supports (navigate, snapshot, click, type, scroll, back, press, eval) and Hermes transparently retries on Chrome for anything it doesn't. Screenshots and `browser_vision` are routed straight to Chrome. **When the engine is ignored.** `browser.engine` is the lowest-precedence browser setting: a cloud provider (including the Nous subscription browser — and on never-configured setups, any `BROWSERBASE_API_KEY` / `BROWSER_USE_API_KEY` in `~/.hermes/.env` auto-selects one), Camofox, a `browser.cdp_url` / `/browser connect` override, or `browser.use_real_profile` all take precedence. Picking Lightpanda in `hermes tools` writes `cloud_provider: local` for you; `/browser status` and `hermes doctor` report when the engine is configured but shadowed, and by what.