Follow a run live
After this page you can submit a turn, watch the agent work, answer a question it asks, detect the exact moment the run reaches a terminal state, and pick the stream back up after your process restarts without losing or duplicating a single event.
Examples use $WAMP_API for the API origin your operator gives you — written as
https://api.example.com throughout these guides — and $WAMP_TOKEN for a
short-lived app-installation bearer, which
Authentication explains how to mint.
The mechanism is a cursor, not a stream
Section titled “The mechanism is a cursor, not a stream”There is one public way to follow a session:
GET /v1/sessions/{sessionId}/events?after=<sequence>&limit=<1..100>It is an ordinary JSON request that returns a page and closes. It is not Server-Sent Events, not a long poll, and not a WebSocket — the request returns immediately with whatever has been recorded so far, including an empty page. You follow a run by calling it repeatedly with the cursor from the previous response.
{ "events": [ { "id": "6f1c1d80-6c2a-4f0e-9d5f-0c2a2f0f4b11", "type": "wamp.run.started", "createdAt": "2026-08-11T09:00:01.412Z", "sequence": 1, "visibility": "summary", "subject": { "sessionId": "3f7d4f4c-2b6a-4a2e-9c1a-1f2b3c4d5e6f", "runId": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d" }, "data": { "status": "running" } } ], "nextAfter": 1, "hasMore": false}That shape is deliberate. Webhooks can push you a notification that something happened (see Receive webhooks), but the events themselves always come from this endpoint, in this order, so there is exactly one place your integration reads truth from.
Guarantees you can rely on
Section titled “Guarantees you can rely on”| Guarantee | What it means for your loop |
|---|---|
sequence is an integer starting at 1 |
It is the cursor. Nothing else is. |
| Sequences are contiguous per session | You may assert sequence === previous + 1 and treat a gap as a bug, not as loss. |
after is exclusive, default 0 |
after=0 means “from the very beginning”. |
| Events are append-only and immutable | A replayed page is byte-identical. Reprocessing is safe if your handlers are. |
| Duplicates are removed before storage | The runtime that produces events retries at-least-once; the log dedupes, so the same fact never appears twice. |
nextAfter never regresses |
An empty page returns the after you sent, so an unconditional loop cannot lose its place. |
visibility is always "summary" |
The literal summary is the only value /v1 serves. |
limit is 1–100, default 100 |
?limit=0, ?limit=101 and ?limit=abc are each a 400. |
The log is retained for as long as the session row exists, and archiving a
session is a soft delete, so a cursor is still valid after an outage of
arbitrary length. If you lose the cursor entirely, replay from after=0.
When events appear
Section titled “When events appear”Events are written at the run’s boundaries, not continuously as the model produces tokens. Concretely:
wamp.run.startedlands once, when a worker observes the run executing in a sandbox.- Everything the run produced — assistant messages, tool activity, artifacts —
is committed in one batch when the run reaches a terminal state or pauses
to ask a question. Within that batch, the transcript comes first and the
terminal
wamp.run.*event is last.
That ordering is a guarantee worth designing around: by the time you see
wamp.run.completed, every event belonging to that run already has a lower
sequence and is already in your hands. A loop that stops at the terminal event
has not missed anything.
The practical consequence is that this is a progress feed, not a token stream.
If your product needs a live typing indicator, drive it from run.status and
wamp.run.started; do not expect partial assistant text to trickle in.
Submit the turn
Section titled “Submit the turn”A turn and its run are one durable pair that shares one identifier: the
turnId you mint is also the runId. You therefore know the run id before
the request returns.
-
Mint a UUID for the turn.
Terminal window TURN_ID=$(uuidgen | tr 'A-Z' 'a-z') -
PUTit. Creation is idempotent on that id — see API conventions.Terminal window curl -sS -X PUT \"$WAMP_API/v1/sessions/$SESSION_ID/turns/$TURN_ID" \-H "Authorization: Bearer $WAMP_TOKEN" \-H 'Content-Type: application/json' \-d '{"message":"Add a regression test for the date parser."}'The response is
202 Accepted:{"turn": { "id": "…", "ordinal": 2, "dispatch": "pending", "run": { "id": "…" } },"run": { "id": "…", "status": "queued", "attempts": 0, "cancellationRequested": false },"created": true}202is honest: the turn and aqueuedrun are committed to Postgres, and nothing has been scheduled onto a sandbox yet.created: falsemeans this was an exact replay of a turn you already submitted. -
Start polling events from the cursor you last persisted for this session — not from
0, unless this is the first turn.
Detect the terminal state
Section titled “Detect the terminal state”Four event types close a run, and each carries the final status in data:
| Event | data.status |
|---|---|
wamp.run.completed |
completed |
wamp.run.failed |
failed |
wamp.run.cancelled |
cancelled |
wamp.run.crashed |
crashed |
wamp.run.started (data.status: "running") is the only non-terminal run
event. Match on subject.runId so a terminal event from an earlier run in the
same session does not end your loop early.
Terminal run events may also carry stopReason, costUsd and turns:
{ "type": "wamp.run.completed", "subject": { "sessionId": "…", "runId": "…" }, "data": { "status": "completed", "stopReason": "completed", "costUsd": 0.0123, "turns": 3 }}stopReason is one of completed, aborted, max_turns, max_response,
error, cost_budget, turn_budget, time_budget, loop_detected.
The authoritative check, if you want one that does not depend on having
observed an event, is GET /v1/sessions/{sessionId}/runs/{runId}. A run has a
completedAt timestamp exactly when its status is terminal — that
equivalence is enforced by a database constraint, so completedAt != null is a
sound “is it done” test.
Handle a question mid-run
Section titled “Handle a question mid-run”An agent can block on a human decision. When it does, the run moves to
awaiting and you receive:
{ "type": "wamp.interaction.opened", "data": { "interactionId": "9d1f0a3c7b2e4d8f", "kind": "ask_user", "request": { "questions": [ { "question": "Which package should the test live in?", "options": ["core", "cli"], "multiSelect": false, "allowCustom": true } ] } }}interactionId is an opaque string of at most 255 characters minted by the
runtime. Store it and echo it back; do not parse it or assume a format.
You answer by submitting a normal turn that names the interaction:
curl -sS -X PUT \ "$WAMP_API/v1/sessions/$SESSION_ID/turns/$(uuidgen | tr 'A-Z' 'a-z')" \ -H "Authorization: Bearer $WAMP_TOKEN" \ -H 'Content-Type: application/json' \ -d '{"message":"core","replyTo":{"interactionId":"9d1f0a3c7b2e4d8f"}}'Then keep polling — the same run continues, and you will see
wamp.interaction.resolved carrying resolvedByTurnId. Two failure modes are
worth coding for:
- Submitting a plain turn while an interaction is open is
409 cloud_turn_unresolved. - Answering an interaction that is already resolved, expired, or not the open
one is
409 cloud_interaction_conflict.
An interaction can also expire on its own
(wamp.interaction.expired, reason one of workspace_lost,
session_archived, run_cancelled); after that it can no longer be answered.
If your process restarted and you do not know what is outstanding,
GET /v1/sessions/{sessionId}/interactions returns only the open ones,
oldest first. It is the actionable edge, not history — history comes from
events.
A complete loop
Section titled “A complete loop”This is the whole protocol, with no SDK and no dependencies. It persists the cursor through the caller, asserts the gap-free invariant, answers nothing and stops at the terminal event for one specific run.
const API = process.env.WAMP_API; // e.g. https://api.example.com — your operator gives you thisconst TOKEN = process.env.WAMP_TOKEN;const auth = { Authorization: `Bearer ${TOKEN}` };
const TERMINAL = new Set([ 'wamp.run.completed', 'wamp.run.failed', 'wamp.run.cancelled', 'wamp.run.crashed',]);
async function get(path) { const res = await fetch(`${API}${path}`, { headers: auth }); if (!res.ok) { const body = await res.json().catch(() => ({})); const err = new Error(`HTTP ${res.status} (${body.error ?? 'unknown'})`); err.status = res.status; err.code = body.error; err.retryAfter = Number(res.headers.get('retry-after')) || undefined; throw err; } return res.json();}
/** Follow one run. `after` is the cursor you persisted for this session. */export async function followRun(sessionId, runId, after = 0) { let cursor = after; let seen = cursor;
for (;;) { const page = await get( `/v1/sessions/${sessionId}/events?after=${cursor}&limit=100`, );
for (const event of page.events) { if (event.sequence !== seen + 1) { throw new Error(`event gap: expected ${seen + 1}, got ${event.sequence}`); } seen = event.sequence;
handle(event); // your own dispatch
if (TERMINAL.has(event.type) && event.subject.runId === runId) { // Persist the cursor BEFORE returning, so a crash here replays nothing. await saveCursor(sessionId, event.sequence); return { status: event.data.status, cursor: event.sequence }; } }
cursor = page.nextAfter; // never regresses, safe when empty await saveCursor(sessionId, cursor);
if (page.hasMore) continue; // page was capped — no sleep await new Promise((r) => setTimeout(r, 1000)); }}
function handle(event) { switch (event.type) { case 'wamp.message.created': console.log(event.data.text); // a whole message, not a token break; case 'wamp.activity.completed': console.log(`[${event.data.category}] ${event.data.name} → ${event.data.status}`); break; case 'wamp.interaction.opened': console.log('needs an answer:', event.data.interactionId); break; case 'wamp.artifact.created': console.log('artifact:', event.data.kind, event.data.artifactId); break; default: break; // unknown types are ignored, by contract }}Resuming after a disconnect
Section titled “Resuming after a disconnect”There is no resume token, no Last-Event-ID, and no subscription state on the
server. The cursor is the resume protocol:
- Persist
nextAfter(or thesequenceof the last event you fully handled) in your own storage, keyed by session. - On restart, call the same endpoint with that value.
- If you handled an event but crashed before persisting, you will see it
again. Make handlers idempotent on the event
id, which is stable across replays.
Persisting after handling gives you at-least-once processing; persisting before gives you at-most-once. Choose deliberately — the server cannot make that choice for you.
How often to poll
Section titled “How often to poll”Nothing in the API prescribes an interval. What constrains you is one token bucket shared by every call your installation makes, and every response tells you where you stand in it:
RateLimit-Policy: "wamp-cloud";q=600;w=60;wamp-burst=120RateLimit: "wamp-cloud";r=117;t=59Read r (remaining) rather than hardcoding a rate — the policy is operational
flow control that an operator can tune, not an entitlement. At the shipped
default, one poll per second per active session is comfortable. See
Limits. When you exceed it you get 429 with
Retry-After; honor it rather than tightening the loop.
After the run
Section titled “After the run”A terminal run event is not the end of the work you care about.
- Results.
GET /v1/sessions/{sessionId}/artifactslists what the run produced — aresult.summarymarkdown document, files the agent chose to present, and agithub.pull_requestrecord if it published one. Fetch bytes from…/artifacts/{artifactId}/content. - Transcript. Every
wamp.message.createdandwamp.activity.completedevent you already paged through is the transcript. It is bounded on purpose: assistant text and tool names with success or failure, never raw tool inputs, tool outputs, or model reasoning. - Continuation.
GET /v1/sessions/{sessionId}returns acontinuationobject telling you whether another turn can pick up where this one stopped, and at what fidelity, if the sandbox is gone. - Next turn. Only one run per session may be non-terminal at a time.
Submitting a turn while one is in flight is
409 cloud_run_in_progresswithRetry-After: 2.
Related
Section titled “Related”- Events reference — every event type and payload.
- Errors — what is retryable and what is terminal.
- Events operations — the generated endpoint reference for this path.
- Turns and runs operations — the generated reference for submitting, reading, and cancelling.