mirror of
https://github.com/openswarm-ai/openswarm.git
synced 2026-10-01 05:54:56 +02:00
[eric] browser: prompt-cache the fixed prefix and push batching to cut first-run turns
This commit is contained in:
@@ -322,6 +322,17 @@ async def run_browser_agent(
|
||||
+ prior_note
|
||||
)
|
||||
|
||||
# Prompt-caching shapes built once: system as a single cached text block,
|
||||
# and the last tool carrying the cache_control marker (Anthropic keys on the
|
||||
# trailing marker, so one marker covers the whole tool array + system).
|
||||
_cached_system = [{
|
||||
"type": "text", "text": run_system_prompt,
|
||||
"cache_control": {"type": "ephemeral"},
|
||||
}]
|
||||
_cached_tools = [dict(t) for t in BROWSER_TOOLS_SCHEMA]
|
||||
if _cached_tools:
|
||||
_cached_tools[-1] = {**_cached_tools[-1], "cache_control": {"type": "ephemeral"}}
|
||||
|
||||
user_msg = Message(role="user", content=task)
|
||||
session.messages.append(user_msg)
|
||||
await ws_manager.send_to_session(session_id, "agent:message", {
|
||||
@@ -429,8 +440,13 @@ async def run_browser_agent(
|
||||
response = await _cancellable(client.messages.create(
|
||||
model=api_model,
|
||||
max_tokens=4096,
|
||||
system=run_system_prompt,
|
||||
tools=BROWSER_TOOLS_SCHEMA,
|
||||
# Cache the ~4k-token fixed prefix (system + tool schema) so it's
|
||||
# reprocessed once, not on every turn: big TTFT + cost win on the
|
||||
# first run, which is dominated by turns x per-turn prefill. The
|
||||
# trailing cache_control marker is what Anthropic keys on; on
|
||||
# non-Anthropic routes (9router) the marker is harmlessly ignored.
|
||||
system=_cached_system,
|
||||
tools=_cached_tools,
|
||||
messages=messages,
|
||||
))
|
||||
if response is None:
|
||||
@@ -445,6 +461,13 @@ async def run_browser_agent(
|
||||
if hasattr(response, 'usage') and response.usage:
|
||||
session.tokens["input"] = session.tokens.get("input", 0) + (response.usage.input_tokens or 0)
|
||||
session.tokens["output"] = session.tokens.get("output", 0) + (response.usage.output_tokens or 0)
|
||||
# Cache-read tokens prove the prompt cache is working (climbs
|
||||
# after turn 1). Logged so the speed win is verifiable, not assumed.
|
||||
_cr = getattr(response.usage, "cache_read_input_tokens", 0) or 0
|
||||
_cw = getattr(response.usage, "cache_creation_input_tokens", 0) or 0
|
||||
if _cr or _cw:
|
||||
session.tokens["cache_read"] = session.tokens.get("cache_read", 0) + _cr
|
||||
logger.info(f"[browser-perf] turn {turn}: cache_read={_cr} cache_write={_cw} input={response.usage.input_tokens}")
|
||||
|
||||
assistant_content = []
|
||||
text_parts = []
|
||||
@@ -881,10 +904,18 @@ async def run_browser_agent(
|
||||
# the next identical task on this host runs via the no-LLM fast path.
|
||||
try:
|
||||
rec_host = browser_skills.host_of(last_seen_url)
|
||||
_distilled = browser_skills.distill_steps(action_log)
|
||||
logger.info(
|
||||
f"[browser-skills] record attempt: host={rec_host!r} "
|
||||
f"last_url={last_seen_url!r} action_tools={[a.get('tool') for a in action_log]} "
|
||||
f"distilled={[s['tool'] for s in _distilled]}"
|
||||
)
|
||||
if browser_skills.record_skill(rec_host, task, action_log):
|
||||
logger.info(f"[browser-skills] learned skill for {rec_host} (future runs replay fast)")
|
||||
except Exception:
|
||||
pass
|
||||
else:
|
||||
logger.info(f"[browser-skills] NOT recorded (host empty or no robust steps)")
|
||||
except Exception as e:
|
||||
logger.warning(f"[browser-skills] record raised: {e}")
|
||||
agent_manager._sync_session_close(session)
|
||||
await ws_manager.send_to_session(session_id, "agent:status", {
|
||||
"session_id": session_id,
|
||||
|
||||
@@ -438,6 +438,14 @@ SYSTEM_PROMPT = (
|
||||
"5. **Coordinate-based fallback**; last resort: take a screenshot, identify the "
|
||||
"button visually, then click by approximate coords.\n\n"
|
||||
|
||||
"## Speed: minimize round-trips (this is the #1 driver of how fast you are)\n"
|
||||
"Every turn is a slow model round-trip; tools themselves are fast. So the way "
|
||||
"to be fast is FEWER TURNS, not faster tools. Once you can see the page, plan "
|
||||
"the whole remaining sequence and emit it in ONE BrowserBatch instead of one "
|
||||
"action per turn. A 3-step form (type, type, click Send) should be a single "
|
||||
"batch turn, not three. Only break the batch when a later step genuinely "
|
||||
"depends on reading what an earlier step produced.\n\n"
|
||||
|
||||
"## Batch known sequences with BrowserBatch\n"
|
||||
"When you have a known sequence of actions; typing then pressing Enter, "
|
||||
"swiping multiple times, clicking through pagination; emit them all in a "
|
||||
|
||||
@@ -268,6 +268,24 @@ def test_replay_falls_back_to_full_agent_when_a_step_fails(monkeypatch):
|
||||
assert len(primary.calls) > 0, "fell back to the full LLM agent"
|
||||
|
||||
|
||||
def test_prompt_caching_markers_present(monkeypatch):
|
||||
# The fixed system+tools prefix must carry cache_control so it's cached
|
||||
# across turns (the first-run speed/cost win). Without the marker the
|
||||
# ~4k-token prefix is reprocessed every turn.
|
||||
BH._browser_history.clear(); BH._domain_notes.clear()
|
||||
primary = FakeLLM([Resp([Blk("text", "done")], stop_reason="end_turn")])
|
||||
aux = FakeAux()
|
||||
_install(monkeypatch, primary, aux)
|
||||
asyncio.run(BA.run_browser_agent(task="hi", browser_id="bz", model="sonnet"))
|
||||
call = primary.calls[0]
|
||||
sys = call["system"]
|
||||
assert isinstance(sys, list) and sys[-1]["cache_control"]["type"] == "ephemeral"
|
||||
tools = call["tools"]
|
||||
assert tools[-1].get("cache_control", {}).get("type") == "ephemeral"
|
||||
# exactly one cache marker on the tools array (Anthropic allows <=4; we use 1)
|
||||
assert sum(1 for t in tools if t.get("cache_control")) == 1
|
||||
|
||||
|
||||
def test_prior_domain_hint_is_seeded_into_system_prompt(monkeypatch):
|
||||
BH._browser_history.clear(); BH._domain_notes.clear()
|
||||
BH.set_domain_note("google.com", "REMEMBERED: Share button is index 43; Tab into the dialog.")
|
||||
@@ -279,7 +297,12 @@ def test_prior_domain_hint_is_seeded_into_system_prompt(monkeypatch):
|
||||
task="open the doc", browser_id="b2", model="sonnet", initial_url=DOC_URL,
|
||||
))
|
||||
assert primary.calls, "LLM should have been called"
|
||||
# system is a cached content-block list (prompt caching); flatten its text
|
||||
system = primary.calls[0]["system"]
|
||||
assert "Notes from a previous visit" in system
|
||||
assert "REMEMBERED: Share button is index 43" in system
|
||||
system_text = system if isinstance(system, str) else " ".join(b.get("text", "") for b in system)
|
||||
assert "Notes from a previous visit" in system_text
|
||||
assert "REMEMBERED: Share button is index 43" in system_text
|
||||
# the cached system block carries the cache_control marker
|
||||
if isinstance(system, list):
|
||||
assert system[-1].get("cache_control", {}).get("type") == "ephemeral"
|
||||
assert len(aux.calls) == 0 # no exhaustion, no adjudication on a clean run
|
||||
|
||||
Reference in New Issue
Block a user