From 4e320033ad74c3c88fd406996dd99efa406d1ced Mon Sep 17 00:00:00 2001 From: ciregenz Date: Sat, 6 Jun 2026 04:49:19 -0700 Subject: [PATCH] [eric] browser: prune abandoned-navigate detours at distill so recorded skills and route hints stay clean --- backend/apps/agents/browser/browser_skills.py | 18 ++++++++++- backend/tests/test_browser_skills.py | 32 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/backend/apps/agents/browser/browser_skills.py b/backend/apps/agents/browser/browser_skills.py index f024ac50..515f45e0 100644 --- a/backend/apps/agents/browser/browser_skills.py +++ b/backend/apps/agents/browser/browser_skills.py @@ -309,7 +309,23 @@ def distill_steps(action_log: list[dict]) -> list[dict]: productive_count += 1 if productive_count == 0: return [] - return steps + return _prune_detours(steps) + + +def _prune_detours(steps: list[dict]) -> list[dict]: + """Drop an abandoned-page detour: a BrowserNavigate whose page was never + acted on because the very next step navigates somewhere else. Conservative + on purpose, only consecutive navigates qualify (if a page had been used, + a Type/Click/etc would sit between them), so a needed step is never removed. + This keeps a wrong-turn (e.g. the wrong profile, then the right one) out of + a recorded macro without any reachability guesswork.""" + out: list[dict] = [] + for i, s in enumerate(steps): + nxt = steps[i + 1] if i + 1 < len(steps) else None + if s.get("tool") == "BrowserNavigate" and nxt is not None and nxt.get("tool") == "BrowserNavigate": + continue # this navigate's page was abandoned immediately; skip it + out.append(s) + return out def first_unsafe_step(steps: list[dict]) -> tuple[int, str]: diff --git a/backend/tests/test_browser_skills.py b/backend/tests/test_browser_skills.py index 8cc532a2..95a2eecf 100644 --- a/backend/tests/test_browser_skills.py +++ b/backend/tests/test_browser_skills.py @@ -665,3 +665,35 @@ def test_route_hint_adoption_matching(_isolated_skills): # the Message and Send clicks did not run assert adopted[0] and adopted[1] and adopted[3] assert not adopted[2] and not adopted[4] + + +# --- conservative detour pruning --------------------------------------------- +def test_distill_prunes_abandoned_navigate_detour(): + # wrong profile opened (navigate), abandoned for a search (navigate), then + # the right profile + the real productive steps. The first navigate is a + # detour: nothing acted on its page before the next navigate. + log = [ + {"tool": "BrowserNavigate", "input": {"url": "https://x.com/in/wrong"}, "ok": True}, + {"tool": "BrowserNavigate", "input": {"url": "https://x.com/search?q=tyler"}, "ok": True}, + {"tool": "BrowserClickIndex", "input": {"index": 1}, "ok": True, + "clicked_role": "link", "clicked_name": "Tyler Chen"}, + {"tool": "BrowserType", "input": {"selector": "#msg", "text": "hi"}, "ok": True}, + ] + steps = sk.distill_steps(log) + urls = [s["params"].get("url") for s in steps if s["tool"] == "BrowserNavigate"] + assert urls == ["https://x.com/search?q=tyler"] # the abandoned wrong nav dropped + assert [s["tool"] for s in steps] == ["BrowserNavigate", "BrowserClickByName", "BrowserType"] + + +def test_distill_keeps_navigate_that_was_acted_on(): + # a navigate followed by a real action on that page is NOT a detour. + log = [ + {"tool": "BrowserNavigate", "input": {"url": "https://x.com/form"}, "ok": True}, + {"tool": "BrowserType", "input": {"selector": "#q", "text": "hi"}, "ok": True}, + {"tool": "BrowserNavigate", "input": {"url": "https://x.com/results"}, "ok": True}, + {"tool": "BrowserClickIndex", "input": {"index": 2}, "ok": True, + "clicked_role": "button", "clicked_name": "Go"}, + ] + steps = sk.distill_steps(log) + urls = [s["params"].get("url") for s in steps if s["tool"] == "BrowserNavigate"] + assert urls == ["https://x.com/form", "https://x.com/results"] # both kept