Skip to main content

Conversations

A Conversation is a multi-party dialogue within a project: ordered messages, each with a role (user or assistant) and an optional authoring Actor.

Overview​

Each message references a Document, has a role, and optionally an Actor author. Ids are prefixed conv_; the internal primary key is never returned.

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

Data Model​

Conversation​

FieldTypeDescription
idstringPublic identifier prefixed with conv_
project_idstringID of the owning project
namestringOptional human-readable title for the conversation
statusstringConversation status: open or closed
actor_idstringOptional ID of the Actor who owns this conversation (nullable)
retrievalstringembed, none, or null to inherit the project — see Retrieval
tagsobjectFree-form string tags
created_atstringISO 8601 creation timestamp
updated_atstringISO 8601 last-updated timestamp

actor_id is the conversation owner (typically the external contact who initiated it, e.g. a WhatsApp contact), set at creation and distinct from message authorship; other actors can still send messages. List distinct participants with GET /actors?conversation_id=....

Conversation Message​

FieldTypeDescription
document_idstringID of the Document attached as a message
rolestringRole of the message: user or assistant — system is refused with 400 SYSTEM_MESSAGE_NOT_ALLOWED
actor_idstring | nullOptional ID of the Actor who authored the message; null for messages not tied to an actor
agent_idstring | nullOptional ID of the Agent that generated this message; null for non-generated messages
positionintegerZero-based position of the message in the conversation
metadataobject | nullCaller-owned annotations on the message (e.g. phone, channel), stored as sent. Never read by the platform and never part of the model's input — see Tags and metadata
contentstringFull text content of the message (read from the underlying document)

The pair (conversation_id, position) is uniquely indexed. See Message ordering for insertion semantics.

Key Concepts​

Actors, Agents, and Chats​

Actors track authorship; generation is triggered by passing agent_id to POST /conversations/:id/generate and needs no actor. Linking and deletion rules: Agent and Chat Linking. Deleting an Actor is blocked while any conversation message references it.

Messages​

Messages are ordered Document references with a role and optional actor_id. A document may appear once per conversation; adding it twice returns 409 Conflict.

role: "system" is refused with 400 SYSTEM_MESSAGE_NOT_ALLOWED: stored history feeds agent generations, so a system entry would let conversation data rewrite the agent's prompt. System content belongs in the agent's instructions or the actor persona.

Listed messages carry the document's full content, role, actor_id, and agent_id (set on assistant messages produced by POST /conversations/:id/generate, null otherwise). Example: Chat with an LLM - Step 7 (View the conversation history).

Removing a message also deletes its Document and File, and deleting a conversation deletes every turn it holds.

Retrieval​

Each message is stored as a Document under /.system/conversations/{conversation_id}/{document_id}.txt — see the reserved root. retrieval decides whether those turns are embedded for vector search:

ValueEffect
noneTurns are stored and chunked, so they stay readable by id, in the message list, and through the full-text channel of knowledge search. No embedding is made or billed.
embedTurns are embedded as well, so the vector channel ranks them.
nullInherits the project's default_conversation_retrieval (itself none by default).

A turn is embedded because someone asked for the conversation to be retrievable, never merely because it was said. Setting retrieval to embed on PATCH /conversations/{conversation_id} embeds the turns already in the conversation, so the whole conversation becomes retrievable rather than only what is said next; setting it back to none stops new turns being embedded and leaves the vectors already paid for in place.

Either way a message document is the runtime's: it is read-only through the document API, and an unfiltered document list or a bare knowledge query leaves it out unless the request names /.system/conversations/.

What a turn is stamped with​

Every message document carries the provenance of the turn in its tags, written by the platform:

KeyValue
system.conversationThis conversation's id
system.actorThe conversation's owner (actor_id), absent when it has none
system.agentThe agent that generated the message, absent on a user turn
system.roleuser or assistant

system.actor is the owner rather than the message's author, because the owner is stable for the whole conversation and is what an isolation rule names; per-message authorship stays on the message's own actor_id.

These keys are read-only. A write naming any key starting with system. — on a create, a tag sub-endpoint or a formation template — is refused with 400 RESERVED_TAG_KEY: a caller who could set system.actor could make their own rows answer to another actor's filter.

They are ordinary tags otherwise, so one filter selects an actor's turns:

{ "query": "refund policy", "tags": { "system.actor": "actor_V1StGXR8Z5jdHi6B" } }

Naming a system.* key is also what reaches the reserved root, so the filter needs no document_paths beside it. A memory a memory rule extracted from a conversation carries the same system.conversation and system.actor, so that one filter returns the actor's raw turns and the facts distilled from them together. To fence one actor's turns from another, put the same key in an IAM condition — see IAM — Tags.

Tags​

Key-value string pairs managed via the tag sub-endpoints and matched by soat:ResourceTag/<key>. GET /api/v1/conversations filters by pair with ?tags=key:value (repeatable, all must match). See IAM — Tags.

Tool-call chains (invocations and results alongside the final text) are preserved internally so later turns see the full exchange; this state is separate from metadata and never returned.

Message ordering​

No two messages share a slot:

  • Append (default): omitted position writes at MAX(position) + 1.
  • Insert between: a colliding position shifts that message and all after it up by one in a single transaction.
  • Concurrent writes at the same position race on the unique index; the loser gets 409 Conflict and retries.

Generating the Next Message​

Any Agent can generate the next message from the history. Provider-backed example: Connect Third-Party LLMs - Step 6 (Start a conversation).

POST /api/v1/conversations/:id/generate?wait=true
{ "agent_id": "agent_...", "stream": false }

By default the call runs in the background and returns 202 Accepted ({ "status": "accepted", "conversation_id": "conv_..." }); poll GET /conversations/:id/messages for the reply. The agent is resolved synchronously, so an unknown agent_id is a 404. It is one of the conversation's project's own agents or one another project shares with it; a shared agent's turn is recorded and metered in the conversation's project.

?wait=true blocks and returns the result inline; it is required to observe requires_action (client tools). See Synchronous & Asynchronous Execution for the platform-wide wait contract.

Flow (with ?wait=true):

  1. Load messages ordered by position.

  2. Compose the system prompt from the agent's instructions, plus a line naming the agent and telling it to reply as itself, without a speaker label.

  3. Map role: 'assistant' messages to assistant turns; all others to user turns, prefixed [<speaker name>]: . A message's metadata is not part of the turn — the model sees message and nothing else the caller attached.

  4. Dispatch to the Agents module, including agent tools and the requires_action client-tool flow.

  5. On completed, a new Document is attached as the next message with role: 'assistant'. The response includes:

    • content — the generated text (always a string).
    • message — the persisted ConversationMessageRecord (document_id, role, actor_id, agent_id, position, content); agent_id is the generating agent.
    • generation_id and trace_id.
    • model — the model used.
    soat generate-conversation-message \
    --conversation-id conv_01 \
    --agent-id agent_01 \
    --wait true
  6. On requires_action (client tools), no message is persisted yet. Submit outputs via POST /agents/:id/generate/:generation_id/tool-outputs; the message is persisted on completion.

Concurrency​

Generate calls hold a per-conversation advisory lock, so concurrent calls on one conversation are serialized and never race for a position.

Streaming​

With "stream": true the response is text/event-stream. The message is persisted only after the stream completes; partial streams produce none. The final event carries document_id, generation_id, and trace_id.

Tool Context​

POST /api/v1/conversations/:id/generate accepts tool_context, forwarded verbatim to the agent generation; see Tool Context.

Filtering by Actor​

GET /conversations?actor_id=... lists conversations where the actor authored at least one message (an EXISTS join on conversation_messages, costlier than the default listing).

Status​

open or closed, default open; change via PATCH /conversations/:id.

Examples​

Create a conversation and add a message​

soat create-conversation --project-id proj_ABC --name "Support Thread"
soat add-conversation-message \
--conversation-id conv_01 \
--message "Hello, I need help." \
--role user

Generate the next message​

soat generate-conversation-message --wait true \
--conversation-id conv_01 \
--agent-id agent_01