Skip to content

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.

curl -N http://localhost:10100/srv/events/stream

The 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.

Each message is named by its type. The run lifecycle is:

TypeMeaning
run-startedA chain or suite run began.
step-startedOne step of a chain began.
step-finishedOne step ended, carrying success and a detail string.
chain-startedInside a suite, one member chain began, with its phase (setup, test or teardown).
chain-finishedThat member chain ended, with success and detail.
run-finishedThe 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 suite suiteId
  • chainId, plus the chain or suite name in stepName on the run and chain events, so you never have to keep your own identifier to name lookup table
  • stepIndex and stepTotal for a progress fraction
  • timestamp and a monotonic seq, so a consumer that reconnects can drop duplicates and measure a stalled step
  • schemaVersion, bumped only when a field changes meaning
  • source: 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.

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.

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 EventSource cannot 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.

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.

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.