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 immutable instruction, submitted by a caller or raised by Cloud, numbered by ordinal starting at 0. |
| 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.
Cloud can also raise a Turn when a child Session finishes or when an opted-in
Session’s published pull request receives feedback. Such a Turn carries
raisedBy: { kind: "child_session", sessionId } or
{ kind: "pull_request", pullRequestNumber, triggers }. The latter counts
review, reviewComment, comment and checkFailure deliveries consumed by
that Turn. It is a system notice, not a person’s chat message.
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 command that creates work 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. Editing a session’s title
(PATCH), archiving it (DELETE) and cancelling a run (POST) create nothing
and need none of this.
| 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 again on
an exact retry and drops the Session out of the default listing.
There is no un-archive.
Archiving stops commands, not reads. This is the part that decides how you write your cleanup path, so it is worth stating exactly:
| After archiving | Still works |
|---|---|
GET the session, its turns, events, artifacts (manifest and content), interactions, publications and merges |
yes |
| Submitting a turn, publishing, merging, cancelling, or any other command | no |
So archiving is safe to do as soon as a conversation is finished — it does not
destroy your ability to reconcile later. What it does destroy is your ability to
act. continuation on an archived session always reports
canContinue: false with reason: "session_archived", whatever fidelity the
checkpoint retained.
One trap follows from the split, and it is worth handling explicitly: a command
that has to resolve the session before it can refuse reports the refusal as a
missing session. Admitting a merge on an archived session answers
404 cloud_session_not_found, so the same session id is 200 on GET and
404 on that command. Read archivedAt on the session rather than inferring
the state from a 404.
GET /v1/sessions takes archived=exclude|include|only, defaulting to
exclude. An archived session has not disappeared; it is behind a parameter.
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 |
The newest non-terminal run, present from admission while it is still queued and before it reaches a sandbox or writes an event. An awaiting origin and a queued interaction reply can coexist; this field names the reply that Stop addresses. It is the cheapest “is this session busy” test: a turn submitted beside it is 409 cloud_run_in_progress unless it answers an open interaction or the run has already ended and the session is finalizing, when the turn is queued behind it. It is also the run id to read or cancel when you did not mint the turn yourself. |
continuation |
Whether another turn can pick up where the last one stopped, and at what fidelity. Two questions, two fields: canContinue is admission only — it is true for a fresh session whose fidelities are both none — while reason == null is the one check that means the previous work carries. See Sandboxes. |
workspace.state |
unavailable, attached, or expired. |
origin |
Your own back-reference (tenantKey, objectType, objectId, optional label and HTTPS url). Set at creation, immutable after. A worker session an agent started carries its parent’s. |
parent |
{ sessionId, runId } for a worker session an agent started from that session’s run; null for a session you or a person created. |
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. |
archivedAt |
Present only once the session is archived. The reliable “commands will be refused” test. |
The execution identity is pinned to the Session owner. If a WAMP user later submits a Turn through a shared Session, connected-resource calls still act as that owner; sharing does not substitute the collaborator’s identity.
title, environment, model and runtime are the only accepted PATCH
fields. environment accepts only null: it detaches the session’s
environment, and the next fresh sandbox starts without it. title
remains mutable; model/runtime may change only while the Session is idle and
become immutable when its first Turn is accepted. The create command, origin
and repository source are immutable.
phase is a closed set of seven values
Section titled “phase is a closed set of seven values”phase is the last execution phase the control plane recorded for the session.
It is a declared enum — these seven values and no others, with additions
arriving as a contract change — so you may switch on it exhaustively. The
@wamp/app-sdk union is CLOUD_SESSION_PHASES.
phase |
What the control plane recorded |
|---|---|
idle |
No run is in flight. A session lands here when a workspace is attached or released with no interaction open. |
provisioning |
A session was created or a turn was accepted, and compute is being arranged. A freshly created session reports this, not idle. |
running |
A turn reached a sandbox and the agent is working. |
awaiting |
The agent is blocked on an open interaction. Answer it, or submit a new turn. |
finalizing |
The run has ended and the control plane is sealing its checkpoint and artifacts. One of the three below follows. |
completed |
The last run finished successfully. |
failed |
The last run did not — a dispatch that could not be completed, or a run that ended failed, cancelled or crashed. |
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. |
An accepted turn is immutable, including its resolved agent configuration. A
new turn accepts message, optional replyTo, and optional runtime, model,
thinkingLevel or runtimeConfig. Omitted runtime/model inherit the session;
an explicit selection changes the agent for that new turn. Interaction replies
stay bound to the configuration that opened the interaction. See
Agents. To change an instruction, submit another turn. GET /v1/sessions/{sessionId}/turns pages them by
ordinal (?after= is an exclusive ordinal, default -1, meaning “from the
start”). The default is -1 rather than 0 precisely because the first turn is
ordinal 0: paging with ?after=0 skips it.
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. Where the reason is depends on how far it got — see below. |
cancelled |
yes | Your cancel request was honored. |
crashed |
yes | Execution was lost — for example the sandbox died. |
lastError is present only on a run that failed before it reached a
sandbox: placement gave up, the organization has no compute, the workspace was
lost. A run that started and then failed carries no lastError at all — its
terminal event has stopReason: "error", and the result.summary artifact
holds what happened. Record whichever of the two you get.
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 executing run per session. Submitting a turn while a run is
in flight is
409 cloud_run_in_progresswithRetry-After: 2. Once that run has ended and the session isfinalizing, one follow-up is accepted and waitsqueueduntil the checkpoint is sealed. 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 when the
runtime stops, or when the platform force-stops it after six cancel RPC attempts
and their backoff. A run that finishes first ends completed. Calling cancel
twice is harmless. Forced Stop cannot capture a terminal checkpoint. A later
Turn can resume only if the Session has an earlier restorable checkpoint;
otherwise start a new Session.
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 a question. 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…" } }For a structured ACP form, request.questions[*].fieldId is present. In that
case message is a JSON object string keyed by those field ids and carrying the
published typed optionValues; a custom answer uses customFieldId, and an
optional unanswered field is omitted. Plain text answers it too: the bare answer
to a single question, or one Label: answer line per question; text naming an
option selects it, other text is the custom answer. A native question without
field ids keeps the human-readable answer string shown above.
That Turn has its own id and Run. Admission resolves the Interaction and
completes the original awaiting Run; execution continues in the same engine
conversation/workspace as the new reply Run. Follow the reply Run id, and
consume wamp.interaction.resolved carrying resolvedByTurnId.
Two conflicts to code for:
- A plain turn while an interaction is open is
409 cloud_conversation_busy. Answer first, and never retry the plain turn — an open interaction has no timeout, so the loop does not terminate. See Follow a run live. - 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. ask_user and exit_plan_mode take reply Turns. An
approval instead retains its complete action in request.approval while its
Run stays running; PUT …/interactions/{interactionId}/decision answers it
with wamp.cloud.approvals:respond and resumes that same Run, without a reply Turn.
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”One atomic call, with both identifiers generated by you.
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", "initialTurn": { "id": "'$TURN_ID'", "message": "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>
# The response already contains the atomic initial Turn and queued Run.initialTurn.message is capped at 100,000 characters. 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
executes at a time; at most one more waits queued behind a run that is
finalizing or awaiting an answer. An archived session stays readable in full.
Handle. 409 cloud_run_in_progress with Retry-After: 2 while a Run is
still executing. Once that Run has ended — the session is finalizing, even if
the run itself has not yet reported its terminal status — a follow-up is
accepted; the new Run remains queued until the boundary is durable. 409 cloud_conversation_busy means an interaction is open — answer
it rather than retrying.
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 event types and new stopReason values
appearing without warning — ignore what you do not recognize. phase is not in
that group: it is a closed set, and a new value would arrive as a contract
change.
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.