From 21b225ded40f9ab8280d66deb9ada0636e47e9fc Mon Sep 17 00:00:00 2001 From: ciregenz Date: Wed, 3 Jun 2026 00:50:23 -0700 Subject: [PATCH] [eric] browser: nudge the agent toward the fast captured-API tier once per host --- backend/apps/agents/browser/browser_agent.py | 16 +++++++++++++ backend/tests/test_browser_agent_loop.py | 25 ++++++++++++++++++++ frontend/src/shared/browserCommandHandler.ts | 15 +++++++++++- 3 files changed, 55 insertions(+), 1 deletion(-) diff --git a/backend/apps/agents/browser/browser_agent.py b/backend/apps/agents/browser/browser_agent.py index 06cb9943..d285ed30 100644 --- a/backend/apps/agents/browser/browser_agent.py +++ b/backend/apps/agents/browser/browser_agent.py @@ -328,6 +328,7 @@ async def run_browser_agent( recent_tool_calls: list[tuple[str, str, str]] = [] loop_trigger_count = 0 card_gone_streak = 0 # consecutive "card is gone" results -> fail fast, don't spin + route_hinted_hosts: set[str] = set() # surface the fast network tier once per host # Stagnation state: busy-but-stuck detection (no URL change + failures # across a run of actions), distinct from the exact-repeat loop above. @@ -1022,6 +1023,21 @@ async def run_browser_agent( session.browser_domains.append(domain) except Exception: pass + # The fast tier is ~0% used because the agent never thinks to ask. + # Once per host, when safe GET routes have been captured, nudge it: + # reading via the API beats re-scraping, especially in a batch loop. + try: + _rc = int(result.get("routes_available") or 0) + _rhost = browser_skills.host_of(result.get("url") or last_seen_url) + if _rc > 0 and _rhost and _rhost not in route_hinted_hosts: + route_hinted_hosts.add(_rhost) + content_blocks = content_blocks + [{"type": "text", "text": ( + f"\n\n💡 {_rc} of this site's own API endpoint(s) were captured. To READ " + "data (and especially to repeat a read for many items), BrowserReplayRoute " + "(or a replay_route step in BrowserRepeatFlow) is much faster and more " + "reliable than navigating + scraping. See BrowserListRoutes.")}] + except Exception: + pass if is_loop: loop_trigger_count += 1 repeat_count = sum(1 for c in recent_tool_calls if c == call_key) diff --git a/backend/tests/test_browser_agent_loop.py b/backend/tests/test_browser_agent_loop.py index 31faf8f3..d1f05f53 100644 --- a/backend/tests/test_browser_agent_loop.py +++ b/backend/tests/test_browser_agent_loop.py @@ -902,6 +902,31 @@ def test_batch_replay_uses_the_fast_network_route_per_value(monkeypatch): assert any("u=ada" in u for u in routes) and any("u=grace" in u for u in routes) +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. + 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([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} + 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) + final_convo = json.dumps(primary.calls[-1]["messages"]) + assert final_convo.count("API endpoint(s) were captured") == 1 + + def test_browser_wait_routes_through_smart_wait_and_returns_early(monkeypatch): # BrowserWait must no longer be a blind sleep: it probes the page (evaluate) # and returns as soon as it's settled, well under the requested cap. diff --git a/frontend/src/shared/browserCommandHandler.ts b/frontend/src/shared/browserCommandHandler.ts index ee702485..ec2d596a 100644 --- a/frontend/src/shared/browserCommandHandler.ts +++ b/frontend/src/shared/browserCommandHandler.ts @@ -96,7 +96,20 @@ async function handleNavigate(wv: BrowserWebview, params: Record): } catch (err: any) { if (!err?.message?.includes('ERR_ABORTED')) throw err; } - return { text: `Navigated to ${url}`, url }; + // 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 }; } async function handleClick(wv: BrowserWebview, params: Record): Promise> {