diff --git a/backend/apps/agents/agent_manager.py b/backend/apps/agents/agent_manager.py index 1e12f466..8476f9d8 100644 --- a/backend/apps/agents/agent_manager.py +++ b/backend/apps/agents/agent_manager.py @@ -3376,9 +3376,14 @@ class AgentManager: if not text: _fp_path = "read->browser" + _entry = browser_fast_path.entry_url_from_brief(brief) + if _entry: + logger.info(f"[browser-cold] brief entry url for {session_id}: {_entry}") + async def _dispatch(task_text: str) -> dict: results = await run_browser_agents( - tasks=[{"task": task_text, "browser_id": selected[0] if selected else "", "url": ""}], + tasks=[{"task": task_text, "browser_id": selected[0] if selected else "", + "url": "", "entry_url": _entry}], model=session.model, dashboard_id=session.dashboard_id, pre_selected_browser_ids=selected, diff --git a/backend/apps/agents/browser/browser_fast_path.py b/backend/apps/agents/browser/browser_fast_path.py index 49e2c417..38419609 100644 --- a/backend/apps/agents/browser/browser_fast_path.py +++ b/backend/apps/agents/browser/browser_fast_path.py @@ -105,6 +105,17 @@ def _parse_verdict_and_brief(text: str) -> tuple[str, str]: return verdict, brief[:700] +_ENTRY_RE = re.compile(r"^\s*ENTRY:\s*(https?://\S+)", re.I | re.M) + + +def entry_url_from_brief(brief: str) -> str: + """The brief's ENTRY deep URL, or ''. Powers dispatch pre-navigation: a NEW + card opens directly on it instead of google, killing the orient+navigate + turns; a REUSED card is never moved (its deeper live state wins).""" + m = _ENTRY_RE.search(brief or "") + return m.group(1).rstrip(".,;)") if m else "" + + def compose_task(prompt: str, brief: str) -> str: """User's words first and authoritative; the brief is advisory routing. Skill replay keys on the parent's user message, so brief variance is safe.""" diff --git a/backend/tests/test_browser_fast_path.py b/backend/tests/test_browser_fast_path.py index f6895efb..bdbdf5fa 100644 --- a/backend/tests/test_browser_fast_path.py +++ b/backend/tests/test_browser_fast_path.py @@ -139,3 +139,39 @@ def test_send_probe_replies_are_honest(): assert "did NOT send it again" in a and "r46-os" in a u = unverifiable_reply("[test] hi r46-os", "browser became unresponsive") assert "not retrying" in u.lower() and "r46-os" in u + + +def test_entry_url_extracted_from_brief(): + from backend.apps.agents.browser.browser_fast_path import entry_url_from_brief + brief = ( + "ENTRY: https://www.linkedin.com/search/results/people/?keywords=tyler%20chen\n" + "1. Open the first matching profile\n2. Click Message\n3. Type the text" + ) + assert entry_url_from_brief(brief) == ( + "https://www.linkedin.com/search/results/people/?keywords=tyler%20chen" + ) + # case-insensitive, mid-brief, trailing punctuation stripped + assert entry_url_from_brief("steps...\nentry: https://news.ycombinator.com/.") == "https://news.ycombinator.com/" + assert entry_url_from_brief("no entry line here") == "" + assert entry_url_from_brief("") == "" + # never a non-http scheme + assert entry_url_from_brief("ENTRY: javascript:alert(1)") == "" + + +def test_results_url_shapes(): + from backend.apps.agents.browser.browser_agent import _RESULTS_URL_RE + hits = [ + "https://www.linkedin.com/search/results/people/?keywords=tyler+chen", + "https://www.google.com/search?q=anything", + "https://www.reddit.com/search/?q=cats", + "https://example.com/find?term=x", + ] + misses = [ + "https://www.linkedin.com/in/tylerchen1200/", + "https://news.ycombinator.com/", + "https://www.linkedin.com/messaging/thread/abc123/", + ] + for u in hits: + assert _RESULTS_URL_RE.search(u), u + for u in misses: + assert not _RESULTS_URL_RE.search(u), u