[eric] browser: a clean success needs an explicit Done; recognize gather asks, steer them to BrowserExtract

This commit is contained in:
ciregenz
2026-06-08 22:37:33 -07:00
parent 91073becec
commit 07f911d200
4 changed files with 41 additions and 5 deletions
+5 -3
View File
@@ -2038,9 +2038,11 @@ async def run_browser_agent(
"browser_id": browser_id,
"summary": summary,
# structured success signal the parent reads (replaces grepping the
# summary for a tag): true only if the run is honest AND, when the
# model called Done, it reported success. No-Done runs lean on honest.
"done": honest and (done_success if done_called else True),
# summary for a tag): true ONLY when the model explicitly finished via
# Done with success AND the honesty gate agreed. A run that just stopped
# (max turns, gave up, never called Done) is NOT a clean success, so the
# fast path recovers instead of shipping a silent half-finish.
"done": honest and done_called and done_success,
# surface the honest failure to the parent so it doesn't treat a
# did-nothing run as a success it can build on
**({} if honest else {"error": summary}),
+2 -1
View File
@@ -308,7 +308,8 @@ def replay_recheck_is_safe(action_log: list[dict]) -> bool:
# never replay (the answer must be fresh), an action ask can.
_INFO_ASK_RE = re.compile(
r"\b(tell me|what(?:'s| is| are)|how (?:many|much)|count|list|summari[sz]e|"
r"extract|find out|read (?:me|the)|get the|give me|which|who (?:is|are)|report back)\b",
r"extract|find (?:me|out)|show me|look up|read (?:me|the)|get the|give me|which|"
r"who (?:is|are)|report back|most (?:viewed|popular|liked|recent|rated|watched)|top \d+)\b",
re.I,
)
_ACTION_ASK_RE = re.compile(
@@ -816,7 +816,15 @@ SYSTEM_PROMPT = (
"- To pull SPECIFIC FIELDS off a page (names + links from results, prices, table "
"rows), call BrowserExtract with what you want; a helper model reads the page and "
"hands you just the JSON. One call replaces BrowserGetText plus you reading 15k "
"chars, so prefer it whenever you know the fields you're after.\n\n"
"chars, so prefer it whenever you know the fields you're after.\n"
"- For ANY 'find me / list / get all / top N / most-viewed' gathering task, "
"BrowserExtract IS your tool: state the fields and let the helper read the rendered "
"page. Do NOT hand-roll CSS/DOM selectors in BrowserEvaluate to scrape a list, modern "
"sites (YouTube, LinkedIn, Amazon) obfuscate and lazy-render class names, so selector "
"scraping burns turns and silently returns the wrong nodes. If one BrowserEvaluate read "
"comes back empty or shaped wrong, STOP, that is your signal to switch to BrowserExtract, "
"not to debug another selector. The answer must end up IN your Done message, so gather it "
"for real; never report done on a gather task you couldn't actually read.\n\n"
"## When you genuinely cannot proceed\n"
"Use RequestHumanIntervention for:\n"
+25
View File
@@ -267,6 +267,21 @@ def test_done_tool_success_false_marks_not_done(monkeypatch):
assert "login wall" in result["summary"]
def test_run_that_never_calls_done_is_not_a_clean_success(monkeypatch):
# A run that does real work but stops with plain text (never calls Done) is a
# half-finish, not a clean success: done must be False so the fast path recovers
# instead of shipping a silent stop (the 'Task completed.' that wasn't).
BH._browser_history.clear(); BH._domain_notes.clear()
primary = FakeLLM([
Resp([_rp("click it"), _tu("BrowserClickIndex", index=3)]),
Resp([Blk("text", "I clicked the thing.")], stop_reason="end_turn"),
])
aux = FakeAux()
_install(monkeypatch, primary, aux)
result = asyncio.run(BA.run_browser_agent(task="open the settings page", browser_id="b1", model="sonnet"))
assert result.get("done") is False # no explicit Done -> not a clean success
def test_early_perception_is_not_cut_short_before_any_action(monkeypatch):
# Orienting on a cold/slow page can take several look-only turns; the stall
# backstop must NOT fire before the agent has done anything (it only bounds a
@@ -1402,6 +1417,16 @@ def test_informational_gate_strips_outcome_boilerplate_on_tie_break():
assert deliverable_is_informational(listy, "")
def test_find_me_and_most_viewed_asks_are_informational():
from backend.apps.agents.browser.browser_loop import deliverable_is_informational
# the exact MKBHD task that previously slipped past the gate ("find me" + "most viewed")
assert deliverable_is_informational("", "find me his 50 most viewed vids")
assert deliverable_is_informational("", "show me the top 10 trending repos")
assert deliverable_is_informational("", "look up the cheapest flight")
# a pure action ask still is not informational
assert not deliverable_is_informational("", "send Tyler a message saying hi")
def test_interstitial_dismiss_target_generalizable_and_safe():
from backend.apps.agents.browser.browser_loop import interstitial_dismiss_target
# a junk popup with a throwaway-dismiss control gets found (any site)