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

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

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

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

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.

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.

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

  • 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 executing run per session. Submitting a turn while a run is in flight is 409 cloud_run_in_progress with Retry-After: 2. Once that run has ended and the session is finalizing, one follow-up is accepted and waits queued until 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 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 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.

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

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.

One atomic call, with both identifiers generated by you.

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",
"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.