Skip to content
Try out in chatDeveloper dashboardLogin

Observability: live view, screencast, and traces

Watch sessions live in the dashboard, stream video and events over SSE, record runs to MP4, and trace every action.

Every computer is fully observable while it runs: a live view in the dashboard, a raw video stream, an event stream of every action, and per-step task traces. This page covers each surface and when to use it.

Every computer has a live view at:

https://lightcone.ai/c/{computer_id}

Open it in a browser to watch the session in real time, no code required. After the session ends, the same URL serves the replay.

When you run a Task, the started event in the stream carries the computer_id, so you can link to the live view as soon as the run begins:

stream = client.agent.tasks.start_stream(instruction="...", kind="browser")
for event in stream:
if event.get("type") == "started":
print(f"Watch live: https://lightcone.ai/c/{event['computer_id']}")
const stream = await client.agent.tasks.startStream({ instruction: "...", kind: "browser" });
for await (const event of stream) {
if (event.type === "started") {
console.log(`Watch live: https://lightcone.ai/c/${event.computer_id}`);
}
}

GET /computers/{id}/screencast is a Server-Sent Events stream of the session’s video. The event shapes depend on the session kind:

Session kindSSE eventsPayload
BrowserUnnamed (default) eventsJSON with a base64-encoded JPEG frame in image_data, plus metadata (timestamp, viewport dimensions)
Desktopevent: h264JSON with nalu_data, base64-encoded raw H.264 NAL units, suitable for a WebCodecs VideoDecoder
Desktopevent: cursor_updateCursor sprite bitmap and hotspot; emitted only when the cursor shape changes
Desktopevent: cursor_positionCursor x/y; emitted once per video frame while the cursor moves

A : heartbeat SSE comment is sent every 30 seconds to keep intermediaries from closing the connection. Action responses are deliberately excluded; those live on /events.

import json
with client.computers.with_streaming_response.retrieve_screencast(computer.id) as response:
for line in response.iter_lines():
if line.startswith("data: "):
frame = json.loads(line[len("data: "):])
# browser: frame["image_data"] is a base64 JPEG
const response = await client.computers.retrieveScreencast(computer.id!);
// Parse the SSE body: unnamed events carry JPEG frames (browser),
// named "h264" events carry NAL units (desktop).

Pipe the screencast into ffmpeg to produce a video file. For a desktop session, decode the base64 NAL units and write them to ffmpeg’s stdin:

import base64
import json
import subprocess
from tzafon import Lightcone
client = Lightcone()
computer = client.computers.create(kind="desktop")
ffmpeg = subprocess.Popen(
["ffmpeg", "-y", "-i", "pipe:", "-c:v", "copy", "recording.mp4"],
stdin=subprocess.PIPE,
)
try:
with client.computers.with_streaming_response.retrieve_screencast(computer.id) as response:
for line in response.iter_lines():
if not line.startswith("data: "):
continue # skips "event: ..." lines and ": heartbeat" comments
payload = json.loads(line[len("data: "):])
if "nalu_data" in payload:
ffmpeg.stdin.write(base64.b64decode(payload["nalu_data"]))
finally:
ffmpeg.stdin.close()
ffmpeg.wait()
client.computers.delete(computer.id)

For a browser session the frames are JPEGs, so use image2pipe instead and write the decoded image_data bytes:

ffmpeg = subprocess.Popen(
["ffmpeg", "-y", "-f", "image2pipe", "-framerate", "10", "-i", "pipe:", "recording.mp4"],
stdin=subprocess.PIPE,
)
# ... ffmpeg.stdin.write(base64.b64decode(payload["image_data"]))

GET /computers/{id}/events is an SSE stream of every action on the session (video excluded), which makes it a step-by-step trace of programmatic control. Each data: line is a JSON object in one of three categories:

CategoryWhat it carries
Action requestAn action has been queued for execution
Action responseA completed action, with a request_id for correlation; shell commands include exit_code, stdout, stderr
Command output chunkStreaming output from a long-running /exec command: request_id, base64 data, is_stderr

GET /computers/{id}/ws is the WebSocket variant of the same stream, with one addition: it’s bidirectional, so you can send actions over the same connection instead of making HTTP calls. Use it when you want a single connection for both control and observation.

For Tasks, the SSE stream is itself the trace. Each step of the run emits events you can log or replay:

EventWhat it carries
screenshotURL of the screenshot the agent saw at this step
executedThe action the agent took
thinking / statusThe agent’s reasoning and progress updates
planThe agent’s current plan
stream = client.agent.tasks.start_stream(instruction="...", kind="browser")
for event in stream:
print(event) # persist these; they are the full run trace
const stream = await client.agent.tasks.startStream({ instruction: "...", kind: "browser" });
for await (const event of stream) {
console.log(event); // persist these; they are the full run trace
}

For a tight see-act loop you don’t need a separate screenshot call after each action. Add ?screenshot_after=true to any action endpoint and the response includes a post-action screenshot taken after the UI settles:

result = client.computers.click(
computer.id,
x=100,
y=200,
extra_query={"screenshot_after": "true", "settle_ms": "500"},
)
const result = await client.computers.click(computer.id!, { x: 100, y: 200 }, {
query: { screenshot_after: "true", settle_ms: "500" },
});
Query paramEffect
screenshot_after=trueReturn a post-action screenshot in the same response
settle_ms=NWait up to N ms for the UI to settle before capturing
base64=trueReturn the fused frame as base64 in the response body instead of a URL

This halves the round trips in a screenshot-act-screenshot loop and guarantees the frame reflects the action you just took.