●NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaply●OMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflows●AGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactions●MEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuously●THROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and region●DEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20●NANOLITE — Nano Banana 2 Lite is here: Google's fastest and most cost-efficient Gemini Image model, made for running lightweight image generation cheaply●OMNIFLASH — Gemini Omni Flash is in public preview, a natively multimodal model that lets enterprises and developers build custom, dynamic video workflows●AGENTS — Managed Agents expand with background: true for async server-side runs and polling, remote MCP server integration, and refreshing credentials across interactions●MEMORY — The Memory Bank IngestEvents API is generally available, decoupling event ingestion from memory generation so you can stream content continuously●THROUGHPUT — Provisioned Throughput now lets you submit up to seven pending orders for the same model and region●DEPRECATE — Image generation models shut down on August 17, and the Grok 4.1 family on the Gemini Enterprise Agent Platform on August 20
Self-Healing Architecture for Gemini Computer Use — Production Patterns That Keep Browser Automation Alive Beyond Day Three
Gemini Computer Use looks magical in demos but breaks daily in production: vanishing elements, surprise modals, network jitter, off-by-four-pixel clicks. This guide builds a five-layer self-healing architecture in Python that classifies failures and recovers them automatically, with working code you can drop into your agent loop today.
Browser automation will break in production. Plan for it.
The first time you watch Gemini 3's Computer Use tool drive a browser is genuinely strange. You write a paragraph in plain English — "open the invoice page, download this month's PDF, upload it to the accounting tool" — and a cursor moves on the screen as if a careful intern were doing the work. It's the kind of demo that makes you close your laptop and start replanning your week.
The trouble starts on day three. By then a cookie banner has appeared that wasn't there before. By day five the vendor has tweaked their layout. By day seven the network is just a bit slow and your default timeout misses. By day ten a checkbox has shifted four pixels to the left and the click lands on empty whitespace. None of these are catastrophic on their own, but in production they accrue: every week, more of them, every week, a few more 3 a.m. alerts.
The official docs explain how to make Computer Use work. They are quieter on what to do when it stops working. To run a daily automation that doesn't wake your team up, you need to wrap the agent loop in a deliberate self-healing layer. This article walks through the five-layer architecture I've been running for my own products, with Python code you can paste into a small project and grow from there. By the end you should have enough scaffolding to take an automation that fails three times a week and bring it down to "rarely" — which, in this domain, is the realistic target.
Why "just retry" doesn't fix Computer Use failures
Before the code, a quick mental model. "If it failed, retry it" is half-right. Computer Use failures come from very different roots, and a blanket retry only helps one of them.
I bucket failures into four categories:
Transient failures — momentary network blips, slow page paint, an upstream 503. Time fixes these.
State drift — the page is in a state your prompt didn't anticipate: a cookie modal appeared, the session expired, a new dialog is in front of the target.
Structural change — the site changed its DOM or layout. The coordinates the model picked yesterday no longer point at the right element.
Semantic mistakes — the model misread the instruction and is about to click the wrong button.
Retries help category one. Category two needs reset, then retry. Category three needs re-observe, then retry. Category four needs a human, immediately. The first job of a self-healing system is therefore not retrying — it's classifying. If you skip classification, your retries either give up too early (transient failures masked) or hammer through real bugs (semantic mistakes amplified).
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦If you've been bitten by an automation that worked on Monday and silently broke by Thursday, you'll leave with a layered recovery architecture you can drop into your agent loop
✦You'll get copy-paste Python: pre-conditions, failure classifier, recovery playbook, screenshot drift detector, and a Slack escalation hook — designed to grow with your real workload
✦You'll have a clear judgment framework for when to retry, when to re-observe, and when to wake a human up — the kind of decisions that actually decide whether on-call sleeps through the night
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
The smallest and most useful piece is a guard that, before each step, asks Gemini whether the screen is in the state we expect. It's a few dozen lines of code, and in my own runs it's the layer with the highest leverage by far.
Why pre-checking pays off
Computer Use looks at the screen and decides what to do next, every turn. When async loads or navigation are involved, it's easy for the agent to act on a screen that looks ready but isn't. A guard that catches "we wanted the invoice list but I see a loading spinner" is cheaper than recovering from a misdirected click ten seconds later.
Implementation
The example below uses Playwright and the google-genai Python client. You'll need:
# state_guard.pyimport osimport jsonfrom playwright.sync_api import Pagefrom google import genaifrom google.genai import typesclient = genai.Client(api_key=os.environ["GEMINI_API_KEY"])MODEL = "gemini-3-pro" # any Computer Use-capable modeldef screenshot_bytes(page: Page) -> bytes: return page.screenshot(type="png", full_page=False)def assert_state(page: Page, expectation: str) -> dict: """ Ask Gemini whether the current screen meets `expectation`. Returns: {"ok": bool, "reason": str, "suggested_recovery": str} """ img = screenshot_bytes(page) prompt = ( "You are the state monitor for a browser-automation agent. " "Decide whether the screenshot satisfies the expectation. " "Respond as JSON only.\n\n" f"Expectation: {expectation}\n\n" 'Format: {"ok": true|false, "reason": "...", "suggested_recovery": "..."}' ) resp = client.models.generate_content( model=MODEL, contents=[ types.Part.from_bytes(data=img, mime_type="image/png"), prompt, ], config=types.GenerateContentConfig( response_mime_type="application/json", temperature=0.0, # judging should be stable, not creative ), ) try: return json.loads(resp.text) except json.JSONDecodeError: # Treat a malformed judgment as "unknown -> not ok" — fail safe. return { "ok": False, "reason": "judge_returned_invalid_json", "suggested_recovery": "wait_and_retry", }
Putting the guard around an action
def click_with_guard(page: Page, target_label: str, expectation_after: str): # 1) Pre-condition pre = assert_state(page, f"A button labeled '{target_label}' is visible") if not pre["ok"]: raise StateMismatchError(pre["reason"], pre["suggested_recovery"]) # 2) Hand off to the Computer Use agent (separate function in your codebase) run_computer_use_step(page, instruction=f"Click the '{target_label}' button") # 3) Post-condition page.wait_for_load_state("networkidle", timeout=15_000) post = assert_state(page, expectation_after) if not post["ok"]: raise PostConditionError(post["reason"], post["suggested_recovery"])
A typical "not ok" response looks like {"ok": false, "reason": "Cookie consent modal is in the foreground", "suggested_recovery": "dismiss_modal_then_retry"}. We use that suggested_recovery field downstream in Layer 3.
I always pin the judge's temperature to 0.0. If the judge wobbles between "ok" and "not ok" for the same screen, the retry logic above it never settles. Trade creativity for reproducibility — it's the right call inside a guard.
Layer 2: classify the failure
Once the guard says "no" or the action throws, the next step is labelling. The classifier maps an exception (or guard hint) onto one of the four categories.
# failure_classifier.pyfrom enum import Enumfrom playwright.sync_api import Pageclass FailureType(str, Enum): TRANSIENT = "transient" STATE_DRIFT = "state_drift" STRUCTURE_CHANGE = "structure_change" SEMANTIC_MISTAKE = "semantic_mistake"def classify(page: Page, error: Exception, guard_result: dict | None) -> FailureType: msg = str(error).lower() # 1) Transient: clearest signal first if any(k in msg for k in ["timeout", "net::err_", "503", "econn", "socket hang up"]): return FailureType.TRANSIENT # 2) State drift: trust the guard if it gave us a hint if guard_result and guard_result.get("suggested_recovery") in { "dismiss_modal_then_retry", "relogin_then_retry", "wait_and_retry", }: return FailureType.STATE_DRIFT # 3) Structural change: element-not-found family if any(k in msg for k in [ "element is not visible", "no element found", "click intercepted", ]): return FailureType.STRUCTURE_CHANGE # 4) Default: unknown failures escalate to humans return FailureType.SEMANTIC_MISTAKE
Why SEMANTIC_MISTAKE is the default
This is the design decision that matters most. A classifier should label only what it can identify with confidence and route everything else to humans. The alternative — letting the model itself decide what category an error belongs to — sounds elegant until the day the agent decides that "clicking the cancel-membership button instead of the renew button" is a transient failure. Be conservative.
Layer 3: recovery playbook
Now that we have a label, we look up a recipe.
# recovery_playbook.pyimport timeimport randomfrom playwright.sync_api import PageMAX_RETRY = 3def recover(page: Page, ftype: FailureType, attempt: int) -> bool: """ Returns True if the caller should retry the original action, False to give up. """ if ftype == FailureType.TRANSIENT: # Exponential backoff with jitter delay = (2 ** attempt) + random.uniform(0, 1.5) print(f"[recover] transient: sleeping {delay:.2f}s") time.sleep(delay) return True if ftype == FailureType.STATE_DRIFT: # Sweep known overlays for sel in [ "button:has-text('Accept all')", "button:has-text('I agree')", "button[aria-label='Close']", "[role='dialog'] button:has-text('Close')", ]: try: page.locator(sel).first.click(timeout=1500) page.wait_for_timeout(500) except Exception: pass if "login" in page.url: relogin(page) return True if ftype == FailureType.STRUCTURE_CHANGE: # Don't fix selectors by hand. Let the agent re-observe. page.wait_for_timeout(1500) return True # SEMANTIC_MISTAKE — never auto-recover. return Falsedef relogin(page: Page): import os page.fill("input[name='email']", os.environ["APP_EMAIL"]) page.fill("input[name='password']", os.environ["APP_PASSWORD"]) page.click("button[type='submit']") page.wait_for_load_state("networkidle")
The most important line in this file is the one that isn't there: there's no manual selector lookup for STRUCTURE_CHANGE. That's the whole point of Computer Use — when the layout shifts, you give the model another screenshot and it re-observes. Don't drag your old Selenium habits in here. The leverage is "hand a fresh image to a model that can see," not "patch a CSS selector at 2 a.m."
Layer 4: visual drift detection
Once single-step failures are tame, the next danger is a site redesign. In the days after a redesign, the model is in unfamiliar territory and failure rates spike. If you notice the change before it bites, you can inject a short familiarity prompt and absorb most of the damage.
# visual_drift.pyimport osimport iofrom PIL import Image, ImageChopsDRIFT_DIR = "/var/lib/agent/baseline"def baseline_path(name: str) -> str: return os.path.join(DRIFT_DIR, f"{name}.png")def update_baseline(name: str, png: bytes): os.makedirs(DRIFT_DIR, exist_ok=True) with open(baseline_path(name), "wb") as f: f.write(png)def measure_drift(name: str, png: bytes) -> float: """ Returns a 0.0–1.0 score: how different the current screenshot is from the baseline. On first call we adopt the current image as baseline and return 0.0. """ p = baseline_path(name) if not os.path.exists(p): update_baseline(name, png) return 0.0 base = Image.open(p).convert("RGB") cur = Image.open(io.BytesIO(png)).convert("RGB") if base.size != cur.size: cur = cur.resize(base.size) diff = ImageChops.difference(base, cur) bbox = diff.getbbox() if bbox is None: return 0.0 bw = (bbox[2] - bbox[0]) * (bbox[3] - bbox[1]) return bw / (base.size[0] * base.size[1])
In my projects I run measure_drift("vendor_login", screenshot_bytes(page)) first thing in the morning. A score above 0.3 pings Slack with "the vendor may have changed something." Above 0.6 we automatically inject this preamble into the next agent prompt: "The site may have been recently redesigned. Be extra careful when locating elements." Treat the thresholds as starting points; tune them per site. The point isn't precision — it's giving the system a chance to react before users notice.
Layer 5: human escalation, by design
The last layer is the most under-discussed: knowing when to give up. Some failures are simply outside what an autonomous agent should attempt. SMS verification, unusual fraud-prevention flows, an unfamiliar consent dialog with legal weight. You want those to surface to a human, fast.
# escalation.pyimport osimport jsonimport urllib.requestSLACK_WEBHOOK = os.environ["SLACK_WEBHOOK_URL"]def escalate(page, reason: str, last_screenshot: bytes): """Notify a human on Slack. Upload the screenshot to your blob store separately.""" payload = { "text": ( ":rotating_light: *Computer Use agent is asking for human help*\n" f"*URL*: {page.url}\n" f"*Reason*: {reason}\n" "Check the linked screenshot in our bucket for context." ), } req = urllib.request.Request( SLACK_WEBHOOK, data=json.dumps(payload).encode("utf-8"), headers={"Content-Type": "application/json"}, ) urllib.request.urlopen(req, timeout=5)
Two things matter here. First, this is the only automatic exit out of the recovery loop — anything that ends up here gets a human's eyes within minutes. Second, the operator shouldn't have to dig through logs to understand what happened. Bundle the URL, the last action attempted, the expected post-condition, and the screenshot together. I attach a "rerun this job once you've fixed the cause" button on top of the alert and the operational cost drops dramatically.
Putting it all together
Here's the loop that ties the layers together. Replace run_computer_use_step with your real Gemini Computer Use call.
# main_loop.pyfrom playwright.sync_api import sync_playwrightclass StateMismatchError(Exception): def __init__(self, reason: str, suggested: str): self.reason = reason self.suggested = suggestedclass PostConditionError(StateMismatchError): passdef run_step_with_recovery(page, label: str, expectation_after: str): last_error = None for attempt in range(MAX_RETRY): try: click_with_guard(page, label, expectation_after) return # success except StateMismatchError as e: ftype = classify(page, e, {"suggested_recovery": e.suggested}) except Exception as e: ftype = classify(page, e, None) last_error = e proceed = recover(page, ftype, attempt) if not proceed: escalate(page, f"automatic recovery declined ({ftype})", screenshot_bytes(page)) raise RuntimeError(f"unrecoverable: {ftype}") escalate(page, f"max retry reached ({MAX_RETRY})", screenshot_bytes(page)) raise RuntimeError("max retry reached") from last_errordef main(): with sync_playwright() as pw: browser = pw.chromium.launch(headless=True) ctx = browser.new_context(storage_state="auth.json") page = ctx.new_page() page.goto("https://example.com/dashboard") drift = measure_drift("dashboard", screenshot_bytes(page)) print(f"[drift] {drift:.2f}") run_step_with_recovery(page, "Invoices", "Invoice list is visible") run_step_with_recovery(page, "This month", "PDF preview for this month is open") run_step_with_recovery(page, "Download", "A download-complete toast is visible") browser.close()if __name__ == "__main__": main()
Run that under cron or Cloud Run Jobs and the "day three" failure curve flattens out. In one of my own automations the alert volume went from four-or-five times a week to roughly zero-or-one. Not zero, but easily worth the night's sleep.
Pitfalls I hit so you don't have to
These are the mistakes I made on the way to the architecture above. Take a moment with each before you ship.
1. Same temperature for the agent and the judge
Your agent can be a little creative; your judge cannot. Pin the judge to temperature=0.0. Otherwise the same screen will swing between "ok" and "not ok" and your retry counter will start lying to you. (I wrote about this in Stabilizing Gemini API responses with temperature; it applies doubly inside Computer Use.)
2. Trusting wait_for_load_state alone
In SPAs many clicks don't trigger navigation, and networkidle may never fire. Always pair the load wait with a semantic post-condition (assert_state). A blanket timeout is a poor substitute for "I expect to see X now."
3. Full-page screenshots everywhere
full_page=True is slow and often hurts the model's accuracy by flooding the prompt. Use viewport screenshots for guards, and downscaled thumbnails for drift detection. I run my judges at 1280×800 and my drift detector against a 384px thumbnail.
4. Unbounded retry
Wrapping everything in while True feels safe and isn't. When a vendor has a multi-hour outage, your agent will quietly burn API credits forever. Cap by attempts (MAX_RETRY=3 is my default) and by wall time (5 minutes is a reasonable hard ceiling).
5. Logging only the exception text
When a human reviews a failure, the exception alone almost never reproduces the problem. Bundle the URL, the action that was attempted, the expected post-condition, and the screenshot — all four. You will thank yourself in two months.
6. Re-authenticating every run
Login is the most flake-prone step on most sites — CAPTCHAs, 2FA, rate limits. Use Playwright's storage_state to persist auth and refresh it before it expires, instead of logging in each run. This single change alone halved my failure rate.
Migration path: from "we just retry" to the five-layer model
Most teams I talk to don't start with this whole architecture. They start with a working but flaky automation and a vague suspicion that retries aren't enough. Here's the order I'd recommend introducing the layers in, and what each gets you in practice.
Week 1 — add the expectation guard to your single noisiest step. Pick the action that fails most often, and wrap it. Even with no other layers, a guard with a good post-condition catches roughly half of the failures that previously escalated to full reruns. Write the post-conditions in plain language. "An invoice list with at least one row is visible" is a far better expectation than "page loaded."
Week 2 — add the classifier and the recovery playbook for TRANSIENT and STATE_DRIFT. Skip STRUCTURE_CHANGE and SEMANTIC_MISTAKE for now. The transient bucket has clear signals (timeouts, 5xx, connection resets) and exponential backoff is cheap to ship. The state-drift bucket needs a small selector sweep tailored to the cookie banners and modals your specific vendor shows. After this week your agent recovers from "noise" without anyone watching.
Week 3 — turn on Slack escalation. Even before the drift detector exists, having SEMANTIC_MISTAKE and "max retry exceeded" reach a human via Slack is enormously valuable. You'll quickly see whether your guards and classifier are tight or loose. If you're getting too many escalations, the guards are over-strict. If you're getting too few and the automation is still failing silently, the guards are under-specified.
Week 4 — add visual drift detection on a single critical screen. Don't try to monitor every page on day one. Pick the one screen whose redesign would hurt most (usually login or dashboard) and run drift detection on the morning's first run only. Watch the score for a week before adding alerts; you want to learn what "normal noise" looks like before promising the threshold to anyone.
Month 2 — STRUCTURE_CHANGE recovery and prompt familiarity preambles. This is where the architecture pays back the investment. By now you have weeks of structured metrics. You know which screens drift, when, and why. Drop in the structure-change branch of the playbook, and start automatically injecting familiarity preambles when drift exceeds 0.6.
Month 3 — observability and CI dashboards. With three months of NDJSON metrics you can write the weekly report I showed earlier and look at trends. This is also when you'll notice that some failures cluster around specific times of day or specific vendor releases — patterns that are invisible to a single-run debugger.
The reason I recommend this order, rather than building everything before shipping, is that each layer's design depends on what you learn from the previous one. Build the playbook before you have a classifier and you'll guess at the categories; build the drift detector before you have escalation and you'll have nowhere to send the alerts. Self-healing systems are like immune systems — they need to be exposed to real failures to develop the right responses.
What this actually costs to run
A reasonable question while reading the architecture above is: how much of my API budget am I burning on guards and judges? It's worth a paragraph because I've seen teams talk themselves out of pre-conditions on cost grounds when the math doesn't support it.
For a typical step (one button click with a pre- and post-condition guard), you're paying for two judge calls — each is one screenshot plus a short JSON response. With Gemini's current pricing on a Computer Use-capable model, that lands at well under a cent per guarded action. A daily automation that does 20 guarded steps costs you about 20 cents in judge fees. Compare that to the engineering hours you save the first time the guard catches a cookie modal at 2 a.m. and the trade-off is no longer interesting.
The exception is high-frequency automations — say, ones that drive thousands of steps per hour. There, you'll want to drop the guard on steps with reliable post-conditions you can detect cheaply (URL changes, presence of a specific selector via Playwright). Treat the AI judge as a tool you reach for when no cheap deterministic check exists, not as the default. The five-layer architecture isn't dogma — it's a starting kit you trim once you know your traffic profile.
A premium application: pipe the metrics into CI
The setup I'm proudest of is running a single-shot of the automation every morning from CI (GitHub Actions in my case), recording the drift score and failure label per step, and dumping the result to a small dashboard. After a week, patterns appear that no human would catch — "the vendor always reskins on Tuesday" or "modal X reappears on the first business day of the month."
Once you can see the pattern, you can prevent the failure. For the address-confirmation modal that always pops up at month-start, I added this single line to the agent prompt:
PROMPT_PREAMBLE = ( "Note: in the first few days of each month, an address-confirmation modal " "may appear over the page. If it does, dismiss it via 'Close' before " "continuing with the main task.")
STATE_DRIFT events on that automation dropped by roughly 80%. The lesson generalises: regression-testing thinking (covered for plain prompts in prompt regression testing with Pytest) applies to Computer Use too — but with screenshots, not strings, as the canonical input.
Observability: emit metrics so you can argue with reality
A self-healing system without metrics is a black box that pretends to be magic. After a few weeks you'll want to know which automations are actually saving themselves, which are silently degrading, and which are just lucky. The fix is mechanical: emit one structured metric per attempt, then plot.
# metrics.pyimport timeimport jsonimport osfrom datetime import datetime, timezoneMETRIC_FILE = os.environ.get("AGENT_METRIC_FILE", "/var/log/agent/metrics.ndjson")def emit(event: str, **fields): """Append one structured event per line. Easy to ship to BigQuery, Datadog, or grep.""" record = { "ts": datetime.now(timezone.utc).isoformat(), "event": event, **fields, } os.makedirs(os.path.dirname(METRIC_FILE), exist_ok=True) with open(METRIC_FILE, "a", encoding="utf-8") as f: f.write(json.dumps(record, ensure_ascii=False) + "\n")
The events I emit at each layer are deliberately small and consistent:
guard_check with ok=true|false, step="Invoices", expectation="Invoice list visible"
failure_classified with type="state_drift", step="Invoices", error="cookie modal in foreground"
recovery_attempted with type="state_drift", attempt=1, outcome="success"
escalated with reason, step, total_attempts
drift_measured with name="dashboard", score=0.42
A week of these events is enough to answer questions you currently can't. Which step has the highest classifier-to-success ratio? When does drift correlate with failures? Are we recovering more or less than last month? Without metrics those are gut-feel arguments. With metrics they become spreadsheets.
A practical tip: write metrics to NDJSON locally first, then ship to whatever data store you prefer. Trying to write directly to a SaaS observability product from inside the agent loop adds another failure surface — exactly the thing you're trying to remove.
A simple weekly report
Once you have metrics, a five-line aggregator is enough to feel productive:
# weekly_report.pyimport jsonfrom collections import Counterc = Counter()with open("/var/log/agent/metrics.ndjson") as f: for line in f: rec = json.loads(line) if rec["event"] == "failure_classified": c[rec["type"]] += 1for label, n in c.most_common(): print(f"{label:>20s} {n}")
Run that on Monday morning. If state_drift is climbing, your sweep selectors are out of date. If structure_change is climbing, the vendor is in a redesign cycle and your prompts need a familiarity preamble. If semantic_mistake is climbing, your task description has drifted from what the model can interpret reliably — usually a sign the prompt has accumulated too many caveats and needs a rewrite.
Edge cases worth a paragraph each
A few situations that don't fit cleanly into the five-layer model and deserve their own treatment.
Captchas and 2FA
These are the failures most likely to look like SEMANTIC_MISTAKE. Don't try to outsmart them. Detect them with a guard ("Is there a CAPTCHA challenge on screen?") and escalate immediately. The cost of a human handling a CAPTCHA every few weeks is dramatically lower than the cost of debugging an agent that occasionally bypasses one and gets your account banned.
File uploads
Computer Use can drive a file picker, but the picker is operating-system chrome and varies between platforms. A safer pattern is to bypass the picker entirely with Playwright's set_input_files against the underlying <input type="file">, then resume the agent for the next step. This is the rare case where the older, deterministic API beats the agentic one — use it.
Pages with infinite scroll
If a target item is below the fold, Computer Use will sometimes loop on a screen that doesn't change. Add an explicit guard: "Is the item visible in the current viewport?" If not, scroll programmatically and re-observe. Don't trust the model to discover that scroll is necessary on its own — telling it explicitly is cheaper than the alternative.
Multi-tab flows
Some workflows open new tabs (downloads, OAuth windows). Track tab handles in your code and pass the right Page object into assert_state and run_step_with_recovery. A surprising amount of "the agent stopped responding" turns out to be "the agent is correctly waiting on the wrong tab."
Browser context restarts
Long-running Chromium sessions accumulate memory and weird state. After a hard escalation, throw the browser context away and re-launch from storage_state. It costs you a few seconds and resets a class of failures that don't show up in any log.
What to try tomorrow
Thank you for reading this far. If I had to suggest one concrete first step: pick the single noisiest action in your current automation and wrap just that with assert_state(). Don't try to install all five layers in one weekend. One pre-condition on one action will visibly reduce night-time alerts, and from there it's easier to justify adding the classifier, the playbook, the drift detector, and finally the escalation hook.
Browser automation with Computer Use is still early. The architecture in this article is my current best, not a finished standard. If you find a better arrangement in your environment, I'd love to hear about it — this is the kind of craft that gets sharper when more practitioners share notes.
Share
Thank You for Reading
Gemini Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.