The event stream
Every chain and suite run on a VirtuProbe instance is published to a live event stream as it happens. You subscribe once and receive each run as it starts, each step as it finishes, and the final verdict, without knowing anything about the run in advance.
That last part is the point. A run you did not start yourself, an agent’s run or one kicked off from elsewhere, was given an identifier you were never told, so a feed keyed on a single run cannot show it to you. This feed is keyed on the instance. If something ran, you can watch it.
The stream
Section titled “The stream”curl -N http://localhost:10100/srv/events/streamThe endpoint is GET /srv/events/stream and it answers as Server-Sent Events
(text/event-stream). It stays open and each event arrives on its own as a named SSE message
carrying the full event as JSON. Nothing is buffered until the run ends, so a long chain reports
step by step while it is still running.
What arrives
Section titled “What arrives”Each message is named by its type. The run lifecycle is:
| Type | Meaning |
|---|---|
run-started | A chain or suite run began. |
step-started | One step of a chain began. |
step-finished | One step ended, carrying success and a detail string. |
chain-started | Inside a suite, one member chain began, with its phase (setup, test or teardown). |
chain-finished | That member chain ended, with success and detail. |
run-finished | The whole run ended, carrying the overall success and detail. |
A suite reuses the same shape one level up: chain-started and chain-finished bracket each
member chain, and that chain’s own step-started and step-finished events arrive in between, so a
consumer can nest the steps under their chain.
Every event also carries:
runId, and for a suitesuiteIdchainId, plus the chain or suite name instepNameon the run and chain events, so you never have to keep your own identifier to name lookup tablestepIndexandstepTotalfor a progress fractiontimestampand a monotonicseq, so a consumer that reconnects can drop duplicates and measure a stalled stepschemaVersion, bumped only when a field changes meaningsource: who caused the run. Absent for a person driving the app, otherwise the agent or session that started it
Two further types ride the same feed and describe what an agent is doing rather than a run:
agent-action when an agent changes the workspace, for example creating a probe or saving a chain,
and attempt-blocked when an agent that has not been approved tries to execute something. Both are
covered under Watching what an agent does.
Narrowing the feed
Section titled “Narrowing the feed”Three query parameters filter the stream, and they combine.
Only runs an agent triggered, tagged by the transport that started them:
curl -N "http://localhost:10100/srv/events/stream?source=mcp"Only a single chain, by id:
curl -N "http://localhost:10100/srv/events/stream?chainId=<chain-id>"Only the parts worth interrupting you, the failures and the final verdict:
curl -N "http://localhost:10100/srv/events/stream?failuresOnly=true"Two related endpoints answer questions about the stream itself rather than delivering it.
GET /srv/events/subscribers reports how many watchers are attached, and GET /srv/events/agents
reports which agents are currently talking to this instance.
Consuming it from a browser
Section titled “Consuming it from a browser”A browser can read the stream with the native EventSource API, with one constraint that decides
your setup: EventSource cannot send an Authorization header. VirtuProbe requires the
access token only once the server is exposed beyond your own machine, so:
- On your machine (the default). The server binds loopback and the token is not enforced, so
new EventSource('http://localhost:10100/srv/events/stream')works directly. If your page is served from a different origin, add that origin to the server’s allowed origins first, otherwise the browser blocks the request. - Exposed on a network. The token is required, and
EventSourcecannot carry it. Read the stream from your own backend, where setting a header is trivial, and re-emit it to the page. A fetch based SSE reader, which can set headers, is the other option if you want the browser to read it directly.
Reading the stream server-side also sidesteps the cross origin question entirely, which is why it is the cleaner choice for anything beyond a local page.
When you already know the run
Section titled “When you already know the run”If your own code started the run, you already hold its identifier, and a per-run WebSocket is simpler than filtering the whole feed:
/exec-feed-ws?runId=<your-run-id>Generate the runId, subscribe to the socket, then call the streaming run endpoint on
chains or suites with that same id. The run returns
immediately and its progress arrives on the socket. This is the path the app’s own run views use.
Reach for the event stream instead when you want to watch runs you did not start.
Access
Section titled “Access”Watching is not a paid capability and is not feature gated. The events carry no more than the run history already exposes, and observing your own instance is not something you should have to buy.
Access is controlled the same way as the rest of the API: your machine only by default, and a bearer token once you expose it. See API access and the access token.