[eric] browser: attached page state shrinks to changed rows, unchanged indices stay valid

This commit is contained in:
ciregenz
2026-06-05 11:14:13 -07:00
parent 0992e25bf2
commit 99bd15fdb1
2 changed files with 73 additions and 1 deletions
+35 -1
View File
@@ -148,9 +148,33 @@ def _truncate_state(text: str, max_lines: int = _AUTO_STATE_MAX_LINES) -> str:
)
def _delta_state(text: str, seen_lines: set[str]) -> str:
"""Shrink an attached element list to the rows that changed since the last
attach; stable indices make a line's identity meaningful, so re-sending 30
unchanged rows every action is pure token burn. Mutates `seen_lines` to the
new baseline. Small overlaps just resend the full list (a reshuffle)."""
rows = [l for l in str(text).splitlines() if l.startswith("[")]
cur = set(rows)
prev = set(seen_lines)
seen_lines.clear()
seen_lines.update(cur)
if not prev or not rows:
return text
fresh = [l for l in rows if l not in prev]
unchanged = len(rows) - len(fresh)
if unchanged < 6:
return text
if not fresh:
return f"(all {unchanged} element rows unchanged since your last look; same numbers still valid)"
return "\n".join(fresh) + (
f"\n(+{unchanged} rows unchanged since your last look; their numbers are still valid)"
)
async def _post_action_state(
tool_name: str, tool_input: dict, result: dict,
browser_id: str, tab_id: str, wait_exec, goal: str,
seen_lines: set[str] | None = None,
) -> str:
"""Settle the page after a mutating action, then return a compact fresh
interactives list to append to its result. Empty string = attach nothing."""
@@ -174,7 +198,8 @@ async def _post_action_state(
return ""
if not isinstance(lst, dict) or "error" in lst or not lst.get("text"):
return ""
return f"\n\n{PAGE_STATE_MARKER}\n{_truncate_state(lst['text'])}"
state = lst["text"] if seen_lines is None else _delta_state(lst["text"], seen_lines)
return f"\n\n{PAGE_STATE_MARKER}\n{_truncate_state(state)}"
async def _request_browser_approval(
@@ -651,6 +676,8 @@ async def run_browser_agent(
# MAX_TURNS doing the same broken thing.
consecutive_violations = 0
MAX_CONSECUTIVE_VIOLATIONS = 3
# rows already shown to the model; attached state shrinks to the delta
attached_state_seen: set[str] = set()
try:
for turn in range(MAX_TURNS):
if cancel_event.is_set():
@@ -1154,8 +1181,15 @@ async def run_browser_agent(
last_seen_url = result["url"]
card_gone_streak = card_gone_streak + 1 if card_is_unavailable(result) else 0
# a direct full list resets the delta baseline to what the model just saw
if tu.name == "BrowserListInteractives" and "error" not in result:
attached_state_seen.clear()
attached_state_seen.update(
l for l in str(result.get("text") or "").splitlines() if l.startswith("[")
)
_auto_state = await _post_action_state(
tu.name, tu.input, result, browser_id, tab_id, _wait_exec, current_next_goal,
seen_lines=attached_state_seen,
)
if _auto_state:
result["text"] = f"{result.get('text') or ''}{_auto_state}"
+38
View File
@@ -1240,3 +1240,41 @@ def test_post_action_state_hung_settle_attaches_nothing(monkeypatch):
"b1", "", _fake_exec(calls), "",
))
assert out == "" and calls == []
def test_delta_state_first_attach_sends_full_list():
from backend.apps.agents.browser.browser_agent import _delta_state
seen = set()
full = "8 interactive elements:\n" + "\n".join(f'[{i}]<button "b{i}">' for i in range(1, 9))
assert _delta_state(full, seen) == full
assert len(seen) == 8
def test_delta_state_shrinks_to_changed_rows():
from backend.apps.agents.browser.browser_agent import _delta_state
rows = [f'[{i}]<button "b{i}">' for i in range(1, 11)]
seen = set()
_delta_state("\n".join(rows), seen)
nxt = rows[:9] + ['[10]<button "b10" value="typed">', '[11]*<button "new">']
out = _delta_state("\n".join(nxt), seen)
assert '[11]*<button "new">' in out and 'value="typed"' in out
assert '[3]<button "b3">' not in out
assert "+9 rows unchanged" in out
assert seen == set(nxt)
def test_delta_state_no_changes_collapses_to_one_line():
from backend.apps.agents.browser.browser_agent import _delta_state
rows = "\n".join(f'[{i}]<link "l{i}">' for i in range(1, 13))
seen = set()
_delta_state(rows, seen)
out = _delta_state(rows, seen)
assert out.startswith("(all 12 element rows unchanged")
def test_delta_state_reshuffle_resends_full():
from backend.apps.agents.browser.browser_agent import _delta_state
seen = set()
_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