Develop
Sessions and turns
The session API is the lower-level interface for durable agent conversations. Use the compound agent invocation endpoint for most product chat surfaces. Use session and turn endpoints when your client must create sessions itself, page the transcript, steer work in progress, or manage compaction directly.
Resource model
A session owns an ordered transcript. A turn is one execution attempt against that transcript. Messages and compaction summaries are durable; typing previews, model deltas, and live tool activity are not.
The core endpoints are:
POST /v1/sessions
GET /v1/sessions/{session_id}
GET /v1/sessions/{session_id}/messages
POST /v1/sessions/{session_id}/turns
GET /v1/sessions/{session_id}/streamEvery session is scoped to one organization and one agent. A stable name lets
your application resolve the same conversation again without storing a Mobius
session ID as its only lookup key.
Start a turn
Send content to POST .../turns:
{
"input": {
"content": [
{
"type": "text",
"text": "Summarize the open support tickets."
}
],
"idempotency_key": "<message-id>"
}
}By default, the endpoint returns 202 Accepted while the turn continues:
{
"session_id": "<session-id>",
"turn_id": "<turn-id>",
"status": "queued",
"after_sequence": 18
}Open the session stream with after_sequence=18 to follow only activity after
that durable cursor. If the start request sends Accept: text/event-stream,
Mobius returns 200 OK and streams the same turn inline.
Use a provider message ID or another durable application event ID as
idempotency_key. Retrying the same key and content resolves the original
turn instead of starting duplicate work.
Stream and replay
Committed transcript rows arrive as user.message, agent.message, and
compaction.created frames. Their server-sent event id equals the durable
message sequence. Persist that value as the reconnect cursor.
Live frames such as turn.started, generation.delta,
session.message.preview, tool.call, and tool.result help render progress,
but they are not transcript history. Do not persist live_sequence or
delta_sequence as a reconnect cursor.
If a client attaches in the middle of a turn, request the turn's live snapshot:
GET /v1/sessions/{session_id}/turns/{turn_id}/liveThe snapshot rebuilds the current preview. The final agent.message still
arrives as a durable transcript row even when a client misses live frames.
Tool history also lives in the transcript. A tool_use block appears on the
agent message and the matching tool_result block appears on the following
user message. Pair them by tool_use_id; treat live tool frames as optional
progress only.
Read what a turn cost
A completed turn carries its token usage on the turn resource, so you can ask what a turn cost long after it ran:
GET /v1/sessions/{session_id}/turns/{turn_id}{
"id": "<turn-id>",
"status": "completed",
"usage": {
"input_tokens": 1840,
"output_tokens": 412,
"cache_read_input_tokens": 20480,
"calls": 3
}
}The same usage object appears on GET .../turns, on the transcript's
turn.upsert frames, and on the live turn.completed event, so a client that
missed the live frame never has to replay the stream to recover it.
Counters are summed over every model call the turn made. Cache reads and writes
are reported separately and are never folded into input_tokens, because
providers price them differently. reasoning_tokens covers hidden reasoning
that is billed as output but never appears in the reply, which is often where
an unexpectedly expensive turn went. A counter that stayed at zero, or that the
provider did not report, is omitted rather than sent as 0.
A turn that failed carries no usage, even if it spent tokens before failing. Read your billing usage events when you need charged amounts rather than a usage report.
Nudge work in progress
A nudge adds direction at the next safe iteration boundary without cancelling the active turn:
POST /v1/sessions/{session_id}/nudges{
"content": "Do not deploy. Prepare the diff and wait.",
"idempotency_key": "<nudge-id>",
"wake": false
}A delivery of current_turn means an existing turn will receive the nudge
as runtime input at its next safe boundary, including one final boundary as
the turn finishes, so direction that arrives during the closing response is
still answered in the same turn. A delivery of new_turn means no turn
could absorb it (the session was idle, or the target turn settled first), so
the content became a regular user message on a fresh turn, exactly as if it
had been sent to the session, and the nudge is delivered immediately.
Set wake: true only when a turn is waiting on an agent tool and should resume
immediately. One oldest nudge is delivered per safe boundary. A session accepts
up to 32 pending nudges.
Use these endpoints to reconcile or cancel pending direction:
GET /v1/sessions/{session_id}/nudges?status=pending
POST /v1/sessions/{session_id}/nudges/{nudge_id}/cancelReusing an idempotency key with different content returns
409 idempotency_conflict. A full queue returns 409 nudge_queue_full.
Compaction
A session compaction policy has four controls. Choose either threshold or
threshold_tokens, not both:
| Field | Values | Meaning |
|---|---|---|
strategy | auto, manual, disabled | Decides whether compaction starts automatically, only on request, or never. |
threshold | xs, sm, md, lg, xl | Sets a model-relative trigger at 10%, 20%, 40%, 60%, or 80% of the session model's context window. |
threshold_tokens | Integer from 1 to 10,000,000 | Sets an exact estimated-token trigger instead of a preset. |
summary_model | Model ID | Chooses the model that writes the summary. |
Update the session to change its policy. Existing sessions do not re-resolve defaults from the agent, loop, or messaging binding.
{
"compaction_policy": {
"strategy": "auto",
"threshold": "md"
}
}For an exact threshold, replace threshold with threshold_tokens:
{
"compaction_policy": {
"strategy": "auto",
"threshold_tokens": 375000
}
}Start compaction under auto or manual with:
POST /v1/sessions/{session_id}/compactdisabled rejects manual compaction. Change the strategy first. A manual
compaction gives up after two minutes; the automatic pass gives up after 30
seconds. Either way, the transcript is unchanged by a pass that fails, and the
next pass recomputes from the same boundary.
Show compaction progress
Summarizing a long transcript takes seconds, and it can start on its own right after a turn completes. Show it, or your users will read the pause as a hang.
Three signals cover every case. compaction.started opens the indicator,
compaction.created or compaction.failed closes it, and the session read
answers the same question for a client that was not connected when the pass
began:
GET /v1/sessions/{session_id}{
"compaction": {
"in_progress": true,
"started_at": "2026-08-02T15:04:05Z",
"deadline": "2026-08-02T15:04:35Z",
"from_sequence": 412,
"through_sequence": 987,
"message_count": 63,
"threshold_tokens": 150000
}
}The recipe:
- Start the indicator on
compaction.started, or onin_progress: truefrom a session read. - End it on
compaction.created, oncompaction.failed, or onin_progress: falseat the next read. - Treat a cleared marker with no
compaction.createdin the transcript as a pass that ended without a summary.
compaction.started and compaction.failed are live-only frames on
GET /sessions/{session_id}/stream. They are not replayed on reconnect, and
like every live frame they are best effort under load, so the session read is
the fallback rather than an edge case. compaction.created is durable: it
replays from the transcript with the summary's own message sequence.
The v2 transcript stream carries no compaction pulses. It streams authoritative
row state, and the turn genuinely is completed while compaction runs. Read
the session's compaction field there, or open the session stream alongside it.
threshold_tokens is the estimated-token size at which this session compacts
automatically, resolved from its policy and the session model. Pair it with the
transcript you already render and you can show a context-capacity gauge, which
tells a user that summarization is coming before it interrupts them. It is
absent for manual and disabled sessions, which never compact on their own.
One ordering note. The session stream stays open while a compaction it
triggered is running, so compaction.created arrives before the terminal turn
frame on a connection that stays put. The terminal turn.* frame, not the
stream closing, is the completion signal.
Cancel or force-unlock a session
Cancel a specific turn when you know its ID:
POST /v1/sessions/{session_id}/turns/{turn_id}/cancelCancellation is idempotent, cooperative, and terminal. Repeating it returns the current terminal turn without another lifecycle transition. Mobius retires pending jobs, waits, interactions, and nudges owned by the cancelled turn, but does not roll back tool or external effects that already occurred. Committed transcript rows remain; live-only preview text is discarded.
A cancelled turn cannot resume. Reusing its original invocation idempotency key
returns the same cancelled turn. Use a new key to attempt the task again, and
make external actions idempotent because the new attempt may repeat an effect.
Stream cursors resume observation, not execution. Direct cancellation of a live
loop-owned turn returns 409 turn_owned_by_run; cancel the run instead.
Use the session cancel endpoint with force=true only when a non-terminal turn
blocks new messages and ordinary cancellation cannot clear it:
POST /v1/sessions/{session_id}/cancel?force=trueForce cancellation can interrupt a loop-owned turn and leave its run needing operator attention. It does not delete transcript rows.
Next
- Use the higher-level agent invocation flow for most embedded assistants.
- Render live events with the event catalog.
- Understand transcript history and compaction in agent sessions.
- See every schema in the interactive API reference.