From af68dcd06d006d705fd32eaf90288bd0869fa73b Mon Sep 17 00:00:00 2001 From: ciregenz Date: Wed, 5 Aug 2026 08:28:38 -0700 Subject: [PATCH] [eric] send: when the task names a field, write into that field (reddit's title) --- .../apps/agents/browser/browser_send_parse.py | 40 ++++++++++++- .../agents/browser/browser_send_script.py | 9 +-- backend/tests/test_task_names_the_field.py | 60 +++++++++++++++++++ 3 files changed, 102 insertions(+), 7 deletions(-) create mode 100644 backend/tests/test_task_names_the_field.py diff --git a/backend/apps/agents/browser/browser_send_parse.py b/backend/apps/agents/browser/browser_send_parse.py index 6c7db5ff..21bafbc0 100644 --- a/backend/apps/agents/browser/browser_send_parse.py +++ b/backend/apps/agents/browser/browser_send_parse.py @@ -292,9 +292,43 @@ def opener_contradicts_task(task: str, opener_name: str) -> bool: return responding and bool(P_CREATE_OPENER_RE.search(name)) -def composer_index_in_state(state_text: str): - """(index, name) of the single compose-shaped textbox, or None. Two - candidates = ambiguous = model's problem.""" +# Fields a person names out loud that a compose-shaped picker will never choose, because they are +# short single-line inputs and nothing about them looks like a composer. Deliberately excludes +# "body"/"message"/"comment": those ARE compose-shaped, the existing picker already finds them, and +# adding them here would only create a second way to pick the same box. +P_FIELD_HINT_RE = re.compile(r"\b(title|subject|headline)\b", re.I) + + +def hinted_field_in_state(state_text: str, task: str): + """(index, name) of the textbox the TASK named by field, or None. + + Measured live on reddit 2026-08-05: the task was 'create a text post whose title is exactly X', + the picker below chose 'Post body text field' (correctly, by its own compose-shape rule), the + Title stayed empty, and reddit's submit therefore stayed DISABLED on every attempt. The task had + already said which field it meant, and nobody read it. + + Not a guess: the field word comes from the user's own sentence and has to appear in the + element's accessible name. Ambiguity still stands down, same rule as everywhere else here. + """ + m = P_FIELD_HINT_RE.search(task or "") + if not m: + return None + word = m.group(1).lower() + hits = [(int(i), n) for i, n in P_COMPOSER_ROW_RE.findall(state_text or "") + if word in (n or "").lower()] + return hits[0] if len(hits) == 1 else None + + +def composer_index_in_state(state_text: str, task: str = ""): + """(index, name) of the textbox to write into, or None. Two candidates = ambiguous = + model's problem. + + A field the task named by name wins over the compose-shaped guess, because it is the one piece + of evidence here that came from the person asking rather than from the page's shape. + """ + hinted = hinted_field_in_state(state_text, task) + if hinted: + return hinted hits = [(int(m.group(1)), m.group(2)) for m in P_COMPOSER_ROW_RE.finditer(state_text or "") if P_COMPOSER_NAME_RE.search(m.group(2) or "")] return hits[0] if len(hits) == 1 else None diff --git a/backend/apps/agents/browser/browser_send_script.py b/backend/apps/agents/browser/browser_send_script.py index 8420f03d..fd1820d4 100644 --- a/backend/apps/agents/browser/browser_send_script.py +++ b/backend/apps/agents/browser/browser_send_script.py @@ -340,7 +340,7 @@ async def run_send_script( return None log: list[dict] = [] - composer = browser_send_parse.composer_index_in_state(state_text) + composer = browser_send_parse.composer_index_in_state(state_text, task_sans_brief) if composer and browser_send_parse.surface_mismatch(task_sans_brief, composer[1]): # Asked to POST and found a COMMENT box, or found a DM box nobody asked for: either way it # is someone else's surface, not a slower route to ours. Drop it and let the tiers below @@ -352,7 +352,7 @@ async def run_send_script( for wait_s in (0.0, 1.2, 1.4): await asyncio.sleep(wait_s) fresh = await fresh_list() - composer = browser_send_parse.composer_index_in_state(fresh) + composer = browser_send_parse.composer_index_in_state(fresh, task_sans_brief) if composer: state_text = fresh break @@ -393,7 +393,7 @@ async def run_send_script( for wait_s in (0.0, 1.2, 1.5, 2.0, 2.0, 2.0): await asyncio.sleep(wait_s) state_text = await fresh_list() - composer = browser_send_parse.composer_index_in_state(state_text) + composer = browser_send_parse.composer_index_in_state(state_text, task_sans_brief) if composer: break p_settled = p_settled + 1 if state_text and state_text == p_prev else 0 @@ -495,7 +495,8 @@ async def run_send_script( if wait_s: await asyncio.sleep(wait_s) state_retry = await fresh_list() - composer_retry = browser_send_parse.composer_index_in_state(state_retry) + composer_retry = browser_send_parse.composer_index_in_state( + state_retry, task_sans_brief) if composer_retry: break if not composer_retry: diff --git a/backend/tests/test_task_names_the_field.py b/backend/tests/test_task_names_the_field.py new file mode 100644 index 00000000..3b01f073 --- /dev/null +++ b/backend/tests/test_task_names_the_field.py @@ -0,0 +1,60 @@ +"""When the task names a field, write into THAT field. + +Measured live on reddit 2026-08-05, twice in one round. Task: "create a text post whose title is +exactly canary5e95c195. Submit it." The compose-shaped picker chose 'Post body text field', which is +the right answer to the question it was asking (which box looks like a composer?) and the wrong +answer to the question that mattered (which box did the person mean?). reddit's Title stayed empty, +its submit stayed DISABLED, and the run could never complete: `submit 'post' is present but +DISABLED; the form is incomplete, handing to the model`, on every attempt. + +The field word comes from the user's own sentence and must appear in the element's accessible name, +so this is not the picker guessing; it is the picker finally reading the instruction. +""" + +from backend.apps.agents.browser import browser_send_parse as bp + +# A reddit submit form as the perception lists it: a short Title input and the big body composer. +REDDIT = '[21]\n[23]\n[25]