mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-11 12:17:45 +02:00
[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:
@@ -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
|
||||
|
||||
@@ -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():
|
||||
|
||||
Reference in New Issue
Block a user