Live events & screen
Stream what the agent is doing, each step as it finishes and the browser's screen while a run works, instead of waiting for the result.
A run takes seconds to minutes. Instead of waiting for the end, stream it: you see what the agent is doing right now, each step as it finishes, the run's state whenever it changes and, if you ask, the browser's screen. The stream ends with the finished run, answer, data and charge_usd included.
Streams use Server-Sent Events (text/event-stream), the format LLM APIs use for streaming. Watching a run is free.
Stream a new run#
Add "stream": true when you create a run. The response is the event stream, with status 200, instead of the 202 JSON. Add "screen": true to get the browser's screen too.
curl -N https://lovelyrouter.com/v1/browser/runs \
-H "Authorization: Bearer $LOVELY_API_KEY" \
-H "content-type: application/json" \
-d '{
"goal": "Find the price of the book \"A Light in the Attic\" and whether it is in stock.",
"start_url": "https://books.toscrape.com/",
"stream": true
}'The stream looks like this (abridged):
event: run
data: {"id":"brun_3f9a1c7e0b2d4e6f8a9b0c1d","object":"browser_run","status":"running","steps":0,"trace":[],"poll":"/v1/browser/runs/brun_3f9a1c7e0b2d4e6f8a9b0c1d?wait=30","events":"/v1/browser/runs/brun_3f9a1c7e0b2d4e6f8a9b0c1d/events","charge_usd":null,"reserved_usd":0.445,…}
event: activity
data: {"step":0,"kind":"starting","text":"Opening https://books.toscrape.com/"}
event: activity
data: {"step":1,"kind":"reading","text":"Reading the page"}
event: activity
data: {"step":1,"kind":"acting","text":"click link \"A Light in the ...\" → https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html"}
event: step
data: {"step":1,"url":"https://books.toscrape.com/","action":"click:e54","description":"click link \"A Light in the ...\" → https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html","ms":2140,"error":null}
event: run
data: {"id":"brun_3f9a1c7e0b2d4e6f8a9b0c1d","status":"running","steps":1,…}
event: activity
data: {"step":2,"kind":"answering","text":"Reading the result and writing the answer"}
event: step
data: {"step":2,"url":"https://books.toscrape.com/catalogue/a-light-in-the-attic_1000/index.html","action":"done","description":"Read the result and wrote the answer","ms":2875,"error":null}
event: run
data: {"id":"brun_3f9a1c7e0b2d4e6f8a9b0c1d","status":"succeeded","answer":"\"A Light in the Attic\" costs £51.77 and is in stock (22 available).","charge_usd":0.012,"reserved_usd":0,…}
event: end
data: {"id":"brun_3f9a1c7e0b2d4e6f8a9b0c1d"}The first run event carries what the 202 response would: poll, events, and webhook_secret when you passed a webhook. Errors before the run starts (400, 402, 429 and so on) come back as JSON with their status, the same as without stream.
Follow a run that is already going#
Open GET /v1/browser/runs/{id}/events at any time: from another process, after a dropped connection, or for a run you started without stream. Add ?screen=1 for the screen. Several streams can watch the same run.
curl -N "https://lovelyrouter.com/v1/browser/runs/brun_3f9a1c7e0b2d4e6f8a9b0c1d/events?screen=1" \
-H "Authorization: Bearer $LOVELY_API_KEY"Every stream starts with a run event that has the full trace, so reconnecting loses nothing. A finished run sends that run event (and its last screen, with screen) followed by end.
Events#
| Event | When | data |
|---|---|---|
run | First, after every step, when a pause begins or ends, and last | The run object with charge_usd and reserved_usd. The first one has trace. |
activity | The agent starts something | step (the step in progress), kind and text |
step | A step finished | One trace entry: step, url, action, description, ms, error |
frame | With screen: on connect, then whenever the page changes | data, a base64 JPEG of the browser, and at (ISO 8601) |
end | The run is over; the stream closes next | id |
Activity kinds:
kind | The agent is |
|---|---|
starting | starting the browser or opening start_url |
reading | reading the page |
waiting | waiting for a page that has not loaded, or loading it again |
deciding | choosing the next action |
thinking | looking more closely before it decides (accurate mode may use a screenshot) |
checking | checking a form's values before it submits |
acting | doing the action text describes: a click, typing, a choice, an upload |
answering | reading the result and writing the answer |
extracting | turning what it read into data |
Lines that start with : are keep-alive comments, sent every 15 seconds while nothing else happens. Ignore them.
Watch the screen#
With "screen": true (or ?screen=1), frame events carry the browser's screen as base64 JPEG, usually 20 to 60 KB each. A frame arrives when you connect and whenever the page changes, at most about 4 a second; a page that holds still sends nothing new. If your reader falls behind, frames are skipped, never other events.
To show it on a web page, put the frame in an image:
img.src = `data:image/jpeg;base64,${frame.data}`;For a single picture without a stream, GET /v1/browser/runs/{id}/screenshot returns the screen as it is now.
Pauses for a person#
The stream stays open while a run is waiting_for_human. The run event that starts the pause has needs; answer it with POST /v1/browser/runs/{id}/reply from anywhere, and the stream carries on with a run event back at running. With screen, you also see what a person does in the browser during a takeover. See Human in the loop.
Streams in a web app#
Keep your API key on your server. Browsers' EventSource can't send an Authorization header, so open the stream from your server and pass it through to your page unchanged, with content-type: text/event-stream. Your page can then use EventSource on your own URL.
Reconnect#
A stream can drop: a network blip, a proxy timeout, a deploy on our side. The run keeps working. Open GET /v1/browser/runs/{id}/events again and replace your state with the first run event. EventSource reconnects by itself, after 3 seconds. Stop when you have seen end or a run with a final status.