Lifecycle

Read a session's conversation and turn, and know when the message you sent is answered.

A session is a conversation with an agent. The session object reports it in two parts: conversation says whether the session can take more messages, and turn says what is happening to the latest message. A turn is one message and the agent's answer to it. The message you sent is answered when its turn reaches a final status.

Conversation

FieldMeaning
conversation.stateopen while the session can take messages; closed is permanent
conversation.interactiveWhether the session stays open after its first turn; set it when starting a session, on an agent, or when starting an agent session
conversation.warmWhether an agent is ready to answer the next message right away; when false, the next message first prepares an environment, which takes longer
conversation.promptingWhether a message you send now would be accepted: enabled, and when it is false, blocked_reason and detail say why

A session started with interactive: false closes when its turn ends; after that, start a new session. warm is a hint for what to show while a message is pending, never something to branch on. prompting.detail is a sentence you can show as is; blocked_reason is one of:

ReasonMeaning
closedThe conversation is closed
mention_surfaceThe session was started by a mention; reply on the surface surface_name names (GitHub, Slack, or Linear)
non_interactiveThe session was started with interactive: false
harness_single_turnThe harness takes one message per session
budget_exhaustedThe session spent its budget

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

Turn

StatusMeaning
pendingThe message was received; the agent hasn't started on it, because another turn is running or the environment is being prepared
runningThe agent is working on the message
completedThe agent finished
failedThe turn ended with an error; reason and detail explain
stoppedSomeone stopped the turn; stopped.at and stopped.by say when and who
cancelledThe turn was refused before the agent started; reason and detail explain

completed, failed, stopped, and cancelled are final. A final turn ends that turn, not the conversation: an interactive session still takes follow-ups while prompting.enabled is true. The other turn fields are id, index (its position in the session, from 0), created_at (when the message was received), started_at, ended_at, cost, and tokens.

reason is set only on failed and cancelled turns:

ReasonMeaning
errorThe agent ended with an error
tool_call_failedThe agent process failed or stalled before finishing
budget_hitThe session budget or a spend limit was reached
payment_requiredThe account has no credit or a subscription problem
blockedThe account or user is blocked
contact_email_requiredThe account needs a contact email
lifecycle_hook_failedAn environment hook exited with a nonzero status
missing_repo_accessThe GitHub installation can't access a repository the session needs
missing_token_permissionsThe GitHub installation lacks a permission the session needs
missing_environment_variablesAn environment variable has no value
missing_github_integrationThe account has no GitHub installation
agent_unavailableThe saved agent was deleted or disabled
upstream_unavailableThe Claude or Codex subscription the session uses is unavailable
unsupported_configurationThe harness, model, and settings can't be combined
execution_disabledSessions from this source are disabled
interruptedThe turn lost its agent before answering; the message is queued again on a new turn

detail is a sentence you can show as is.

Wait for the answer

The message you sent is answered when the turn with its turn_id reaches a final status. POST /v1/sessions returns the session with the turn for its prompt in turn; POST /v1/sessions/{session_id}/messages returns the message and its turn. Poll GET /v1/sessions/{session_id}/turns/{turn_id} or watch the stream's session frames until status is final. Waiting for the conversation to close, or for it to go quiet, is wrong: an open conversation stays open between turns.

The SDK handle's wait() does this for you. It returns when the turn it waits on ends: the session's opening turn, then each turn a send() creates.

1
session = handle.wait(timeout=900)
2
turn = session.turn
3
print(turn.status, turn.reason, turn.detail)
4
print(session.conversation.state, session.conversation.prompting.enabled)

session.turn is the turn in progress, or the latest one when none is running, and null until the session has a message. GET /v1/sessions/{session_id}/turns lists every turn and message, oldest first.

Follow the stream

1
from ellipsis.frames import StreamFrame
2
3
4
def on_frame(frame: StreamFrame):
5
if frame.type in ("snapshot", "session"):
6
turn = frame.session.turn
7
if turn is not None:
8
print(turn.index, turn.status)
9
print(frame.session.conversation.state)
10
elif frame.type == "records_append":
11
for record in frame.records:
12
print(record.feed_seq, record.kind)
13
elif frame.type == "error":
14
print(frame.message)
15
elif frame.type == "done":
16
print("Conversation closed")

Pass this callback to the stream in the Python or TypeScript SDK. snapshot sends the current session, session replaces it whenever the turn, the conversation, or the cost changes, records_append adds saved records in feed_seq order, and done means the conversation closed. delta frames carry live partial output that isn't saved. A turn_ended record carries the same status, reason, and detail as the turn. Frames can repeat, so ignore duplicates and types you don't recognize. See Session events for every event.

A session keeps the Claude or Codex subscription it started with; connecting a different one affects only new sessions.

On this page

Schedule a demo