[eric] browser: read script looks cheap first and only a decline buys the whole page

This commit is contained in:
ciregenz
2026-07-31 00:51:54 -07:00
parent 4f4d5e8d55
commit 17ab446945
2 changed files with 39 additions and 15 deletions
@@ -19,6 +19,12 @@ logger = logging.getLogger(__name__)
ToolRunner = Callable[[str, Dict, str, str], Awaitable[Dict]]
P_MIN_PAGE_CHARS = 500
# First look: the same modest slice the main loop reads. Most pages answer from this.
FIRST_READ_CHARS = 15000
# Second look, only after a decline: everything we can actually use. Half our live reads used to
# arrive pinned at exactly the smaller cap, with the answer (a reddit thread's comment scores sit
# after the post body) sitting just past the cut, so the aux declined and a 100-220s model loop went
# scrolling for text we had truncated ourselves.
MAX_PAGE_CHARS = 24000
P_TEXT_TIMEOUT_S = 8.0
P_AUX_TIMEOUT_S = 12.0
@@ -102,16 +108,13 @@ async def run_read_script(
try:
from backend.apps.agents.core.aux_llm import safe_resp_text
async def p_page_text() -> tuple:
async def p_page_text(cap: int) -> tuple:
"""Page text, but only once two consecutive reads agree it has stopped growing."""
prev = -1
text, url = "", ""
for attempt in range(MAX_READS):
# Ask for the whole budget we can actually use. The handler's default is sized for the
# main loop's context, not for this one aux call, and taking that default meant half
# our reads arrived pre-truncated with the answer sitting just past the cut.
r = await asyncio.wait_for(
execute_tool("BrowserGetText", {"max_chars": MAX_PAGE_CHARS}, browser_id, tab_id),
execute_tool("BrowserGetText", {"max_chars": cap}, browser_id, tab_id),
timeout=P_TEXT_TIMEOUT_S)
text = str(r.get("text") or "") if isinstance(r, dict) and "error" not in r else ""
url = str(r.get("url") or "") if isinstance(r, dict) else ""
@@ -125,7 +128,13 @@ async def run_read_script(
return (text, url) if len(text) >= P_MIN_PAGE_CHARS else ("", "")
for ask in range(1 + P_INSUFFICIENT_RETRIES):
page, p_live_url = await p_page_text()
# Cheap read first, full read only if that one came up short. A search-results page is one
# we ALWAYS leave (the answer is a click deeper), so buying the whole thing up front is
# text we can never answer from. Note the wall-clock case for this is NOT proven: live
# sweeps of these tasks vary 5-12x run to run, which buries an effect this size. It stands
# on the shape alone, never pay for what you don't need, and the retry is where the big
# read earns its keep, on a page that IS the right page but got cut off.
page, p_live_url = await p_page_text(FIRST_READ_CHARS if ask == 0 else MAX_PAGE_CHARS)
if len(page) < P_MIN_PAGE_CHARS:
logger.info(f"[browser-readscript] page too thin ({len(page)} chars); loop runs")
return None
+24 -9
View File
@@ -165,25 +165,40 @@ def test_a_page_that_simply_lacks_the_field_is_still_an_answer():
assert rs.is_answer(answer) == answer, answer
def test_the_read_asks_for_its_whole_budget_not_the_loops_default():
"""The handler's default char cap is sized for the MAIN model loop's context, not for this one
cheap aux call. Taking that default silently truncated the page: measured, 9 of 18 live reads
came back at EXACTLY the cap, and on a reddit thread the comment scores sit past the post body,
so the answer was cut off and a 100-220s model loop went looking for what we had removed."""
asked = {}
def test_the_first_look_is_cheap_and_only_a_decline_buys_the_big_read():
"""Reading the whole page every time made a search-results page (which we ALWAYS leave, one
click deeper) pay for chars it could never answer from: amazon's median went 26.1s -> 58.5s.
So the first look is the modest slice and only an INSUFFICIENT escalates."""
caps = []
async def run_tool(name, params, browser_id, tab_id):
asked.update(params or {})
caps.append((params or {}).get("max_chars"))
return {"text": PAGE, "url": "https://www.reddit.com/r/x/comments/1/y"}
aux = Aux("The top comment is by u/someone with 387 upvotes.")
out = asyncio.run(rs.run_read_script(aux, "m", "top comment?", "b1", "t1", run_tool))
assert out == "The top comment is by u/someone with 387 upvotes."
assert asked.get("max_chars") == rs.MAX_PAGE_CHARS, (
f"read must ask for the {rs.MAX_PAGE_CHARS} chars it can use, asked {asked!r}")
assert set(caps) == {rs.FIRST_READ_CHARS}, f"an answered page must never buy the big read, got {caps}"
def test_a_decline_escalates_to_the_full_page():
"""The big read exists for the page that IS the right page but was cut off. On a decline we
re-read at the full budget before giving the task up to the model loop."""
caps = []
async def run_tool(name, params, browser_id, tab_id):
caps.append((params or {}).get("max_chars"))
return {"text": PAGE, "url": "https://www.reddit.com/r/x/comments/1/y"}
aux = Aux("INSUFFICIENT")
out = asyncio.run(rs.run_read_script(aux, "m", "top comment?", "b1", "t1", run_tool))
assert out is None # still fails open to the loop
assert rs.FIRST_READ_CHARS in caps # cheap first
assert rs.MAX_PAGE_CHARS in caps, f"a decline must escalate to the full page, got {caps}"
def test_the_budget_it_asks_for_is_the_budget_it_sends():
"""If the ask and the send drift apart we are either paying for text we discard, or discarding
text we paid for. They are the same number by construction."""
assert rs.MAX_PAGE_CHARS >= 24000
assert rs.FIRST_READ_CHARS < rs.MAX_PAGE_CHARS