Skip to main content

Sessions

Durable, resumable dialogues with an agent — messages, generations, forks and expiry.

Overview​

A session binds one agent to one thread of conversation. Creating it also creates the conversation underneath, so a single call is enough to start talking; the transcript is then read through that conversation, because naturali stores no message bodies of its own.

On top of a bare conversation, a session adds an agent binding, a lifecycle (open / closed / expired), optional auto-generation, inactivity expiry, message debouncing, and forking.

This module is a verbatim mirror of the runtime: every field, method, status code and error shape is the runtime's own, re-rooted under the project in the path.

See the OpenAPI spec for the full endpoint and schema reference, or browse it rendered under API Reference → Sessions.

Data Model​

Session​

FieldTypeDescription
idstringPublic session ID.
agent_idstringThe agent that answers in this session. Required on create.
conversation_idstringThe conversation holding the transcript.
statusstringopen, closed or expired.
namestring, nullableOptional label.
actor_idstring, nullableThe actor speaking as the end user, or null — see Cap spend per end user.
auto_generatebooleanWhen true, posting a user message triggers generation automatically.
generating_atstring (date-time), nullableWhen the in-flight generation started, else null.
tool_contextobject, nullableKey/value pairs forwarded as X-Naturali-Context-<key> headers on every http and mcp tool call in this session.
inactivity_ttl_secondsinteger, nullableSeconds of inactivity after which the session expires. 0 never expires.
message_delay_secondsinteger, nullableDebounce: wait this long after the last user message before calling the model; each new message resets the timer.
last_activity_atstring (date-time)
forked_from_session_idstring, nullableThe session this one branched from, if any.
forked_from_positioninteger, nullableThe parent position it branched after.
tagsobject, nullableTag map.
usageobjectToken counts and cost_usd summed across the session's generations. Single-session read only — the listing and the fork listing omit it.
created_atstring (date-time)
updated_atstring (date-time)

Key Concepts​

Posting a message, with or without auto-generation​

POST /v1/projects/{project_id}/sessions/{session_id}/messages saves a user message. If auto_generate is on and nothing is generating, it also starts the run and answers like a generate call; otherwise it returns the saved message and you call POST …/generate yourself. Replay a bad answer posts with it on; Cap spend per end user adds the message and generates in two calls.

idempotency_key on the message body deduplicates within the session: the same key returns the original message with 200 and starts no second generation — which is what makes a retrying webhook safe.

Generation is background by default​

POST …/generate answers 202 Accepted with a generation_id unless you pass wait=true. Poll the generation for the result. A client-type tool pauses the run with requires_action and a required_action payload; resume it with POST …/tool-outputs, which takes the generation_id and one output per pending call.

On a managed provider the model the session's agent would run must carry a price, or the call answers 503 model_not_priced and generates nothing; a project that owes for usage already served answers 402 insufficient_credit until it is topped up. See A managed model must carry a price and A negative balance stops managed generation.

A Free account that has used its plan's monthly runs answers 403 plan_limit_reached with resource: "runs" here as well, on its own credential too. See A plan's run allowance can stop generation.

Handing the outputs back with POST /v1/projects/{project_id}/sessions/{session_id}/tool-outputs is what makes the next call, so it answers 402 insufficient_credit on the same balance. It needs no price check: the generation it continues cleared one when it started, on the same model. Adding a message is refused the same way only on a session with auto_generate on, where the message starts the generation; otherwise it sits in the session until something generates on it.

Debouncing a chatty user​

message_delay_seconds waits that many seconds after the last user message before calling the model, and each new message resets the timer. Three messages typed in quick succession become one generation with all three in context, instead of three runs racing each other.

What a session cost​

GET /v1/projects/{project_id}/sessions/{session_id} carries a usage roll-up: the token counts and cost_usd of every metered generation dispatched through the session. It answers "what did this conversation cost" without joining the generations log against a rate card.

A fork is a session of its own and starts at zero rather than inheriting what the history it copied cost, so summing usage across a session and its forks never double-counts. Work the platform does around a session without a generation of its own is metered on the project, not here — read GET /v1/projects/{project_id}/usage for the project-wide view.

usage is one number. For the same spend split by day, model or provider, pass the session to the project meter as session_id: see narrowing to one session or one end user; Cap spend per end user splits it by end user. The listing omits usage — reading it per session is a read per session, and a customer's dashboard is better served by one narrowed rollup.

Expiry​

inactivity_ttl_seconds expires a session after a quiet period — status becomes expired and it stops accepting messages. 0 means it never expires.

Forking​

POST …/fork branches a new session from a point in this one's history: same context, different continuation. The fork gets its own conversation whose messages reference the same documents as the parent rather than copying them, so there is one stored copy of the shared history. Omit fork_at_position to branch at the tip, and pass agent_id to have a different agent continue. GET …/forks lists one level of lineage: a fork of a fork is listed under its own parent. Replay a bad answer forks at a customer's question so a fixed agent answers it again.

Deleting​

DELETE removes the session, its conversation and their messages. The actor is not deleted, and neither are the generations the session produced — those are records of what happened, not parts of the session.

Examples​

Open a session and send the first message​

SESSION_ID=$(naturali create-session \
--project-id proj_V1StGXR8Z5jdHi6B \
--agent-id agent_V1StGXR8Z5jdHi6B \
--auto-generate true | jq -r .id)

naturali add-session-message \
--project-id proj_V1StGXR8Z5jdHi6B \
--session-id "$SESSION_ID" \
--message "Where is my order?"

Read the transcript​

The transcript lives on the session's conversation:

CONVERSATION_ID=$(naturali get-session \
--project-id proj_V1StGXR8Z5jdHi6B \
--session-id ses_V1StGXR8Z5jdHi6B | jq -r .conversation_id)

naturali list-conversation-messages \
--project-id proj_V1StGXR8Z5jdHi6B \
--conversation-id "$CONVERSATION_ID"

Fork a session to try a different continuation​

naturali fork-session \
--project-id proj_V1StGXR8Z5jdHi6B \
--session-id ses_V1StGXR8Z5jdHi6B \
--fork-at-position 4 \
--name "alternative reply"

Close a session​

naturali update-session \
--project-id proj_V1StGXR8Z5jdHi6B \
--session-id ses_V1StGXR8Z5jdHi6B \
--status closed