[eric] browser: agent writes a plain human final message (no UI jargon/OUTCOME mechanics); delete the leaky send-ready advisory, dead prefix flag, and humanize aux experiment

This commit is contained in:
ciregenz
2026-06-08 00:09:50 -07:00
parent ce8e74b96a
commit f8c812353e
3 changed files with 19 additions and 60 deletions
+6 -45
View File
@@ -43,17 +43,6 @@ from backend.apps.agents.browser.browser_loop import (
)
from backend.apps.agents.browser.browser_validator import adjudicate_stuck
# Send-skill PREFIX replay: replay the learned steps UP TO the irreversible Send
# mechanically, then hand the gated Send to the live model (the Send is NEVER
# replayed). Unlocked by first_unsafe_step now using is_replay_boundary, a
# composer OPENER ("Message"/"DM" click) is reversible and no longer ends the
# prefix (the r93/r94 blocker), so a clean skill replays [open composer ...] and
# only the real Send crosses to the live agent. A still-brittle recorded name
# self-heals: the prefix step fails, the skill quarantines, and the run falls
# back to the full agent (send still verified). Both fixes that parked it
# (settle-before-step, detour-pruning) already landed.
_PREFIX_REPLAY_ENABLED = True
# Single actions the model could have folded into one BrowserBatch turn;
# reads, waits, and the batch tools themselves don't count toward the streak.
_BATCHABLE_ACTION_TOOLS = {
@@ -207,12 +196,6 @@ def _delta_state(text: str, seen_lines: set[str]) -> str:
)
# A button row whose name is exactly a Send control (not "Send InMail credit" or
# "Send a message to X"); used to hand the model the Send button after it types,
# so it never burns turns hunting a button that's right there.
_SEND_ROW_RE = re.compile(r'\[(\d+)\]\*?<\s*button\s+"([^"]*)"', re.I)
def _is_composer_fill(tool_name: str, tool_input: dict) -> bool:
"""True if this action typed a message into a composer (the moment the Send
button is about to matter). Covers the solo fill, BrowserType, and a batched
@@ -228,16 +211,6 @@ def _is_composer_fill(tool_name: str, tool_input: dict) -> bool:
return False
def _send_index_in_state(state_text: str):
"""(index, name) of a real Send button in an interactives list, or None.
Strict exact match so it never grabs an upsell or a profile 'Send a message' link."""
for line in (state_text or "").splitlines():
m = _SEND_ROW_RE.search(line)
if m and m.group(2).strip().lower() in ("send", "send now", "send message"):
return int(m.group(1)), m.group(2)
return None
async def _post_action_state(
tool_name: str, tool_input: dict, result: dict,
browser_id: str, tab_id: str, wait_exec, goal: str,
@@ -276,16 +249,7 @@ async def _post_action_state(
if not isinstance(lst, dict) or "error" in lst or not lst.get("text"):
return ""
state = lst["text"] if seen_lines is None else _delta_state(lst["text"], seen_lines)
out = f"\n\n{PAGE_STATE_MARKER}\n{_truncate_state(state)}"
# Hand the Send button over so the model clicks it instead of hunting via CSS/JS.
if _composer_fill:
_si = _send_index_in_state(lst["text"])
if _si:
out = (f"\n\n[send-ready] Your message is typed and the Send button is index "
f"{_si[0]} below. To deliver, click it SOLO with BrowserClickIndex + an "
f"`expect` proof. Do NOT hunt for it with CSS/JS/screenshots, it is right here."
) + out
return out
return f"\n\n{PAGE_STATE_MARKER}\n{_truncate_state(state)}"
async def _request_browser_approval(
@@ -738,7 +702,7 @@ async def run_browser_agent(
# always run the live agent, which confirms before anything outward.
unsafe_i, why = browser_skills.first_unsafe_step(steps)
if unsafe_i >= 0:
if not (allow_prefix and _PREFIX_REPLAY_ENABLED and unsafe_i >= 1):
if not (allow_prefix and unsafe_i >= 1):
logger.info(f"[browser-skills] skill on {host} not replayed: {why}; running the full agent so the send is confirmed")
return None
prefix = steps[:unsafe_i]
@@ -1122,7 +1086,7 @@ async def run_browser_agent(
if send_confirmed:
_proof = next((str(a.get("result_summary") or "") for a in reversed(action_log)
if a.get("ok") and "Confirmed:" in str(a.get("result_summary") or "")), "")
text_parts = ["OUTCOME: DONE - the action was completed on the page. " + _proof[:160]]
text_parts = ["OUTCOME: DONE - " + (_proof[:160] or "the task completed and was confirmed on the page.")]
break
else:
perception_stall = 0
@@ -1477,12 +1441,9 @@ async def run_browser_agent(
for r in (result.get("results") or []))
if _send_click:
send_confirmed = True
result["text"] = (f"{result.get('text') or ''}\n\n[done] You clicked a "
"send control and it ran cleanly, the message is sent (the composer "
"clears on send; the sent text can render late in the thread, so do "
"NOT go hunting for it to 'confirm'). The task is complete. Your NEXT "
"message must be ONLY the 'OUTCOME: DONE - <result>' line with NO tool "
"calls, no more screenshots, lists, or reads.")
result["text"] = (f"{result.get('text') or ''}\n\n[task complete] The send "
"went through (the composer cleared). Don't re-check it. Give your "
"final answer now, your OUTCOME line.")
action_log.append({
"tool": tu.name,
@@ -793,12 +793,15 @@ SYSTEM_PROMPT = (
"Don't use it for normal tool failures; try a different approach first.\n\n"
"Complete the task autonomously. Your FINAL message MUST begin with exactly one "
"line: 'OUTCOME: DONE - <the verifiable result>' or 'OUTCOME: NOT DONE - <what is "
"missing and why>'. The parent agent decides whether to re-dispatch another browser "
"from that line alone, so a vague or missing outcome line costs a whole redundant "
"agent run. For irreversible actions, DONE requires the proof you observed: the exact "
"thing now on the page that confirms it, and where or when you saw it. Keep the rest "
"of the summary brief."
"line: 'OUTCOME: DONE - <result>' or 'OUTCOME: NOT DONE - <what is missing and why>'. "
"This line is read by another agent to decide what happens next, so it must be there, "
"but write the <result> itself the way you'd tell the PERSON who asked: a plain, "
"friendly sentence about what got done and the proof you saw (the name, the time). A "
"real human reads this, so NEVER use UI jargon (composer, textbox, element index, "
"'value now empty', coordinates) and don't repeat any bracketed system notes you were "
"shown; say it like a helpful assistant would. For irreversible actions, DONE requires "
"the proof you observed: the exact thing now on the page that confirms it, and where or "
"when you saw it. One sentence is ideal; never more than two."
)
MAX_TURNS = 40
+4 -9
View File
@@ -1427,9 +1427,10 @@ def test_message_pairing_validator_catches_both_orphan_and_dangling():
{"role": "assistant", "content": "done"}]) is True
def test_composer_fill_detection_and_send_handoff():
from backend.apps.agents.browser.browser_agent import _is_composer_fill, _send_index_in_state
# a composer fill is detected across the three ways the model types
def test_composer_fill_detection():
# detecting a composer fill is what arms the post-type wait for the Send button
# to render before we re-list (so the model sees it instead of hunting)
from backend.apps.agents.browser.browser_agent import _is_composer_fill
assert _is_composer_fill("BrowserClickIndex", {"index": 4, "text": "hello world"})
assert _is_composer_fill("BrowserType", {"selector": "#m", "text": "hi"})
assert _is_composer_fill("BrowserBatch", {"actions": [
@@ -1437,12 +1438,6 @@ def test_composer_fill_detection_and_send_handoff():
# a plain click (no text) is NOT a fill
assert not _is_composer_fill("BrowserClickIndex", {"index": 4})
assert not _is_composer_fill("BrowserScroll", {})
# the real Send button is handed over; upsells / profile links are never mistaken for it
page = '[1]<link "Tyler Chen">\n[33]<textbox "Write a message">\n[44]<button "Send">'
assert _send_index_in_state(page) == (44, "Send")
assert _send_index_in_state('[12]<button "Send InMail credit">') is None
assert _send_index_in_state('[5]<button "Send a message to Maya">') is None
assert _send_index_in_state("") is None
def test_strip_lone_surrogates():