[eric] browser: verified-action executor foundation = generic site-agnostic effect verification (did the action produce the SPECIFIC expected effect: url_changed/appeared/gone/filled/cleared), zero per-site code; wired into the send-script's receipt (proves it on the real flow, behavior-identical to the old inline check via an equivalence test) so it's the executor's verification core, not dead scaffolding

This commit is contained in:
ciregenz
2026-07-07 18:00:35 -07:00
parent c423517a9f
commit 1e6dd84495
3 changed files with 124 additions and 2 deletions
@@ -19,6 +19,8 @@ import re
import time
from typing import Awaitable, Callable
from backend.apps.agents.browser import browser_verified_action
logger = logging.getLogger(__name__)
# Double quotes are unambiguous. Single quotes only delimit when the opener is at a word boundary (start/space/colon), so an in-word apostrophe like "chen's" is never mistaken for a payload quote, that mispairing was silently corrupting the canonical "text him '...'" errand.
@@ -196,12 +198,15 @@ async def run_send_script(
if not send_ok:
logger.info(f"[browser-sendscript] send click errored ({send_name}); handing to model (fill committed, NOT sent)")
return None
# 4. two-sided receipt: composer seen cleared of the payload
# 4. two-sided receipt via the GENERIC verifier: the send is confirmed only when
# the composer clears the payload ("cleared:<payload>"). Same verdict as the old
# inline check, now expressed as the site-agnostic expectation a verified-action
# executor uses everywhere, so this proven flow IS the executor's verification core.
cleared = False
for wait_s in (0.4, 1.0, 1.6):
await asyncio.sleep(wait_s)
state3 = await fresh_list()
if state3 and not payload_in_textbox(state3, payload):
if state3 and browser_verified_action.expectation_met(f"cleared:{payload}", state2, state3):
cleared = True
break
note = ("" if cleared else
@@ -0,0 +1,72 @@
"""Generic, site-agnostic verification: did an action produce the SPECIFIC effect
it was meant to? This is the load-bearing piece that lets a verified-action executor
work on any website without per-site code, the model (or a scripted flow) names a
generic expectation, and this checks it against a cheap before/after page snapshot.
A snapshot is just the interactives-list text plus the URL, the same things every
site exposes, so nothing here knows about LinkedIn or any particular page. It
generalizes the send-script's proven two-sided receipt ("the composer cleared") from
one hand-tuned flow into one predicate ("cleared:<text>") reusable everywhere.
Expectations (kind, or "kind:arg"):
url_changed the page navigated
changed the page changed at all (weakest; a fallback)
appeared:X X is present now but wasn't before (a menu/dialog/result opened)
gone:X X was present before but isn't now (an item/row deleted)
filled:X some textbox value now carries X (a fill committed)
cleared:X no textbox value carries X (a composer sent + emptied)
"""
import re
from typing import Tuple
# Match payload_in_textbox: long values truncate in the list, so compare on a prefix.
P_VALUE_PREFIX_LEN = 24
P_TEXTBOX_LINE = "<textbox"
def parse_expectation(expect: str) -> Tuple[str, str]:
"""(kind, arg) from 'kind' or 'kind:arg'. Unknown kinds are returned as-is and
treated as unmet by expectation_met, so a typo fails safe (verification withheld)."""
raw = (expect or "").strip()
if ":" in raw:
kind, arg = raw.split(":", 1)
return kind.strip().lower(), arg.strip()
return raw.lower(), ""
def value_present(state_text: str, sub: str) -> bool:
"""True if any listed textbox VALUE carries sub (prefix match, like a committed
fill). Same logic as payload_in_textbox so the send-script stays behavior-identical."""
probe = (sub or "")[:P_VALUE_PREFIX_LEN]
if not probe:
return False
return any(P_TEXTBOX_LINE in line and probe in line
for line in (state_text or "").splitlines())
def p_contains(state_text: str, sub: str) -> bool:
s = (sub or "").strip().lower()
return bool(s) and s in (state_text or "").lower()
def expectation_met(
expect: str, before: str, after: str,
before_url: str = "", after_url: str = "",
) -> bool:
"""Did `after` satisfy `expect` given `before`? Pure; unknown expectation = False
(fail safe: verification withheld rather than a false pass)."""
kind, arg = parse_expectation(expect)
if kind == "url_changed":
return bool(after_url) and after_url != before_url
if kind == "changed":
return before != after or (bool(after_url) and after_url != before_url)
if kind == "appeared":
return p_contains(after, arg) and not p_contains(before, arg)
if kind == "gone":
return p_contains(before, arg) and not p_contains(after, arg)
if kind == "filled":
return value_present(after, arg)
if kind == "cleared":
return not value_present(after, arg)
return False
@@ -0,0 +1,45 @@
"""The generic, site-agnostic verification core of the verified-action executor:
does an action produce the SPECIFIC expected effect, checked against a before/after
snapshot with zero per-site code. Pinned here, and pinned to match the send-script's
proven receipt so wiring it in was behavior-preserving."""
from backend.apps.agents.browser import browser_verified_action as va
from backend.apps.agents.browser.browser_agent import payload_in_textbox
EMPTY = '[2]<textbox "Write a message">\n[9]<button "Attach">'
FILLED = '[2]<textbox "Write a message" value="[test] hello world r9-os">\n[14]<button "Send">'
SENT = '[2]<textbox "Write a message">\n[9]<button "Attach">'
PAYLOAD = "[test] hello world r9-os"
def test_url_changed_and_changed():
assert va.expectation_met("url_changed", "s", "s", "u1", "u2")
assert not va.expectation_met("url_changed", "s", "s", "u1", "u1")
assert va.expectation_met("changed", "a", "b")
assert not va.expectation_met("changed", "a", "a")
def test_appeared_and_gone():
assert va.expectation_met("appeared:Send", EMPTY, FILLED) # Send button showed up
assert not va.expectation_met("appeared:Send", FILLED, FILLED)
assert va.expectation_met("gone:Send", FILLED, SENT) # Send button vanished after send
assert not va.expectation_met("gone:Send", EMPTY, EMPTY)
def test_filled_and_cleared_match_the_send_receipt():
# filled == the fill committed; cleared == the composer emptied (the send receipt)
assert va.expectation_met("filled:" + PAYLOAD, EMPTY, FILLED)
assert va.expectation_met("cleared:" + PAYLOAD, FILLED, SENT)
assert not va.expectation_met("cleared:" + PAYLOAD, EMPTY, FILLED) # still in the box
def test_generic_verifier_agrees_with_the_proven_inline_check():
# The send-script's receipt was `not payload_in_textbox(state, payload)`; the
# generic `cleared:` predicate must give the identical verdict on every state.
for state in (EMPTY, FILLED, SENT):
old = not payload_in_textbox(state, PAYLOAD)
new = va.expectation_met("cleared:" + PAYLOAD, FILLED, state)
assert old == new, f"mismatch on state={state!r}"
def test_unknown_expectation_fails_safe():
assert not va.expectation_met("teleported:X", EMPTY, FILLED) # typo/unknown = not met