mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-09-12 04:37:44 +02:00
[eric] browser: a run out of wall time delivers what it has instead of nothing
This commit is contained in:
@@ -58,6 +58,11 @@ P_BATCHABLE_ACTION_TOOLS = {
|
||||
}
|
||||
|
||||
# Injected when the spin backstop trips: one chance to land a real answer from what's already gathered, instead of the loop cutting it off mid-thought.
|
||||
# Past this many seconds a run stops exploring and delivers what it has. Sits above every run that
|
||||
# actually succeeded in the 2026-07-30 sweeps (slowest good one: 142.8s) and well under the 300s
|
||||
# that produced an empty answer. It nudges rather than kills, so the answer still comes from Done.
|
||||
P_WALL_BUDGET_S = 180.0
|
||||
|
||||
P_WRAPUP_NUDGE = (
|
||||
"You've spent several turns looking without finishing. Wrap up NOW: call Done with the "
|
||||
"best answer you can give from what you've ALREADY gathered. For a find/list ask, put the "
|
||||
@@ -2042,10 +2047,19 @@ async def run_browser_agent(
|
||||
p_novel_read = True
|
||||
|
||||
# Out of turn budget with no answer yet: nudge a wrap-up so a long-running gather delivers what it has via Done at the cap, instead of the for-loop ending on the model's half-finished sentence. Same one-shot channel.
|
||||
if turn >= MAX_TURNS - 4 and not wrapup_nudged and not done_called and not send_confirmed:
|
||||
# Turns are not minutes: 40 turns bounds the LOOP but not the clock, and nobody waits
|
||||
# five minutes for a browser errand they could have run themselves. Measured: a reddit
|
||||
# task ground past 300s and returned NOTHING, while every run that ever succeeded that
|
||||
# day finished inside 143s. So the clock gets the same nudge the turn cap gets, and the
|
||||
# answer arrives partial-but-honest instead of never.
|
||||
p_out_of_time = time.time() - metrics_started_at > P_WALL_BUDGET_S
|
||||
if ((turn >= MAX_TURNS - 4 or p_out_of_time)
|
||||
and not wrapup_nudged and not done_called and not send_confirmed):
|
||||
wrapup_nudged = True
|
||||
wrapup_pending = True
|
||||
logger.info(f"[browser-agent {session_id}] turn budget low ({turn}/{MAX_TURNS}); nudging wrap-up")
|
||||
p_why = (f"wall budget spent ({int(time.time() - metrics_started_at)}s)" if p_out_of_time
|
||||
else f"turn budget low ({turn}/{MAX_TURNS})")
|
||||
logger.info(f"[browser-agent {session_id}] {p_why}; nudging wrap-up")
|
||||
|
||||
# Spin backstop: a pure-perception turn that ISN'T gathering new data is wasted (re-verifying a send, or re-looking at the same page). Bound it.
|
||||
if p_turn_actions == 0:
|
||||
|
||||
@@ -367,6 +367,40 @@ def test_spin_backstop_nudges_a_clean_wrapup_instead_of_a_midthought(monkeypatch
|
||||
assert result.get("done") is True
|
||||
|
||||
|
||||
def test_a_run_out_of_wall_time_delivers_what_it_has(monkeypatch):
|
||||
# Turns bound the LOOP, not the clock. Measured 2026-07-30: a reddit task ground past 300s and
|
||||
# returned an EMPTY answer, while every run that succeeded that day finished inside 143s. The
|
||||
# clock now gets the same one-shot wrap-up nudge the turn cap gets, so a long errand comes back
|
||||
# partial-but-honest instead of never.
|
||||
BH.BROWSER_HISTORY.clear(); BH.DOMAIN_NOTES.clear()
|
||||
monkeypatch.setattr(BA, "P_WALL_BUDGET_S", 0.0) # over budget from the first turn
|
||||
primary = FakeLLM([
|
||||
Resp([p_rp("looking around"), p_tu("BrowserGetText")]),
|
||||
Resp([p_tu("Done", message="Top comment is from u/someone, 387 upvotes.")]),
|
||||
])
|
||||
p_install(monkeypatch, primary, FakeAux())
|
||||
result = asyncio.run(BA.run_browser_agent(task="read the top comment", browser_id="b1", model="sonnet"))
|
||||
assert any("Wrap up NOW" in json.dumps(c["messages"]) for c in primary.calls), \
|
||||
"a run past its wall budget must be nudged to deliver"
|
||||
assert result["summary"] == "Top comment is from u/someone, 387 upvotes."
|
||||
assert result.get("done") is True
|
||||
|
||||
|
||||
def test_a_quick_run_is_never_nudged_by_the_wall_budget(monkeypatch):
|
||||
# The budget must not touch normal runs, or it would cut short the very tasks it exists to save.
|
||||
BH.BROWSER_HISTORY.clear(); BH.DOMAIN_NOTES.clear()
|
||||
assert BA.P_WALL_BUDGET_S >= 150, "budget must sit above the slowest run that actually succeeded"
|
||||
primary = FakeLLM([
|
||||
Resp([p_rp("reading"), p_tu("BrowserGetText")]),
|
||||
Resp([p_tu("Done", message="It costs $9.99.")]),
|
||||
])
|
||||
p_install(monkeypatch, primary, FakeAux())
|
||||
result = asyncio.run(BA.run_browser_agent(task="what does it cost", browser_id="b1", model="sonnet"))
|
||||
assert not any("Wrap up NOW" in json.dumps(c["messages"]) for c in primary.calls), \
|
||||
"a fast run must never see the wrap-up nudge"
|
||||
assert result["summary"] == "It costs $9.99."
|
||||
|
||||
|
||||
def test_early_perception_is_not_cut_short_before_any_action(monkeypatch):
|
||||
# Orienting on a cold/slow page can take several look-only turns; the stall backstop must NOT fire before the agent has done anything (it only bounds a POST-action spin). Here 7 perception turns precede the finish; all must run.
|
||||
BH.BROWSER_HISTORY.clear(); BH.DOMAIN_NOTES.clear()
|
||||
|
||||
Reference in New Issue
Block a user