Skip to content

Sessions, turns, runs

After this page you can model a WAMP Cloud conversation in your own database: you will know which identifiers you own, which states each object can be in, which transitions are legal, and which of them your code has to handle rather than assume away.

Object What it is
Session A durable conversation with an agent, plus the intent for its workspace. It owns everything else — turns, runs, events, artifacts, publications. It survives the compute it ran on.
Turn One instruction you submitted. Immutable once accepted, numbered by ordinal starting at 1.
Run The execution of exactly one turn: the agent working, retrying, waiting on you, and finishing.

A session is the long-lived thing you store a reference to. A turn is what you send. A run is what you watch.

This is the one non-obvious fact in the API, and everything downstream is easier once you hold it:

PUT /v1/sessions/{sessionId}/turns/{turnId} → creates turn {turnId}
→ and run {turnId}
GET /v1/sessions/{sessionId}/runs/{turnId} → that run

A turn and its run are a 1:1 pair sharing a single primary key. Because you mint the turnId yourself, you know the runId before the submit request returns — there is no id to parse out of a response body, and no window in which you have submitted work you cannot yet address. The 202 response still echoes both (turn.run.id and run.id) and sets Location to the run, but you never have to wait for it.

The practical consequences:

  • Cancel with the same id: POST /v1/sessions/{sessionId}/runs/{turnId}/cancel.
  • Correlate events with the same id: every event carries subject.runId, so filtering the log for one turn’s work is a string compare against the id you generated.
  • Store one id per instruction, not two.

You mint the identifiers, and PUT makes retries safe

Section titled “You mint the identifiers, and PUT makes retries safe”

Every mutating operation in this API is a PUT to an identifier you chose — sessions, turns, publications, merges. There is no Idempotency-Key header, because the resource id is the idempotency key.

You send First time Replay, same body Replay, changed body
PUT /v1/sessions/{sessionId} 201 + Location 200 + Location, same session 409 cloud_session_conflict
PUT /v1/sessions/{sessionId}/turns/{turnId} 202, created: true 202, created: false 409 cloud_turn_conflict

Use a version-4 UUID. Generate it before the call and persist it before the call — a network timeout on a request whose id you did not keep is the one failure this design cannot rescue you from. With the id in hand, an ambiguous timeout is resolved by repeating the identical request.

202 Accepted on a turn is honest rather than cautious. It means the turn and a queued run are committed to durable storage; it does not mean a sandbox exists or that any agent has read your message. Provisioning happens after the response, which is why the HTTP call never blocks on compute.

(absent) ──PUT /v1/sessions/{id}──▶ active ──DELETE /v1/sessions/{id}──▶ archived

Two states, one transition, and it is one-way. DELETE returns 204 and is a soft delete: the session, its turns, its event log and its artifact manifests remain readable, and archiving is how you signal “this conversation is over”, not how you erase it. There is no un-archive.

Creating a session allocates no compute. A brand-new session reports workspace.state: "unavailable", and that is normal — see Sandboxes.

The session resource carries the fields you will actually branch on:

Field Use it for
activeRun.id Present only while a run is non-terminal. The cheapest “is this session busy” test.
continuation Whether another turn can pick up where the last one stopped, and at what fidelity.
workspace.state unavailable, attached, or expired.
origin Your own back-reference (tenantKey, objectType, objectId, optional label and HTTPS url). Set at creation, immutable after.
sessionUrl A deep link into the WAMP Cloud web app. Hand it to a human, never parse it.
createdBy { kind: "user" | "app_installation", id }. An installation id is not a user id.

title, model and runtime are the only mutable fields, via PATCH with at least one of them — an empty body is 400 invalid_request. The task you supplied at creation, the origin and the repository source are immutable.

A turn’s own state is one field, dispatch, and it answers exactly one question: has this durably accepted instruction reached a sandbox yet?

pending ──▶ attempted ──┬──▶ confirmed
└──▶ rejected
dispatch Meaning
pending Accepted and committed. Nothing has been handed to a runtime yet.
attempted Delivery to a sandbox has been tried.
confirmed The runtime acknowledged the instruction.
rejected Delivery was refused.

The turn body itself never changes: message, and the model, runtime and replyTo recorded with it, are fixed at admission. To change an instruction you submit another turn. GET /v1/sessions/{sessionId}/turns pages them by ordinal (?after= is an exclusive ordinal, default -1, meaning “from the start”).

Do not use dispatch as progress. It describes delivery, not work; a confirmed turn whose run has already failed is an ordinary state.

Eight states, four of them terminal.

queued ──▶ dispatching ──▶ running ──┬──▶ completed
▲ ├──▶ failed
│ ├──▶ cancelled
▼ └──▶ crashed
awaiting
status Terminal What it means
queued no Committed, waiting for a worker.
dispatching no A worker is placing it on a sandbox.
running no The agent is working.
awaiting no Blocked on a human answer — an interaction is open.
completed yes The agent finished the turn.
failed yes It could not finish. lastError.code names the reason.
cancelled yes Your cancel request was honored.
crashed yes Execution was lost — for example the sandbox died.

Three guarantees are worth building on, because each is enforced in the database rather than by convention:

  • completedAt is set exactly when the status is terminal. Not “usually” — a check constraint makes the two equivalent, so completedAt != null is a sound “is it done” test even if you have never looked at status.
  • At most one non-terminal run per session. Submitting a turn while a run is in flight is 409 cloud_run_in_progress with Retry-After: 2. Use one session per concurrent task; do not multiplex unrelated work into one conversation.
  • A run may be retried without you doing anything. The attempts counter is exposed so you can notice. A run going from running back to queued with attempts incremented is the server recovering, not a new run.

POST /v1/sessions/{sessionId}/runs/{runId}/cancel takes no body and returns { run, cancellationAccepted }. It sets a flag, which the run resource reports as cancellationRequested: true. The status becomes cancelled later, if the runtime stops in time; a run that finishes first ends completed. Calling cancel twice is harmless.

lastError.code on a failed run and the errorCode on failure events are the domain error codes documented in Errors. Treat 4xx-shaped domain codes as your bug and 503-shaped ones (a sandbox still starting, a runtime account temporarily unavailable) as worth another turn.

An agent can stop and ask. When it does, the run moves to awaiting and an interaction opens:

open ──┬──▶ resolved (you answered)
└──▶ expired (workspace_lost | session_archived | run_cancelled)

You answer by submitting an ordinary turn that names the interaction:

{ "message": "core", "replyTo": { "interactionId": "ask_user:01H…" } }

That turn resolves the interaction, the same run continues, and you receive wamp.interaction.resolved carrying resolvedByTurnId. One turn can resolve at most one interaction.

Two conflicts to code for:

  • A plain turn while an interaction is open is 409 cloud_turn_unresolved. Answer first.
  • Answering an interaction that is already resolved, expired, or not the open one is 409 cloud_interaction_conflict.

After a process restart, GET /v1/sessions/{sessionId}/interactions returns the open ones, oldest first. It is the actionable edge, not history — history comes from the event log. Interaction kinds are ask_user and exit_plan_mode.

There is no push channel on this API: no Server-Sent Events, no public WebSocket. You follow a session by paging its append-only event log with an integer cursor:

GET /v1/sessions/{sessionId}/events?after=<sequence>&limit=<1..100>

sequence starts at 1, is contiguous per session with no gaps, and after is exclusive. Events are immutable and deduplicated before storage, so replaying a page is safe. The whole protocol, including how to resume after a disconnect, is Follow a run live.

Two calls, both with identifiers you generated.

Terminal window
SESSION_ID=$(uuidgen | tr 'A-Z' 'a-z')
TURN_ID=$(uuidgen | tr 'A-Z' 'a-z')
curl -sS -i -X PUT "$WAMP_API/v1/sessions/$SESSION_ID" \
-H "Authorization: Bearer $WAMP_TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"title": "T-142 · Invoice export",
"task": "Fix T-142, run the relevant tests, and summarize the result.",
"model": "<model-id-from-/v1/capabilities>",
"origin": {
"tenantKey": "acme-prod",
"objectType": "ticket",
"objectId": "T-142",
"url": "https://crm.acme.example/tickets/T-142"
}
}'
# 201 Created
# Location: /v1/sessions/<SESSION_ID>
curl -sS -i -X PUT "$WAMP_API/v1/sessions/$SESSION_ID/turns/$TURN_ID" \
-H "Authorization: Bearer $WAMP_TOKEN" \
-H 'Content-Type: application/json' \
-d '{"message":"Fix T-142, run the relevant tests, and summarize the result."}'
# 202 Accepted
# Location: /v1/sessions/<SESSION_ID>/runs/<TURN_ID>

task is required at creation and capped at 100,000 characters, as is a turn’s message. model is required unless you selected a runtime that brings its own — see Agents. Then poll /v1/sessions/$SESSION_ID/events?after=0, and read /v1/sessions/$SESSION_ID/runs/$TURN_ID for the authoritative final status.

What you may assume, and what you must handle

Section titled “What you may assume, and what you must handle”

Assume. Ids you mint are the ids the server uses. A turn is immutable. The event log is ordered, gap-free and retained for the life of the session. completedAt and terminality are equivalent. Only one run per session is non-terminal at a time. Archiving destroys nothing you have already read.

Handle. 409 cloud_run_in_progress and 409 cloud_session_finalizing, both with Retry-After: 2. 409 cloud_turn_unresolved when an interaction is open. Runs that end crashed because compute was lost, and the continuation object that tells you whether a further turn can recover. Silent server-side retries showing up as a rising attempts. New phase strings, new event types, and new stopReason values appearing without warning — ignore what you do not recognize.