A session is a conversation with an agent. The session object reports where it is in `status`, why its last message ended badly in `error`, and whether it can take more messages in `conversation`. Every message you send is answered in order. The message you sent is answered once the session is `idle` or `closed`.

## Status

<span id="statuses" />
<span id="turn" />

| Status | Meaning |
| --- | --- |
| `queued` | A message was received and waits to start, or waits until a session ends when your account has its [limit](/docs/organization#platform-allowances) of sessions alive |
| `preparing` | The environment for the next message is being set up |
| `running` | The agent is working on a message |
| `idle` | Every message has been answered and the session can take another |
| `closed` | The session can take no more messages. `closed` is permanent |

A session started with `interactive: false` closes when its first message is answered. After that, start a new session.

## Error

`error` is null while the session is busy and after a message was answered. When the last message failed, was stopped, or was cancelled, `error.reason` says why and `error.detail` is a sentence you can show as is. The next message clears it.

| Reason | Meaning |
| --- | --- |
| `error` | The agent ended with an error |
| `tool_call_failed` | The agent process failed or stalled before finishing |
| `budget_hit` | The session [budget](/docs/budgets) or a spend limit was reached |
| `payment_required` | The account has no credit or a billing problem |
| `blocked` | The account or user is blocked |
| `contact_email_required` | The account needs a contact email |
| `lifecycle_hook_failed` | An environment [hook](/docs/environments#setup-and-hooks) exited with a nonzero status |
| `missing_repo_access` | The GitHub installation can't access a repository the session needs |
| `missing_token_permissions` | The GitHub installation lacks a permission the session needs |
| `missing_environment_variables` | An environment variable has no value |
| `missing_github_integration` | The account has no GitHub installation |
| `agent_unavailable` | The saved agent was deleted or disabled |
| `upstream_unavailable` | The model provider was rate limited or down after retries |
| `unsupported_configuration` | The harness, model, and settings can't be combined |
| `execution_disabled` | Sessions from this source are disabled |
| `interrupted` | The agent was lost before answering. The message starts again on a new environment |
| `stopped` | Someone stopped the session |

## Conversation

| Field | Meaning |
| --- | --- |
| `conversation.interactive` | Whether the session stays open after its first message is answered. Set it when [starting a session](/docs/sessions#run-a-one-off-task), on an agent, or when [starting an agent session](/docs/agents#override-settings-for-a-session) |
| `conversation.warm` | Whether an agent is ready to answer the next message right away. When `false`, the next message first prepares an environment, which takes longer |
| `conversation.prompting` | Whether a message you send now would be accepted. When `enabled` is `false`, `blocked_reason` and `detail` say why |

`warm` is a hint for what to show while a session is `queued` or `preparing`, never something to branch on. `prompting.detail` is a sentence you can show as is. `blocked_reason` takes these values.

| Reason | Meaning |
| --- | --- |
| `closed` | The session is closed |
| `mention_surface` | The session was started by a mention. Reply on the surface `surface_name` names (GitHub, Slack, or Linear) |
| `non_interactive` | The session was started with `interactive: false` |
| `harness_single_turn` | The harness takes one message per session |
| `budget_exhausted` | The session spent its [budget](/docs/budgets) |

`POST /v1/sessions/{id}/messages` fails with `409` while `prompting.enabled` is `false`.

## Wait for the answer

<span id="check-the-status" />

Poll [`GET /v1/sessions/{id}`](/docs/api/sessions/get-sessions-id) or watch the stream's `session` frames until `status` is `idle` or `closed`. Waiting for the session to go quiet is wrong because a busy session can be quiet while its environment is prepared.

How a message ended is in the session's log. Its [`message_ended`](/docs/api/session-events#message_ended) record carries `status` (`completed`, `failed`, `stopped`, or `cancelled`), `error` (the same `reason` and `detail` object the session carries, null when answered), and what answering it cost. Messages answered together report the cost once, on the last of them. [`GET /v1/sessions/{id}/records?message_id=...`](/docs/api/sessions/get-sessions-id-records) returns one message's records.

The SDK handle's `wait()` does both. It returns the `message_ended` payload of the message it waits on. That is the last one sent through `send()`, else the session's latest message.

```python
ended = handle.wait(timeout=900)
print(ended.status, ended.error)
print(handle.session.status, handle.session.conversation.prompting.enabled)
```

```typescript
const ended = await handle.wait({ timeoutMs: 900_000 });
console.log(ended?.status, ended?.error);
console.log(handle.session.status, handle.session.conversation.prompting.enabled);
```

## Follow the stream

```python
from ellipsis.frames import StreamFrame

def on_frame(frame: StreamFrame):
    if frame.type == "session":
        print(frame.session.status, frame.session.error)
    elif frame.type == "records":
        for record in frame.records:
            print(record.feed_seq, record.kind, record.message_id)
    elif frame.type == "error":
        print(frame.message)
```

```typescript
import type { StreamFrame } from '@ellipsis-dev/sdk/stream';

function onFrame(frame: StreamFrame) {
  if (frame.type === 'session') {
    console.log(frame.session.status, frame.session.error);
  } else if (frame.type === 'records') {
    for (const record of frame.records) {
      console.log(record.feed_seq, record.kind, record.message_id);
    }
  } else if (frame.type === 'error') {
    console.error(frame.message);
  }
}
```

Pass this callback to the stream in the [Python](/docs/api/python-sdk#stream-a-session) or [TypeScript](/docs/api/typescript-sdk#stream-a-session) SDK. The first frame is `session`, resent whenever the status, the error, or the cost changes. `records` adds saved records in `feed_seq` order. A `session` frame with status `closed` is the last one. The stream then closes normally. `delta` frames carry live partial output that isn't saved. Frames can repeat, so ignore duplicates and types you don't recognize. See [Session events](/docs/api/session-events) for every event.
