Skip to main content

Conversations

The Conversations module represents a multi-party dialogue within a project. A Conversation groups ordered messages, each carrying an explicit role (user or assistant) and an optional reference to an Actor for authorship tracking.

Overview

A Conversation belongs to a project and contains an ordered list of messages. Each message references a Document, has a role, and optionally references an Actor as its author.

Conversations are identified by an id prefixed with conv_. The internal database 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)
tagsobjectFree-form string tags
created_atstringISO 8601 creation timestamp
updated_atstringISO 8601 last-updated timestamp

actor_id identifies the owner of the conversation — typically the external contact who initiated the thread (e.g. a WhatsApp contact). This is a direct ownership reference set at creation time and is distinct from message authorship: multiple actors can still participate by sending messages. To list all distinct message participants, filter the Actors listing by conversation: GET /actors?conversation_id=....

Conversation Message

FieldTypeDescription
document_idstringID of the Document attached as a message
rolestringRole of the message: user or assistantsystem 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 | nullOptional structured key-value data attached to the message (e.g. phone, channel). Injected into the AI prompt context.
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 who wrote a message (authorship); generation is triggered separately by passing agent_id directly to POST /conversations/:id/generate — no actor is required. For actor↔agent/chat linking and deletion rules, see Agent and Chat Linking. Deleting an Actor is blocked while any conversation message references it — remove its messages (or delete the containing conversations) first.

Messages

Messages are ordered references to Documents within a conversation. Each message has a role (user or assistant) and an optional actor_id for authorship tracking. Each document can appear at most once per conversation — adding the same document twice returns 409 Conflict.

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

When listing messages, each entry includes the full text content of the underlying document, the message role, the optional authoring actor_id, and the optional agent_id of the Agent that generated it (set for assistant messages produced by POST /conversations/:id/generate, null otherwise). See it end to end in Chat with an LLM - Step 7 (View the conversation history).

Removing a message from a conversation also deletes its underlying Document and the associated File on disk, preventing orphaned records.

When a generation includes tool calls, the full tool-call chain (the assistant's tool invocations and their results, alongside the final text) is preserved internally so later turns see the complete exchange. This internal state is stored separately from metadata — it is never part of the caller-supplied bag and is not returned by any API response.

Message ordering

The unique index (conversation_id, position) enforces that no two messages share a slot.

  • Append (default): if position is omitted, the new message is written at MAX(position) + 1.
  • Insert between: if an explicit position collides with an existing message, all messages at position and after are shifted up by one in a single transaction, and the new message is inserted.
  • Concurrent writes: two concurrent appends or inserts at the same position race on the unique index; the loser receives 409 Conflict and must retry.

Generating the Next Message

Any Agent can generate the next message from the conversation history. For a provider-backed agent driving a fresh thread, see Connect Third-Party LLMs - Step 6 (Start a conversation).

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

The call runs in the background by default and returns 202 Accepted immediately ({ "status": "accepted", "conversation_id": "conv_..." }); the reply lands as a new message when it completes, so poll GET /conversations/:id/messages for it. The agent is still resolved synchronously, so an unknown agent_id is a 404 rather than a failure you discover by polling.

Pass ?wait=true to block and receive the result inline, as the flow below describes. Waiting is required to observe requires_action (client tools), so a client-tool flow should always pass it. See Synchronous & Asynchronous Execution for the platform-wide wait contract.

Flow (with ?wait=true):

  1. Load all messages ordered by position.

  2. Compose the effective system prompt from the agent's instructions.

  3. Map each message to a model message using the stored role field. Messages with role: 'assistant' become assistant turns; all others become user turns.

  4. Dispatch to the Agents module, reusing its generation plumbing — including agent tools and the requires_action client-tool flow.

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

    • content — the AI-generated text of the reply (the canonical field; always a string).
    • message — the persisted ConversationMessageRecord (document_id, role, actor_id, agent_id, position, content). agent_id is set to the ID of the generating agent.
    • generation_id and trace_id for observability.
    • model — the model name used for this generation.
    const { data } = await soat.conversations.generateConversationMessage({
    path: { conversation_id },
    query: { wait: true },
    body: { agent_id: agentId },
    });
    // data.content is always the AI-generated text when data.status === 'completed'
    const responseText = data?.content;
  6. On requires_action (agent client tools only), no message is persisted yet. Submit outputs via POST /agents/:id/generate/:generation_id/tool-outputs; the resolved message is persisted on completion.

Concurrency

Generate calls acquire a per-conversation advisory lock for the duration of the request. Concurrent generate calls on the same conversation are serialized to prevent two assistant messages racing for the same position.

Streaming

With "stream": true, the response is a text/event-stream emitting incremental tokens. The new message is persisted only after the stream completes successfully; partial streams produce no message. The final SSE event carries the document_id, generation_id, and trace_id.

Tool Context

POST /api/v1/conversations/:id/generate accepts an optional tool_context field in the request body, forwarded verbatim to the underlying agent generation — see the Tool Context reference.

Filtering by Actor

Use GET /conversations?actor_id=... to list conversations in which the given actor has authored at least one message. This is evaluated via an EXISTS join on conversation_messages and is more expensive than the default listing.

Status

A conversation transitions between open and closed. Use PATCH /conversations/:id to update the status. New conversations default to open.

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