arena: v29 discriminative row context (DOM group labels + sibling groups) -- pre-registered pilot

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WsbS5x2rYsMDxP2kW3qqmQ
This commit is contained in:
ciregenz
2026-08-14 04:19:52 -07:00
co-authored by Claude Fable 5
parent 6328e78772
commit 2aade59eb5
4 changed files with 143 additions and 13 deletions
+15
View File
@@ -284,6 +284,21 @@ v29 = discriminative per-row context (nearest section/heading ancestor + local s
per row where twins exist) — a perception primitive for any multi-section page, nothing
CompWoB-specific. Pre-registration follows implementation.
## PRE-REGISTERED (2026-08-14, before any v29 episode): discriminative row context pilot
Mechanism: `local_ctx` (v29), perception layer, three rungs -- (1) `dom_group_hints`: nearest
SMALL labeled DOM ancestor (id/first class token, <=8 bids under it) becomes '§ <label>'; the
AX tree prunes unlabeled wrappers so this is recovered from the DOM snapshot ('div.widget >
input' was arriving as an anonymous textbox among 29 siblings); (2) sibling-group names ('w/
kgfN 1E9F ...') when no labeled ancestor exists; (3) old nearest-text fallback. Section labels
always render (a unique name cannot answer WHICH-group questions). Acid test passed offline:
the widget section self-labels (§ widget on its 5 rows incl. both named textboxes), dialog
titlebar self-labels, choice groups get distinct sibling contexts. Nothing task-specific: ids
and class tokens are how every real page names its sections. Prediction: >=3 of 8 pilot
targets (every one whose FIRST failing clause is an anonymous-among-twins pick — all 5
click-widget-containing targets are candidates), controls 12/12 unharmed (v22 arm untouched;
gate is the arm flag).
## Positioning vs public generic-harness baselines (user-supplied 2026 survey)
The comparable class is generic agents, NOT MiniWoB-specialized systems (HTML-T5++ 95.2 trained
+13 -1
View File
@@ -135,6 +135,11 @@ class LlmPolicy:
return (f"\nINSTRUCTION CLAUSES (complete IN ORDER; you last reported clause {self.cur_clause}):\n"
f"{rows}\nBegin your PLAN line with 'CLAUSE <n>:' stating the clause you are working on.")
# v29: discriminative local-group row context (perception layer). See
# perception.build_local_context -- same-role nameless twins in different page sections must
# render distinct, or the model's choice between them is a coin flip it cannot know it's making.
local_ctx: bool = False
# v28: per-step self-evaluation. The reply gains one trailing line -- 'EVAL: <verdict on the
# previous action, judged from the current page>' -- which rides in history, so every turn
# opens with the model's own judgment of whether its last step worked. browser-use's schema
@@ -307,7 +312,7 @@ class OpenSwarmLlmPolicy(LlmPolicy):
def view(self, obs: dict[str, Any], goal: str) -> tuple[str, int]:
raw_items: list[RankItem] = perception.interactives(
obs, include_clickable=self.clickable, attr_hints=self.hints,
include_offscreen=self.offscreen)
include_offscreen=self.offscreen, local_ctx=self.local_ctx)
shown, truncated = rank_and_cap(raw_items, goal=goal)
new = {it.bid for it in shown} - self.prev_bids if self.prev_bids else set()
self.prev_bids = {it.bid for it in shown}
@@ -873,6 +878,13 @@ def build(name: str, model: str = "", endpoint: str = "", **_: Any) -> Any:
scripted_drag=True, auto_complete=True, som=False,
native_pickers=True, verify_terminal=True, post_mouse_vision=True,
multi_cap=6, fill_verify=True, **v17)
if name == "osw-llm-v29": # v22 + discriminative local-group row context (perception primitive)
v29 = dict(v7, system=OSW_SYSTEM_V8 + OSW_SYSTEM_V9_WIDGETS + OSW_SYSTEM_V16, max_tokens=800)
return OpenSwarmLlmPolicy(name=name, multi=True, vision="progressive", fastpath=True,
scripted_drag=True, auto_complete=True, som=False,
native_pickers=True, verify_terminal=True, post_mouse_vision=True,
multi_cap=6, fill_verify=True, dispatch=True, offscreen=True,
local_ctx=True, **v29)
if name == "osw-llm-v28": # v22 + per-step self-eval line riding in history (eval-memory half)
v28 = dict(v7, system=OSW_SYSTEM_V8 + OSW_SYSTEM_V9_WIDGETS + OSW_SYSTEM_V16, max_tokens=800)
return OpenSwarmLlmPolicy(name=name, multi=True, vision="progressive", fastpath=True,
+111 -11
View File
@@ -71,9 +71,95 @@ def build_context(nodes: list[dict[str, Any]], by_id: dict[str, dict[str, Any]],
return ""
def dom_group_hints(obs: dict[str, Any], max_group: int = 8) -> dict[str, str]:
"""bid -> '§ <label>' from the nearest SMALL labeled DOM ancestor (id or first class token).
The AX tree prunes unlabeled wrappers, flattening section structure ('div.widget > input'
arrives as a bare textbox among 29 siblings). The DOM snapshot still has it. An ancestor
only counts if few enough bids live under it (<= max_group) -- page-level wrappers like
<div id=area> label everything and therefore label nothing."""
out: dict[str, str] = {}
dom = obs.get("dom_object") or {}
strings: list[str] = dom.get("strings") or []
for doc in dom.get("documents") or []:
nd = doc.get("nodes") or {}
parent: list[int] = nd.get("parentIndex") or []
attr_rows: list[list[int]] = nd.get("attributes") or []
n = len(parent)
pairs: list[dict[str, str]] = []
for a in attr_rows:
pairs.append({strings[a[k]]: strings[a[k + 1]] for k in range(0, len(a) - 1, 2)})
while len(pairs) < n:
pairs.append({})
cnt = [0] * n
withbid = [i for i in range(n) if pairs[i].get("bid")]
for i in withbid:
j = parent[i]
while 0 <= j < n:
cnt[j] += 1
j = parent[j]
for i in withbid:
j = parent[i]
while 0 <= j < n:
p = pairs[j]
label = (p.get("id") or (p.get("class") or "").split(" ")[0]).strip()
if label and not label.startswith("browsergym") and cnt[j] <= max_group:
out[pairs[i]["bid"]] = f"§ {label[:24]}"
break
if cnt[j] > max_group:
break # every higher ancestor is even bigger
j = parent[j]
return out
def build_local_context(by_id: dict[str, dict[str, Any]], node: dict[str, Any],
inter_ids: set, name_of, depth: int = 4) -> str:
"""v29: context from the node's LOCAL GROUP -- the names of its fellow members in the
smallest ancestor holding 2..8 interactives. build_context's nearest-ancestor-with-text
walk degrades to identical page-level soup on multi-section pages, so same-role nameless
twins (two anonymous textboxes in different sections) rendered indistinguishably and the
model's pick was a measured coin flip. Sibling names differ per section by construction."""
memo: dict[str, int] = {}
def icount(nid: str) -> int:
if nid in memo:
return memo[nid]
memo[nid] = 0 # cycle guard
n = by_id.get(nid) or {}
c = (1 if nid in inter_ids else 0) + sum(icount(cid) for cid in n.get("childIds") or [])
memo[nid] = c
return c
def members(nid: str, out: list, self_id: str) -> None:
if len(out) > 8:
return
if nid in inter_ids and nid != self_id:
out.append(nid)
for cid in (by_id.get(nid) or {}).get("childIds") or []:
members(cid, out, self_id)
self_id = node.get("nodeId")
cur = node
for _ in range(depth):
pid = cur.get("parentId")
if not pid or pid not in by_id:
break
parent = by_id[pid]
k = icount(pid)
if 2 <= k <= 8:
got: list = []
members(pid, got, self_id)
names = [name_of(by_id[m])[:14] for m in got]
names = [x for x in names if x]
if names:
return "w/ " + " ".join(names)[:56]
cur = parent
return ""
def interactives(obs: dict[str, Any], include_hidden: bool = False,
include_clickable: bool = False, attr_hints: bool = False,
include_offscreen: bool = False) -> list[RankItem]:
include_offscreen: bool = False, local_ctx: bool = False) -> list[RankItem]:
"""Every actionable node in document order, before any ranking or capping is applied.
include_clickable is the technique ingested from browser-use: elements the page wires for
@@ -86,7 +172,7 @@ def interactives(obs: dict[str, Any], include_hidden: bool = False,
extra = obs.get("extra_element_properties") or {}
by_id = {n["nodeId"]: n for n in nodes if "nodeId" in n}
hints = dom_attr_hints(obs) if attr_hints else {}
out: list[RankItem] = []
picked: list[tuple[dict[str, Any], str, str, dict[str, Any], bool]] = []
for n in nodes:
if n.get("ignored"):
continue
@@ -106,22 +192,36 @@ def interactives(obs: dict[str, Any], include_hidden: bool = False,
if not onscreen and not include_hidden:
if not (include_offscreen and props.get("bbox")):
continue
name = node_name(n).strip()
picked.append((n, role if is_role else (role or "clickable"), str(bid), props, onscreen))
def display_name(n: dict[str, Any]) -> str:
# Nameless rows resolve by their OWN text first ('dignissim' from the child text node), and
# only then by DOM identity ('(trash)'). The class hint alone made every link in a tab panel
# read '(alink)' -- indistinguishable, so the model guessed and the guess was terminal.
if not name:
name = subtree_text(by_id, n)
if not name and str(bid) in hints:
name = f"({hints[str(bid)]})"
bbox = (extra.get(str(bid)) or {}).get("bbox")
nm = node_name(n).strip() or subtree_text(by_id, n)
b = str(n.get("browsergym_id") or "")
if not nm and b in hints:
nm = f"({hints[b]})"
return nm
inter_ids = {n.get("nodeId") for n, *_ in picked}
grp = dom_group_hints(obs) if local_ctx else {}
out: list[RankItem] = []
for n, role, bid, props, onscreen in picked:
name = display_name(n)
bbox = props.get("bbox")
center = (bbox[0] + bbox[2] / 2, bbox[1] + bbox[3] / 2) if bbox else None
ctx = ""
if local_ctx:
ctx = grp.get(bid) or build_local_context(by_id, n, inter_ids, display_name)
if not ctx:
ctx = build_context(nodes, by_id, n)
out.append(RankItem(
role=role if is_role else (role or "clickable"),
role=role,
name=name,
bid=str(bid),
bid=bid,
value=node_value(n)[:80],
context=build_context(nodes, by_id, n),
context=ctx,
center=center,
options=child_options(by_id, n) if role in ("combobox", "listbox", "menu") else None,
offscreen=not onscreen,
+4 -1
View File
@@ -141,7 +141,10 @@ def render(items: list[RankItem], truncated: int, new_bids: set[str] | None = No
dup = counts.get(f"{el.role}|{el.name}", 0) > 1
# ctx also whenever the name alone cannot identify the row (empty or a bare attr hint).
weak_name = not el.name or el.name.startswith("(")
ctx = f' ctx="{el.context}"' if (dup or weak_name) and el.context else ""
# Section labels ('§ widget') always display: they answer WHICH-group questions the goal
# asks ('the text widget', 'in the dialog') that a unique name alone cannot.
show_ctx = (dup or weak_name or el.context.startswith("§")) and el.context
ctx = f' ctx="{el.context}"' if show_ctx else ""
val = f' value="{el.value}"' if el.value else ""
star = "*" if new_bids and el.bid in new_bids else ""
off = " off" if el.offscreen else ""