[eric] browser: skill recording judges the task ask, OUTCOME boilerplate no longer blocks learning

This commit is contained in:
ciregenz
2026-06-05 13:09:02 -07:00
parent 792ff7ecd5
commit e37ba6c66b
3 changed files with 54 additions and 6 deletions
+2 -1
View File
@@ -1419,7 +1419,8 @@ async def run_browser_agent(
# run was dishonest (ghost) OR when its answer was gathered/judged content
# (a list/report): replay can redo the clicks but not regenerate the
# judgment, so recording it would create a thin shortcut that later ghosts.
informational = deliverable_is_informational(summary)
informational = deliverable_is_informational(summary, skill_key_task)
logger.info(f"[browser-skills] record gate: honest={honest} informational={informational}")
if honest and not informational:
try:
rec_host = browser_skills.host_of(last_seen_url)
+29 -5
View File
@@ -8,6 +8,7 @@ prevents the model from burning the entire turn budget on a failing approach.
"""
import json
import re
# Tools that are read-only / idempotent and should NOT count toward loop
# detection. Repeating these is normal (scrolling through a feed, taking
@@ -247,17 +248,40 @@ def replay_recheck_is_safe(action_log: list[dict]) -> bool:
return not any(a.get("tool") in _REPLAY_DIRTYING_TOOLS for a in action_log)
def deliverable_is_informational(summary: str) -> bool:
# What the user ASKED FOR outranks how the sub narrated it: an info ask can
# 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",
re.I,
)
_ACTION_ASK_RE = re.compile(
r"\b(open|go to|navigate|click|send|post|submit|fill|type|search for|log ?in|"
r"sign ?in|upload|download|book|order|buy|add|create|delete|message|dm|text)\b",
re.I,
)
def deliverable_is_informational(summary: str, task: str = "") -> bool:
"""True if the run's final answer is GATHERED CONTENT (a list/report the model
extracted or judged), not a short action confirmation. A deterministic replay
reproduces clicks and navigations but CANNOT regenerate judged/collected
information, so recording a skill for such a run would make a thin shortcut
that replays the mechanical scaffolding and then falsely claims the whole task
is done (the 'find me 10 X' ghost). Tool counts can't separate this from a
legit search (measured: both look read-heavy), but the deliverable shape can.
Conservative + FAIL-SAFE: when in doubt we DON'T record, so the worst case is
a lost speedup (re-run via the LLM), never a ghost completion."""
is done (the 'find me 10 X' ghost). The task's ask decides when it's clear
(mixed asks count as informational); the summary's shape breaks ties, with the
mandatory OUTCOME line stripped first since boilerplate made every summary
look like a report and silently stopped all recording. Conservative +
FAIL-SAFE: when in doubt we DON'T record, so the worst case is a lost speedup
(re-run via the LLM), never a ghost completion."""
t = (task or "").strip()
if t:
if _INFO_ASK_RE.search(t):
return True
if _ACTION_ASK_RE.search(t):
return False
s = (summary or "").strip()
s = re.sub(r"OUTCOME:.*$", "", s, flags=re.S).strip()
if len(s) > 300:
return True
if s.count("\n") >= 2: # 3+ lines reads as a list/report, not a one-liner
+23
View File
@@ -1278,3 +1278,26 @@ def test_delta_state_reshuffle_resends_full():
_delta_state("\n".join(f'[{i}]<button "a{i}">' for i in range(1, 11)), seen)
new_page = "10 interactive elements:\n" + "\n".join(f'[{i}]<button "z{i}">' for i in range(1, 11))
assert _delta_state(new_page, seen) == new_page
def test_informational_gate_judges_the_task_ask_first():
from backend.apps.agents.browser.browser_loop import deliverable_is_informational
chatty = (
"The Wikipedia article on the Golden Gate Bridge is now open. The page has "
"loaded successfully with the full article content visible, including links "
"to related topics like suspension bridge, Golden Gate, and various related "
"articles.\n\nOUTCOME: DONE - opened the article at https://en.wikipedia.org/wiki/Golden_Gate_Bridge"
)
action_task = "go to wikipedia and search for golden gate bridge and open the article"
info_task = "go to hacker news and open the Ask section and tell me the title of the first question"
assert not deliverable_is_informational(chatty, action_task)
assert deliverable_is_informational("short answer", info_task)
assert deliverable_is_informational(chatty, "open the page and tell me how many rows it shows")
def test_informational_gate_strips_outcome_boilerplate_on_tie_break():
from backend.apps.agents.browser.browser_loop import deliverable_is_informational
short_action = "Sent.\n\nOUTCOME: DONE - bubble visible at 12:05 PM with the exact text, composer cleared and Send greyed out which proves delivery"
assert not deliverable_is_informational(short_action, "")
listy = "Found these:\n- a\n- b\n- c"
assert deliverable_is_informational(listy, "")