diff --git a/backend/apps/agents/browser/browser_agent.py b/backend/apps/agents/browser/browser_agent.py index a0294799..36554b5a 100644 --- a/backend/apps/agents/browser/browser_agent.py +++ b/backend/apps/agents/browser/browser_agent.py @@ -799,7 +799,7 @@ async def run_browser_agent( "action_log": [], "final_screenshot": None, } - # A/B lever (flag-gated, default off): pin the browser LOOP to a fast-capable tier instead of inheriting the parent's frontier model. Measured 2026-07-03: Opus->Sonnet is ~2x/turn on a gen-bound turn and mechanical browsing rarely needs frontier reasoning. Provider-agnostic via resolve_aux_model's tier map; any failure falls back to the inherited model so the flag can never break a run. + # A/B lever (flag-gated, default off): pin the browser LOOP to a fast tier instead of inheriting the parent's frontier model, since mechanical browsing rarely needs frontier reasoning. RE-MEASURED 2026-07-15 (live cc/ lanes, n=10 browser-shaped tool turns): the old "Opus->Sonnet ~2x" is STALE, opus-4-6 ~= sonnet-4-6 now (~2.3s, ratio 1.08x); the real ~2x/turn lever is HAIKU (1.2s vs 2.4s, complete distribution separation, tool_use 10/10). So set OPENSWARM_BROWSER_LOOP_TIER=haiku for the speedup, not sonnet. Provider-agnostic via resolve_aux_model's tier map; any failure falls back to the inherited model so the flag can never break a run. p_loop_tier = os.environ.get("OPENSWARM_BROWSER_LOOP_TIER", "").strip().lower() if p_loop_tier in ("haiku", "sonnet"): try: diff --git a/backend/apps/agents/browser/seed_for.py b/backend/apps/agents/browser/seed_for.py index 728e259d..58c6c719 100644 --- a/backend/apps/agents/browser/seed_for.py +++ b/backend/apps/agents/browser/seed_for.py @@ -89,6 +89,7 @@ SEED_PLAYBOOKS: dict[str, list[str]] = { ], "x.com": [ "Most actions need sign-in. Once logged in, search via URL: x.com/search?q=QUERY (twitter.com redirects here).", + "To post: click the composer ('What is happening?'), type, then click Post; do NOT press Enter (it inserts a newline). To reply, open the tweet and use its Reply box then the Reply button.", ], "quora.com": [ "Search via URL: quora.com/search?q=QUERY. Reading often triggers a sign-in wall after a bit of scrolling.", @@ -111,6 +112,44 @@ SEED_PLAYBOOKS: dict[str, list[str]] = { "figma.com": [ "The landing page is marketing; the app needs sign-in. If not signed in, use RequestHumanIntervention.", ], + "youtube.com": [ + "Search via URL: youtube.com/results?search_query=QUERY. A video is youtube.com/watch?v=ID; browsing and watching work logged-out.", + "To comment: open the video, click the 'Add a comment...' box, type, then click Comment (needs sign-in).", + ], + "reddit.com": [ + "Search via URL: reddit.com/search/?q=QUERY. A subreddit is reddit.com/r/NAME; most browsing works logged-out.", + "Posting/commenting needs sign-in and the composer is bot-gated; prefer the built-in write path (BrowserApiWrite) over driving the UI composer.", + ], + "wikipedia.org": [ + "Read via URL: en.wikipedia.org/wiki/TITLE (spaces become _). Search via en.wikipedia.org/w/index.php?search=QUERY. Fully readable logged-out.", + ], + "mail.google.com": [ + "Needs sign-in. Search via URL: mail.google.com/mail/u/0/#search/QUERY. To send: click Compose, fill To then Subject then the body, then click Send.", + ], + "imdb.com": [ + "A title lives at imdb.com/title/ttID. Search via URL: imdb.com/find/?q=QUERY. Readable logged-out.", + ], + "stackoverflow.com": [ + "Search via URL: stackoverflow.com/search?q=QUERY. Questions and answers are readable logged-out; voting/answering needs sign-in.", + ], + "indeed.com": [ + "Job search via URL: indeed.com/jobs?q=WHAT&l=WHERE. Browsing works logged-out; applying needs sign-in and is heavily bot-gated (expect a captcha).", + ], + "zillow.com": [ + "Search via URL: zillow.com/homes/CITY-STATE_rb/. Heavy anti-bot: a press-and-hold or captcha wall is common on more than a few requests.", + ], + "news.ycombinator.com": [ + "The front page is news.ycombinator.com; an item (post + comments) is news.ycombinator.com/item?id=ID. Fully readable logged-out; commenting/voting needs sign-in.", + ], + "medium.com": [ + "Articles are readable but many hit a metered paywall after a few reads; search via URL: medium.com/search?q=QUERY.", + ], + "notion.so": [ + "The landing page is marketing; the workspace needs sign-in. If not signed in, use RequestHumanIntervention.", + ], + "glassdoor.com": [ + "Heavy sign-in and anti-bot walls appear quickly; treat as often not reliably automatable, and use RequestHumanIntervention on a wall rather than fighting it.", + ], } diff --git a/backend/tests/test_browser_playbook.py b/backend/tests/test_browser_playbook.py index a399cd01..b4003d74 100644 --- a/backend/tests/test_browser_playbook.py +++ b/backend/tests/test_browser_playbook.py @@ -186,3 +186,21 @@ def test_seed_playbook_fallback_and_supersede(): pb.persist("github.com", ["learned: use the org filter"]) pb.CACHE.clear() assert pb.get_playbook("github.com") == ["learned: use the org filter"] + + +def test_broadened_seed_coverage_and_write_mechanics(): + from backend.apps.agents.browser.seed_for import SEED_PLAYBOOKS, seed_for + # the newly added high-traffic sites all resolve to non-empty guidance + for host in ("youtube.com", "reddit.com", "wikipedia.org", "mail.google.com", + "imdb.com", "stackoverflow.com", "indeed.com", "zillow.com", + "news.ycombinator.com", "medium.com", "notion.so", "glassdoor.com"): + assert seed_for(host), f"{host} should have a seed" + assert seed_for("www." + host) == seed_for(host) # www-strip still matches + # the popular WRITE targets carry a first-run write mechanic (the point of the seed for writes) + assert any("Post" in b for b in seed_for("x.com")) + assert any("Comment" in b for b in seed_for("youtube.com")) + assert any("Send" in b for b in seed_for("mail.google.com")) + # reddit steers first writes to the deterministic path, not the bot-gated UI composer + assert any("BrowserApiWrite" in b for b in seed_for("reddit.com")) + # no duplicate host keys slipped in (a dup would silently drop a site's guidance) + assert len(SEED_PLAYBOOKS) == len(set(SEED_PLAYBOOKS))