# Sessions & events



A **session** runs an agent; **events** are the messages on its timeline. This
page covers driving a turn end to end.

## Create a session [#create-a-session]

<Tabs items="['Console', 'API']">
  <Tab value="Console">
    Open an agent and start a session — the Console opens a transcript view where you
    chat with the agent and watch its events render live.
  </Tab>

  <Tab value="API">
    `POST /v1/sessions`. The `agent` field is either a bare id (pins to the latest
    version) or `{ "id": "...", "version": N }` to pin a specific version.

    ```bash
    curl https://agents.clusterbase.dev/v1/sessions \
      -H "Authorization: Bearer $CLUSTER_TOKEN" \
      -H "Content-Type: application/json" \
      -d '{ "agent": "agt_3f9c2a" }'
    ```

    ```json
    {
      "id": "ses_8b1d4e",
      "agent_id": "agt_3f9c2a",
      "agent_version": 1,
      "status": { "status": "idle" },
      "usage": { "input_tokens": 0, "output_tokens": 0 },
      "created_at": "2026-06-16T12:00:00Z"
    }
    ```

    To give the session VM-backed tools (shell, file, patch), bind it to an
    [environment](/docs/agents/environments) with `environment_id`. To attach MCP
    credentials, pass `vault_ids` — see [Vaults & MCP](/docs/agents/vaults-and-mcp).
  </Tab>
</Tabs>

## Send a message [#send-a-message]

User events are submitted as a **batch** under `events`, so you can send a
message (and, say, a follow-up interrupt) atomically. A turn begins when you
append a `user.message`.

```bash
curl https://agents.clusterbase.dev/v1/sessions/ses_8b1d4e/events \
  -H "Authorization: Bearer $CLUSTER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "events": [
      { "type": "user.message", "content": [{ "type": "text", "text": "Summarize the React 19 release notes." }] }
    ]
  }'
```

## Stream the response [#stream-the-response]

`GET /v1/sessions/{id}/events/stream` is a Server-Sent Events stream of events as
they occur. A typical turn:

```
data: {"type":"session.status_running","id":"evt_01","processed_at":"..."}
data: {"type":"agent.thinking","id":"evt_02","content":[{"type":"text","text":"..."}],...}
data: {"type":"agent.tool_use","id":"evt_03","tool":"web_search","input":{"query":"react 19"},...}
data: {"type":"agent.tool_result","id":"evt_04","tool_use_id":"evt_03","content":[...],"is_error":false,...}
data: {"type":"agent.message","id":"evt_05","content":[{"type":"text","text":"React 19 adds..."}],...}
data: {"type":"session.status_idle","id":"evt_06","stop_reason":{"type":"end_turn"},...}
```

See [Concepts → Events](/docs/agents/concepts#events) for the full list of event
types.

Long-running sessions are compacted automatically: once a turn's model context
grows past an internal threshold, the runtime summarizes older exchanges into a
durable checkpoint (`session.context_compacted`) and continues the turn from
there. The visible transcript returned by the events endpoints is never
shortened — compaction only affects what's sent to the model.

## Read history [#read-history]

To fetch the timeline so far (rather than stream it live):

```bash
curl https://agents.clusterbase.dev/v1/sessions/ses_8b1d4e/events \
  -H "Authorization: Bearer $CLUSTER_TOKEN"
# → { "data": [ { "type": "user.message", ... }, { "type": "agent.message", ... } ] }
```

## Interrupt a turn [#interrupt-a-turn]

Send a `user.interrupt` to stop the agent mid-turn. The session goes `idle` with
`stop_reason: interrupted`; your next `user.message` resumes from there.

```bash
curl https://agents.clusterbase.dev/v1/sessions/ses_8b1d4e/events \
  -H "Authorization: Bearer $CLUSTER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "events": [{ "type": "user.interrupt" }] }'
```

## Usage and lifecycle [#usage-and-lifecycle]

`GET /v1/sessions/{id}` returns the session's current `status`, cumulative
`usage` (input/output and cache tokens), and `shared` (whether the session
currently has a public transcript view — see [Share a session](#share-a-session)
below). When you're done, archive the session to soft-delete it (it must not be
running):

```bash
curl -X POST https://agents.clusterbase.dev/v1/sessions/ses_8b1d4e/archive \
  -H "Authorization: Bearer $CLUSTER_TOKEN"
```

## Share a session [#share-a-session]

`PUT /v1/sessions/{id}/sharing` toggles a public, read-only, revocable view of
the session's transcript:

```bash
curl -X PUT https://agents.clusterbase.dev/v1/sessions/ses_8b1d4e/sharing \
  -H "Authorization: Bearer $CLUSTER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "shared": true }'
# → { "shared": true }
```

While shared, anyone with the id can fetch the transcript with no
authentication:

```bash
curl https://agents.clusterbase.dev/v1/shared-sessions/ses_8b1d4e
# → { "id": "ses_8b1d4e", "title": "...", "created_at": "...", "messages": [...] }
```

`messages` is a projection of only stable user and assistant message content —
tenant metadata, provider internals, and private storage identifiers are never
included. It carries text, reasoning, file, and completed image-generator
parts; every other part, and session errors, are dropped rather than
redacted. Any file attachment is exposed as a public Storage URL rather than a
durable `fileId` or signed URL. Responses are sent `Cache-Control: private,
no-store`.

Completed web, news, and shopping search results, as well as explicit citation
sources, are also retained (in their original order, so inline citation
markers still resolve correctly): titles, snippets, and result URLs are kept,
but URLs have any credentials and private query parameters stripped, and a
failed fetch is reduced to a boolean `error` flag rather than its original
error detail. No search input or private tool payload is published.

Sharing is off by default and revoking it (`{ "shared": false }`) takes effect
immediately: the shared endpoint and every attachment URL it returned start
responding `404`. Turning sharing *on* fails with `400` if the history contains
an attachment referenced only by a raw URL — those can't be re-authorized once
public — so re-upload such a file as a stored attachment before sharing.

## Read receipts [#read-receipts]

Every session carries an `activity` summary (returned on `GET /v1/sessions/{id}`
and in each entry of `GET /v1/sessions`) so clients can show unread state
without diffing the timeline themselves:

```json
{ "activity": { "state": "needs_help", "unseen": true, "attention_event_id": "evt_09" } }
```

`state` is `idle`, `working`, `done`, `needs_help`, `failed`, or `interrupted`.
`unseen` is `true` when the owner hasn't acknowledged the response or approval
request currently at `attention_event_id`. Sending a `user.message` or
resolving an approval does not by itself clear `unseen` — the owner has to have
actually seen it.

Acknowledge what's currently displayed with `PUT /v1/sessions/{id}/seen`:

```bash
curl -X PUT https://agents.clusterbase.dev/v1/sessions/ses_8b1d4e/seen \
  -H "Authorization: Bearer $CLUSTER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "through_event_id": "evt_09" }'
# → { "state": "needs_help", "unseen": false, "attention_event_id": "evt_09" }
```

The cursor only advances — acknowledging an older `through_event_id` than
what's already been seen is a no-op — and it's account-wide, not per-device.
Because it's keyed to the specific event actually rendered, a receipt sent for
a stale response can't accidentally clear a newer one, and answering a pending
approval doesn't itself mark it seen.
