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.
Three objects, one sentence each
Section titled “Three objects, one sentence each”| 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.
The turn id is the run id
Section titled “The turn id is the run id”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 runA 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.
Session lifecycle
Section titled “Session lifecycle”(absent) ──PUT /v1/sessions/{id}──▶ active ──DELETE /v1/sessions/{id}──▶ archivedTwo 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.
Turn lifecycle
Section titled “Turn lifecycle”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 └──▶ rejecteddispatch |
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.
Run lifecycle
Section titled “Run lifecycle”Eight states, four of them terminal.
queued ──▶ dispatching ──▶ running ──┬──▶ completed ▲ ├──▶ failed │ ├──▶ cancelled ▼ └──▶ crashed awaitingstatus |
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:
completedAtis set exactly when the status is terminal. Not “usually” — a check constraint makes the two equivalent, socompletedAt != nullis a sound “is it done” test even if you have never looked atstatus.- At most one non-terminal run per session. Submitting a turn while a run is
in flight is
409 cloud_run_in_progresswithRetry-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
attemptscounter is exposed so you can notice. A run going fromrunningback toqueuedwithattemptsincremented is the server recovering, not a new run.
Cancellation is a request
Section titled “Cancellation is a request”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.
Failure is not always terminal
Section titled “Failure is not always terminal”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.
Interactions: when the agent needs you
Section titled “Interactions: when the agent needs you”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.
Following the work
Section titled “Following the work”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.
A minimal end-to-end exchange
Section titled “A minimal end-to-end exchange”Two calls, both with identifiers you generated.
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.
Related
Section titled “Related”- Sandboxes and environments — why a session outlives
its compute, and what
continuationpromises. - Agents — choosing a
runtimeand amodel. - Follow a run live — the polling loop in full.
- Sessions operations and Turns and runs operations — the generated endpoint reference.
- Interactions operations, Events operations, and the API reference — field-by-field detail.