[eric] send: when the task names a field, write into that field (reddit's title)

This commit is contained in:
ciregenz
2026-08-05 08:28:38 -07:00
parent 46f2213a9b
commit af68dcd06d
3 changed files with 102 additions and 7 deletions
@@ -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
@@ -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:
@@ -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]<textbox "Title" />\n[23]<textbox "Post body text field" />\n[25]<button "post" />'
# LinkedIn's post modal: one compose-shaped box, no named field anywhere.
LINKEDIN = '[54]<textbox "Text editor for creating content" />\n[57]<button "Post" />'
def test_a_named_title_beats_the_compose_shaped_guess():
"""The regression, exactly as it happened."""
assert bp.composer_index_in_state(
REDDIT, 'create a text post whose title is exactly "canary5e95c195"') == (21, "Title")
def test_without_a_field_word_the_old_picker_still_decides():
"""No hint means no change: every site that worked before must keep working."""
assert bp.composer_index_in_state(
REDDIT, 'create a text post with body "hello"') == (23, "Post body text field")
assert bp.composer_index_in_state(
LINKEDIN, 'create a post with exactly this text: "hello"') == (
54, "Text editor for creating content")
def test_a_field_word_with_no_matching_box_changes_nothing():
"""LinkedIn has no Title field. Naming one must not break the composer it does have."""
assert bp.composer_index_in_state(
LINKEDIN, 'post with the title "hello"') == (54, "Text editor for creating content")
def test_two_boxes_matching_the_hint_stand_down():
"""Ambiguity is the model's problem here, same as everywhere else in this file."""
two = '[1]<textbox "Title" />\n[2]<textbox "Subtitle title" />\n[3]<textbox "Post body text field" />'
assert bp.hinted_field_in_state(two, 'post with the title "x"') is None
# and the compose-shaped picker still answers
assert bp.composer_index_in_state(two, 'post with the title "x"') == (3, "Post body text field")
def test_the_hint_words_stay_off_compose_shaped_fields():
"""'body', 'message' and 'comment' are deliberately NOT hints: those boxes are already found by
shape, and a second route to the same element is a second thing to keep in sync."""
for word in ("body", "message", "comment"):
assert bp.P_FIELD_HINT_RE.search(word) is None
def test_the_old_single_argument_call_still_works():
"""Callers outside the send script pass state only."""
assert bp.composer_index_in_state(LINKEDIN) == (54, "Text editor for creating content")