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}/stream

Every 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}/live

The 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}/cancel

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

FieldValuesMeaning
strategyauto, manual, disabledDecides whether compaction starts automatically, only on request, or never.
thresholdxs, sm, md, lg, xlSets a model-relative trigger at 10%, 20%, 40%, 60%, or 80% of the session model's context window.
threshold_tokensInteger from 1 to 10,000,000Sets an exact estimated-token trigger instead of a preset.
summary_modelModel IDChooses 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}/compact

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

  1. Start the indicator on compaction.started, or on in_progress: true from a session read.
  2. End it on compaction.created, on compaction.failed, or on in_progress: false at the next read.
  3. Treat a cleared marker with no compaction.created in 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}/cancel

Cancellation 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=true

Force cancellation can interrupt a loop-owned turn and leave its run needing operator attention. It does not delete transcript rows.

Next