mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-08-28 10:49:46 +02:00
[eric] browser: skill recording judges the task ask, OUTCOME boilerplate no longer blocks learning
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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, "")
|
||||
|
||||
Reference in New Issue
Block a user