Skip to main content

Sessions

A simplified 1 user ↔ 1 agent conversational interface, owned by an agent.

Overview

Sessions hide the underlying Conversation and generation plumbing. The Actor is not hidden and not created for you — link one with actor_id when the session represents a specific end user. By default, interacting with an agent requires three API calls: create a session, save a user message, and trigger generation. When auto_generate is enabled, the message and generation collapse into a single call. Walk through it end to end in Chat with an LLM - Step 5 (Create a session) and Step 6 (Send messages and receive replies).

Sessions are a top-level resource at /sessions. Each session belongs to an Agent — set agent_id on create, and filter by it with GET /sessions?agent_id=. Each session exposes its conversation_id as an escape hatch to the full Conversations API; list a session's messages via GET /conversations/:conversation_id/messages (this is governed by conversations:GetConversation, not the agents:* session actions).

See the Permissions Reference for the IAM action strings for this module.

Data Model

Session

FieldTypeDescription
idstringPublic identifier prefixed with sess_
agent_idstringPublic ID of the agent this session belongs to
conversation_idstringPublic ID of the underlying conversation
statusstringopen (default), closed, or expired
namestringOptional display name
actor_idstring | nullOptional public ID of the Actor linked on create (actor_ prefix); null when none was supplied
tagsobjectFree-form key-value metadata
auto_generatebooleanWhen true, saving a message automatically triggers LLM generation (default: false)
message_delay_secondsinteger | nullDebounce delay in seconds before the LLM is called after a user message. null means no delay (default).
inactivity_ttl_secondsintegerSeconds of inactivity before the session expires. 0 means never expires (default: 0)
last_activity_atstring | nullISO 8601 timestamp of the last user message; null until the first message is added
forked_from_session_idstring | nullPublic ID of the session this one was forked from; null when it is not a fork, or once the parent is deleted
forked_from_positioninteger | nullParent conversation position this session branched after; null when it is not a fork or was forked at the tip
created_atstringISO 8601 creation timestamp
updated_atstringISO 8601 last-updated timestamp

Message (within a session)

FieldTypeDescription
rolestringuser or assistant
contentstringMessage text
modelstringModel used for assistant messages
created_atstringISO 8601 timestamp

When creating a session message (POST .../messages), send exactly one of:

  • message: raw text body
  • document_id: public ID of an existing document (its content is used as the message text)

An optional idempotency_key string can be included with either variant — see Idempotency.

Key Concepts

How Sessions Relate to Other Concepts

ConceptRelationship
ChatsRaw LLM completions — no agents, no tools, caller manages history
Sessions1 user ↔ 1 agent — full tool support, automatic history, owned by an agent
ConversationsMulti-party dialogue engine — powers sessions internally, available as escape hatch

The Session's End User (Actor)

A session has an end user only when actor_id is supplied on create. Actors are created separately and are never auto-created here. This matters beyond naming: end-user attribution on the resulting usage events is derived from the session's actor, so a session without one produces generations that match no actor-scoped quota. Attach an actor before relying on a per-user spend cap.

Lifecycle

A session starts in open status. It can be updated to closed when the interaction is complete. If inactivity_ttl_seconds is configured, the status transitions to expired lazily when the session is next fetched or listed after the TTL elapses. See Deletion for what happens when a session is deleted.

Deletion

DELETE .../sessions/:session_id removes the session row and its underlying Conversation row in the same transaction. Deleting the conversation cascades at the database level to every message in it.

What deletion does not remove:

  • The session's actor. The Actor referenced by actor_id is left untouched and can still be looked up or reused by other sessions.
  • Documents backing message content. Each message's content is stored in a Document row; deleting the session does not delete these documents (or their underlying files), so they remain in place after the session and its messages are gone.
  • Generations and traces. A session's generations and traces are not linked to the session or conversation record, so they are unaffected by session deletion and remain queryable via GET /api/v1/traces/{trace_id} after the session no longer exists.

Delete these resources explicitly beforehand if you need a full cleanup.

Forking

POST /api/v1/sessions/{session_id}/fork branches a new session from a point in an existing one: same context, different continuation. It answers "what if" — a support agent gave a bad answer at message 7, and you want to try a stricter prompt or a different agent version against that exact context without replaying the conversation by hand.

curl -X POST "$SOAT_URL/api/v1/sessions/$SESSION_ID/fork" \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"fork_at_position": 7,
"agent_id": "agent_V1StGXR8Z5jdHi6B",
"name": "retry with stricter system prompt",
"tags": { "experiment": "prompt-v2" }
}'
FieldDefaultMeaning
fork_at_positionbranch at the tipParent position to branch after; positions 0..N are carried over
agent_idthe parent's agentThe agent the fork runs against. Must be in the same project
name, tagsSet on the new session
tool_contextinheritedOverrides the parent's tool context on the fork

Fork by reference, not by copy. The fork gets its own conversation whose messages point at the same Document rows as the parent — only the ordering is duplicated. There is one stored copy of the content, so a retention purge erases it from parent and fork together, and the branch cannot drift from what actually happened.

Replay, never re-invoke. Recorded tool calls and their results ride along on the copied messages and are replayed as model input on the fork's next turn. Forking never calls a tool, so exploring a "what if" cannot send an email or charge a card a second time. The trade-off is that a forked turn sees the tool data as it was, not as it is now — right for comparison, wrong for "resume this session for real".

The fork is inert. auto_generate is false and no generation is triggered; drive the branch with the normal POST .../messages and POST .../generate endpoints. It also starts without an actor, because single session per actor allows one open session per (agent, actor) pair and inheriting the parent's actor would make forking impossible for exactly those agents.

Lineage reads back on the session itself (forked_from_session_id, forked_from_position) and from the parent:

curl "$SOAT_URL/api/v1/sessions/$SESSION_ID/forks" -H "Authorization: Bearer $TOKEN"

That walks one level. Forking a fork is allowed and unbounded; each fork is listed under its own parent. Deleting a parent does not delete its forks — they keep their history and their forked_from_session_id becomes null.

Forking requires both agents:GetSession and agents:CreateSession: it reads a session's full history and creates a new session, and neither permission alone should imply the other.

Auto-Generate

When auto_generate is true, POST .../messages saves the user message and automatically triggers LLM generation in the same request. The response body contains the assistant reply instead of just the saved user message.

This collapses the three-call flow into two calls: create a session, then send messages. auto_generate defaults to false and can be set at creation or toggled at any time via PATCH /sessions/{session_id}.

The explicit POST .../generate endpoint continues to work regardless of this setting. With auto_generate enabled, POST .../messages returns as soon as the message is saved and the triggered generation runs in the background.

Message Delay (Debounce)

When message_delay_seconds is set, POST .../messages does not trigger LLM generation immediately. A timer starts and resets with each new message. The LLM is only called after the configured delay elapses with no new messages.

With message_delay_seconds: 3, three rapid messages ("What's the" / "weather in" / "Paris?") each reset the timer; after 3 seconds of silence the LLM is called once with all three messages in context.

POST .../messages always returns immediately with the saved user message, regardless of the delay setting. Generation fires asynchronously after the delay elapses.

message_delay_seconds has no effect when auto_generate is false or when a generation is already in progress.

Single Session Per Actor

When the parent agent has single_session_per_actor: true, creating a session with an actor_id returns 409 Conflict if an open session for that actor already exists. The error body includes meta.session_id with the existing session's ID. See Single Session Per Actor on the Agents module.

Idempotency

Channels like WhatsApp use at-least-once webhook delivery — the same inbound message may arrive multiple times. Pass idempotency_key in the POST .../messages body to deduplicate:

{
"message": "Hello",
"idempotency_key": "wamid.HBgLNTUxMTk4..."
}
  • First call — message is saved and generation is triggered if auto_generate is on. Returns 201 Created.
  • Subsequent calls with the same key — returns the original message with 200 OK. No new message or generation is created.

The key is scoped to the session.

Inactivity TTL

Sessions can expire automatically after a period of inactivity using inactivity_ttl_seconds.

  • 0 (default) — the session never expires.
  • Positive integer — the session expires if no user message has been added for that many seconds since last_activity_at.

When a session exceeds its TTL, its status is lazily updated to expired the next time it is fetched or listed. Once expired, POST .../generate returns 410 Gone with error code SESSION_EXPIRED — open a fresh session to continue.

The TTL can be updated at any time via PATCH .../sessions/{session_id} — the inactivity clock continues from the last last_activity_at timestamp, so changing the TTL takes effect on the next fetch.

Tool Context

Sessions support the same tool_context mechanism as direct agent generations — see the Tool Context reference for the key→header rule, validation and security notes.

When a generation is triggered through a session, the server automatically injects the following keys into tool_context:

Injected keyForwarded headerValue
actor_idX-Soat-Context-actor_idPublic ID of the session's actor; omitted if not set
actor_external_idX-Soat-Context-actor_external_idExternal ID of the session's actor; omitted if not set
session_idX-Soat-Context-session_idPublic ID of the session; always present

These three keys are always taken from the session and actor, regardless of what the caller supplies — a caller-provided actor_id, actor_external_id or session_id in either the session's stored tool_context or a per-request tool_context is ignored in favor of the auto-populated value. Any other key a caller sets in tool_context is unaffected and still wins in the usual way (a per-request value overrides the session's stored value).

Note that actor_external_id carries the actor's external_id to every http and mcp tool the agent calls; see Actors if that value holds PII.

Background Generation

POST .../generate runs in the background by default and returns immediately with 202 Accepted. Pass ?wait=true to block until the generation settles and receive the assistant reply in the response. See Synchronous & Asynchronous Execution for the platform-wide wait contract. The default 202 body:

{ "status": "accepted", "session_id": "sess_..." }

When a new generation request arrives while a previous one is still in-flight, the server cancels the previous generation and starts a fresh one so the model always sees the complete, up-to-date message history.

Debugging (Session, Generation, Trace)

Each call to POST .../generate returns generation_id and trace_id. Store these alongside session_id for debugging:

See Traces for the full correlation strategy.

Webhook Events

The following events are dispatched to project webhooks as sessions change state:

Event typeTrigger
sessions.createdA new session is created
sessions.updatedA session's name, status, or tags are changed
sessions.deletedA session is deleted
sessions.generation.completedLLM generation finished successfully
sessions.generation.requires_actionLLM returned a client-tool call requiring tool outputs
sessions.generation.startedLLM generation has started for a session

All events include session_id. Generation events additionally include generation_id and trace_id. Permissions are namespaced under agents: since each session belongs to an agent.

Examples

Basic session flow

soat create-session --agent-id agent_01 --name "My Session"
soat add-session-message --session-id sess_01 --message "Hello!"
soat generate-session-response --wait true --session-id sess_01

List sessions

Filter by agent, actor, or status.

soat list-sessions --agent-id agent_01 --status open