# Sessions, turns, runs The three durable objects every WAMP Cloud integration is built from, the identifiers you mint for them, and the exact states they move through. Source: https://docs.cloud.vampikez.fun/concepts/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 | 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 This is the one non-obvious fact in the API, and everything downstream is easier once you hold it: ```text 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 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 ```text (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](/concepts/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](/concepts/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](https://docs.vampikez.fun/identity/connected-resources/) 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 `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`. | :::caution[`phase` still lags the run] `phase` is presentation state, written after the fact. The **run** carries the authoritative `status`, and `phase` can trail it between a run's terminal transition and reconciliation — so a session can read `running` for a moment after its run is already `completed`. Drive your own state machine from `run.status` and the event log; use `phase` to label a session in a list. ::: ## 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? ```text 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](/concepts/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 Eight states, four of them terminal. ```text 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. ### 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 `lastError.code` on a failed run and the `errorCode` on failure events are the domain error codes documented in [Errors](/reference/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 An agent can stop and ask a question. The run moves to `awaiting` and an interaction opens: ```text open ──┬──▶ resolved (you answered) └──▶ expired (workspace_lost | session_archived | run_cancelled) ``` You answer by submitting an ordinary turn that names the interaction: ```json { "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](/guides/follow-a-run/#handle-a-question-mid-run). - 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 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: ```text GET /v1/sessions/{sessionId}/events?after=&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](/guides/follow-a-run/). ## A minimal end-to-end exchange One atomic call, with both identifiers generated by you. ```bash 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": "", "origin": { "tenantKey": "acme-prod", "objectType": "ticket", "objectId": "T-142", "url": "https://crm.acme.example/tickets/T-142" } }' # 201 Created # Location: /v1/sessions/ # 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](/concepts/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 **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 - [Sandboxes and environments](/concepts/sandboxes/) — why a session outlives its compute, and what `continuation` promises. - [Agents](/concepts/agents/) — choosing a `runtime` and a `model`. - [Follow a run live](/guides/follow-a-run/) — the polling loop in full. - [Sessions operations](/api/operations/tags/sessions/) and [Turns and runs operations](/api/operations/tags/turns-and-runs/) — the generated endpoint reference. - [Interactions operations](/api/operations/tags/interactions/), [Events operations](/api/operations/tags/events/), and [the API reference](/api/) — field-by-field detail.