Picture an evening spent watching Gemini drive a browser on your screen. A search page opens, text lands in the input field, and just before the button should be pressed, everything stops. No error appears. Your script prints a normal exit.
On October 30, 2026, the Gemini API changelog added an entry titled "Computer Use generally available (GA)". The Computer Use tool is now GA across both the Interactions API and the GenerateContent API. Whether you started in preview or are only trying it now, this "it just ended" moment is likely to come up for more people.
What I'd like to say first is that, quite often, the loop didn't stop. It ended because it was written to end there. Before doubting the model, you can start by counting the loop's exits.
Where the official loop is written to end
The official Computer Use walkthrough follows roughly this shape:
- Send the prompt, an initial screenshot, and the tool config to
client.interactions.create - Read the returned
function_callsteps and run each action in your own environment (the examples use Playwright) - Take a fresh screenshot and send it back as a
function_result, withprevious_interaction_id - Repeat until the model stops returning
function_call
You enable the tool with something like tools=[{"type": "computer_use", "environment": "browser"}], and environment can be browser, mobile, or desktop.
One thing is easy to miss here. The signal for "done" is not an error. It is simply that no function_call came back. A normal finish and an interruption look the same. The official example also caps the loop with turn_limit = 5. If you copy that number as-is, the loop ends at step five without a word.
Log the four exits so you can tell them apart
As far as I can count from the official description, a loop can end for four reasons.
| Exit | What happened | Clue in your logs |
|---|---|---|
| 1. The model finished | A response arrived with no function_call. The model considers the task done, or cannot continue | The last response text reports completion or says what it couldn't do |
| 2. Turn cap | Your own turn_limit was reached | The loop count equals the cap |
| 3. Waiting on confirmation | A safety_decision of require_confirmation arrived, and confirmation was declined or the code aborted | The last function_call carries a safety_decision whose decision is require_confirmation |
| 4. A bad return | function_result wasn't sent, previous_interaction_id wasn't chained, or the screenshot was empty or in the wrong format | An exception in the log, or a break just before the next create call |
Writing one line at the end of the loop is enough to tell them apart. The skeleton below borrows the official structure. capture_screenshot and execute_action are yours to supply (Playwright, for example), and the details of the API call should follow the official sample.
# Skeleton: always leave one line saying why the loop ended
TURN_LIMIT = 20 # the official example uses 5; don't carry a prototype cap into production
exit_reason = None
prev_id = None
turn = 0
while True:
turn += 1
if turn > TURN_LIMIT:
exit_reason = f"turn_limit({TURN_LIMIT})"
break
try:
interaction = send_to_model(prev_id) # your wrapper around create
except Exception as e:
exit_reason = f"api_error:{type(e).__name__}"
break
prev_id = interaction.id
calls = [s for s in interaction.steps if s.type == "function_call"]
if not calls:
exit_reason = "model_finished_no_function_call"
break
results = []
for call in calls:
decision = getattr(call, "safety_decision", None)
if decision and decision.decision == "require_confirmation":
if not ask_user(decision.explanation):
exit_reason = "safety_confirmation_declined"
break
results.append(execute_action(call)) # return a screenshot and the URL too
if exit_reason:
break
print(f"[computer_use] exit={exit_reason} turns={turn}")The point of this code is that no path leaves with exit_reason empty. Once every break has a name, the next morning's log tells you which exit it was, and that decides what to look at next.
Exit four looks like a stop, but the loop could have continued
Exit four is the hardest to spot. The screen stops moving and nothing is raised. From the official text I could read three things to get right when returning results:
- Return the current URL and a PNG image in
function_result(base64, withmime_typeset toimage/png) - Pass
previous_interaction_idon the follow-up call - Attach a screenshot to the very first request too
On coordinates, the official description wavers a little. One example converts coordinates in the 0 to 999 range into pixels, while another passage describes pixel predictions scaled to the screen. I couldn't settle which is correct from that single page. If clicks always land slightly off and nothing happens before the loop ends, whether you convert is worth suspecting.
The example's typing handler also uses Meta+A, which is the macOS way to select all. On Windows or Linux browsers the key differs, so old text may remain in the field and the model may conclude it couldn't type. That path is my guess, not something the docs state.
Exit three, waiting on confirmation, is a correct way to stop
When safety_decision arrives with require_confirmation, you ask the user, and if they agree, set safety_acknowledgement on the result and continue. The docs describe safety overrides as only a preference and say you should always implement safety_decision handling.
The safety policy categories include financial transactions, sensitive data modification, communication tools, account creation, data modification, consent management, and legal agreements. An action that submits something in a store console is likely to fall into one of them. As an indie developer who would like to hand some console chores to a browser agent, I read this stop as something worth keeping rather than an inconvenience.
If the reason turns out to be exit three, don't fix the loop; fix the side that shows the confirmation. If you run unattended, for instance overnight, the cleaner line is not to delegate that action at all.
A small preparation before you start
The docs warn that Computer Use may contain errors and security vulnerabilities. They recommend close supervision, not using it for critical decisions, sensitive data, or irreversible actions, and running it in a sandboxed VM or container. Prompt injection detection (enable_prompt_injection_detection) is off by default and requires Gemini 3.5 Flash-Lite or later. The model name in the official example is gemini-3.8-flash.
I'd start with read-only work that sends nothing outside, and widen the scope only once the exit reason reads clearly in a single line. For problems where the agent acts on a stale screen, see Gemini Computer Use quietly acting on a stale screen, and for recovery design after failures, Self-healing architecture for Gemini Computer Use. This article sits one step earlier: why it ended at all.
Where I'd draw the line
What stays with me after writing this is that automation failures linger less because "it didn't work" and more because "I couldn't read why". In a prototype, ending after five steps barely registers. Forget the cap and move it into operation, and you spend the night just hunting for the reason.
I won't hand over a loop whose ending I can't read in one line. That's the line I've set for myself after this check.
A good first step is to add one exit_reason line at the end of your own loop, so that the next time it stops, the exit has a name.