# Conversations

> Multi-party dialogues that group ordered, role-tagged messages with optional actor authorship within a SOAT project.

# 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](./actors.md) for authorship tracking.

## Overview

A Conversation belongs to a project and contains an ordered list of messages. Each message references a [Document](./documents.md), has a `role`, and optionally references an [Actor](./actors.md) as its author.

Conversations are identified by an `id` prefixed with `conv_`. The internal database primary key is never returned.

> See the [Permissions Reference](../permissions.md) for the IAM action strings for this module.

## Related Tutorials

- [Chat with an LLM - Step 7 (View the conversation history)](/docs/tutorials/chat-with-llm#step-7--view-the-conversation-history)
- [Connect Third-Party LLMs - Step 6 (Start a conversation)](/docs/tutorials/connect-third-party-llms#step-6--start-a-conversation)
- [Debug Session, Generation, and Trace History - Step 4 (Retrieve the full session message timeline)](/docs/tutorials/debug-session-generation-trace-history#step-4---retrieve-the-full-session-message-timeline)

## Data Model

### Conversation

| Field        | Type   | Description                                                        |
| ------------ | ------ | ------------------------------------------------------------------ |
| `id`         | string | Public identifier prefixed with `conv_`                            |
| `project_id` | string | ID of the owning project                                           |
| `name`       | string | Optional human-readable title for the conversation                 |
| `status`     | string | Conversation status: `open` or `closed`                            |
| `actor_id`   | string | Optional ID of the Actor who **owns** this conversation (nullable) |
| `tags`       | object | Free-form string tags                                              |
| `created_at` | string | ISO 8601 creation timestamp                                        |
| `updated_at` | string | ISO 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](./actors.md) listing by conversation: [`GET /actors?conversation_id=...`](/docs/api/actors/list-actors).

### Conversation Message

| Field         | Type           | Description                                                                                                                |
| ------------- | -------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `document_id` | string         | ID of the Document attached as a message                                                                                   |
| `role`        | string         | Role of the message: `user` or `assistant` — `system` is refused with `400 SYSTEM_MESSAGE_NOT_ALLOWED` |
| `actor_id`    | string \| null | Optional ID of the Actor who authored the message; `null` for messages not tied to an actor                                |
| `agent_id`    | string \| null | Optional ID of the Agent that generated this message; `null` for non-generated messages                                    |
| `position`    | integer        | Zero-based position of the message in the conversation                                                                     |
| `metadata`    | object \| null | Optional structured key-value data attached to the message (e.g. `phone`, `channel`). Injected into the AI prompt context. |
| `content`     | string         | Full text content of the message (read from the underlying document)                                                       |

The pair `(conversation_id, position)` is uniquely indexed. See [Message ordering](#message-ordering) for insertion semantics.

## Key Concepts

### Actors, Agents, and Chats

[Actors](./actors.md) track _who_ wrote a message (authorship); generation is triggered separately by passing `agent_id` directly to [`POST /conversations/:id/generate`](/docs/api/conversations/generate-conversation-message) — no actor is required. For actor↔agent/chat linking and deletion rules, see [Agent and Chat Linking](./actors.md#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](./agents.md#instructions), 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](./actors.md).

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`](/docs/api/conversations/generate-conversation-message), `null` otherwise). See it end to end in [Chat with an LLM - Step 7 (View the conversation history)](/docs/tutorials/chat-with-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](./agents.md) 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)](/docs/tutorials/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`](/docs/api/conversations/list-conversation-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](../advanced/sync-and-async.md) 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.

   ```ts
   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`](/docs/api/agents/submit-agent-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`](/docs/api/conversations/generate-conversation-message) accepts an optional `tool_context` field in the request body, forwarded verbatim to the underlying agent generation — see the [Tool Context reference](../advanced/tool-context.md).

### Filtering by Actor

Use [`GET /conversations?actor_id=...`](/docs/api/conversations/list-conversations) 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`](/docs/api/conversations/update-conversation) to update the status. New conversations default to `open`.

## Examples

### Create a conversation and add a message

<Tabs groupId="client">
<TabItem value="cli" label="CLI" default>

```bash
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
```

</TabItem>
<TabItem value="sdk" label="SDK">

```ts
// SDK

const soat = new SoatClient({
  baseUrl: 'https://api.example.com',
  token: 'sk_...',
});

const { data: conv } = await soat.conversations.createConversation({
  body: { project_id: 'proj_ABC', name: 'Support Thread' },
});

const { data: msg } = await soat.conversations.addConversationMessage({
  path: { conversation_id: conv.id },
  body: { message: 'Hello, I need help.', role: 'user' },
});
```

</TabItem>
<TabItem value="curl" label="curl">

```bash
curl -X POST https://api.example.com/api/v1/conversations \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"project_id": "proj_ABC", "name": "Support Thread"}'

curl -X POST https://api.example.com/api/v1/conversations/conv_01/messages \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"message": "Hello, I need help.", "role": "user"}'
```

</TabItem>
</Tabs>

### Generate the next message

<Tabs groupId="client">
<TabItem value="cli" label="CLI" default>

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

</TabItem>
<TabItem value="sdk" label="SDK">

```ts
// SDK
const { data: reply } = await soat.conversations.generateConversationMessage({
  path: { conversation_id: 'conv_01' },
  query: { wait: true },
  body: { agent_id: 'agent_01' },
});
```

</TabItem>
<TabItem value="curl" label="curl">

```bash
curl -X POST https://api.example.com/api/v1/conversations/conv_01/generate?wait=true \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{"agent_id": "agent_01"}'
```

</TabItem>
</Tabs>
