diff --git a/backend/tests/test_browser_agent_loop.py b/backend/tests/test_browser_agent_loop.py index d1f05f53..b9b86a2e 100644 --- a/backend/tests/test_browser_agent_loop.py +++ b/backend/tests/test_browser_agent_loop.py @@ -903,26 +903,27 @@ def test_batch_replay_uses_the_fast_network_route_per_value(monkeypatch): def test_captured_routes_are_surfaced_once_per_host(monkeypatch): - # Drives the dead network tier: when safe GET routes exist, the agent gets a - # ONE-TIME nudge per host toward BrowserReplayRoute, not on every navigation. + # Drives the dead network tier: when a READ shows safe GET routes were captured + # (sampled on get_text, after the SPA's XHRs fired, not on navigate), the agent + # gets a ONE-TIME nudge per host toward BrowserReplayRoute, not on every read. BH._browser_history.clear() primary = FakeLLM([ - Resp([_rp("go 1"), _tu("BrowserNavigate", url="https://docs.google.com/a")]), - Resp([_rp("go 2"), _tu("BrowserNavigate", url="https://docs.google.com/b")]), + Resp([_rp("read 1"), _tu("BrowserGetText")]), + Resp([_rp("read 2"), _tu("BrowserGetText")]), Resp([Blk("text", "done")], stop_reason="end_turn"), ]) sent = _install(monkeypatch, primary, FakeAux()) orig = BA.ws_manager.send_browser_command async def _with_routes(request_id, action, browser_id, params, tab_id=""): - if action == "navigate": - return {"text": f"Navigated to {params.get('url')}", "url": params.get("url"), "routes_available": 4} + if action == "get_text": + return {"text": "page content", "url": DOC_URL, "routes_available": 4} return await orig(request_id, action, browser_id, params, tab_id) monkeypatch.setattr(BA.ws_manager, "send_browser_command", _with_routes, raising=False) asyncio.run(BA.run_browser_agent(task="browse", browser_id="b1", model="sonnet", initial_url=DOC_URL)) # messages are cumulative across calls, so count within ONE call's full - # conversation: the nudge must appear exactly once for docs.google.com (not per nav) + # conversation: the nudge must appear exactly once for docs.google.com (not per read) final_convo = json.dumps(primary.calls[-1]["messages"]) assert final_convo.count("API endpoint(s) were captured") == 1 diff --git a/frontend/src/shared/browserCommandHandler.ts b/frontend/src/shared/browserCommandHandler.ts index ec2d596a..8e06df51 100644 --- a/frontend/src/shared/browserCommandHandler.ts +++ b/frontend/src/shared/browserCommandHandler.ts @@ -80,11 +80,28 @@ async function handleScreenshot(wv: BrowserWebview): Promise return { error: `Screenshot failed after retries: ${lastErr?.message || String(lastErr)}` }; } +// Count the safe (GET) API endpoints captured for this site so the backend can +// nudge the agent toward the fast network path. Best-effort, never throws. +async function countSafeRoutes(wv: BrowserWebview): Promise { + try { + const bridge = (window as any).openswarm?.cdpRoutesGet as + | ((id: number, origin?: string) => Promise) | undefined; + if (!bridge) return 0; + let origin = ''; + try { origin = new URL(wv.getURL()).origin; } catch {} + const routes = (await bridge(wv.getWebContentsId(), origin)) || []; + return routes.filter((r) => r && r.safe).length; + } catch { return 0; } +} + async function handleGetText(wv: BrowserWebview): Promise> { const text: string = await wv.executeJavaScript( 'document.body.innerText.substring(0, 15000)' ); - return { text, url: wv.getURL(), title: wv.getTitle() }; + // Sampled HERE (on a read), not on navigate: by the time the agent reads the + // page, the SPA's XHR/fetch have fired, so routes are actually captured. + const routes_available = await countSafeRoutes(wv); + return { text, url: wv.getURL(), title: wv.getTitle(), routes_available }; } async function handleNavigate(wv: BrowserWebview, params: Record): Promise> { @@ -96,20 +113,9 @@ async function handleNavigate(wv: BrowserWebview, params: Record): } catch (err: any) { if (!err?.message?.includes('ERR_ABORTED')) throw err; } - // Count the safe GET endpoints captured for this site so the backend can nudge - // the agent toward the fast network path (the audit found it's ~0% used). - let routesAvailable = 0; - try { - const bridge = (window as any).openswarm?.cdpRoutesGet as - | ((id: number, origin?: string) => Promise) | undefined; - if (bridge) { - let origin = ''; - try { origin = new URL(wv.getURL()).origin; } catch {} - const routes = (await bridge(wv.getWebContentsId(), origin)) || []; - routesAvailable = routes.filter((r) => r && r.safe).length; - } - } catch {} - return { text: `Navigated to ${url}`, url, routes_available: routesAvailable }; + // Route-count is sampled on the next READ (handleGetText), not here: at + // navigate-return the SPA's XHRs haven't fired yet, so this would always be ~0. + return { text: `Navigated to ${url}`, url }; } async function handleClick(wv: BrowserWebview, params: Record): Promise> {