# Conversation State

> **Info:** This is documentation for **Stream Chat React SDK v14**, which is no longer actively maintained. For up-to-date documentation, see the [latest version (v15)](https://getstream.io/chat/docs/sdk/react/).

`AIConversation` shows a conversation, and `AIMessageComposer` takes what the user writes. Neither keeps the conversation itself: they show the data you give them, and hand you what the user does. With Stream Chat, the channel holds that data: its messages, with the agent's replies and their steps, and its `ai_indicator.*` events, which say what the agent is doing. This page covers what the components take, how to read it from a channel, and what your backend writes into the channel for them to show.

```tsx
import { useEffect, useState } from "react";
import type { Channel, LocalMessage } from "stream-chat";
import { AIStates, useAIState } from "stream-chat-react";
import {
  AIConversation,
  AITypingIndicator,
  fromStreamMessage,
} from "stream-chat-react/ai-components";

export const AssistantConversation = ({ channel }: { channel: Channel }) => {
  const [messages, setMessages] = useState<LocalMessage[]>([]);
  const { aiState } = useAIState(channel);

  useEffect(() => {
    const update = () => setMessages([...channel.state.messages]);
    update();
    const subscription = channel.on(update);
    return () => subscription.unsubscribe();
  }, [channel]);

  return (
    <AIConversation
      messages={messages.map((message) => fromStreamMessage(message))}
      footer={
        aiState === AIStates.ExternalSources ? (
          <AITypingIndicator text="Checking external sources" />
        ) : undefined
      }
    />
  );
};
```

A channel's state lists its loaded messages oldest first, which is the order the conversation expects, and the channel's events say when the backend writes the reply. Load the channel with `channel.watch()` before you show it. [Conversation view](https://getstream.io/chat/docs/sdk/react/v14/ai-components/conversation-view/) covers the rest of what `AIConversation` takes.

>
> **Info:** `AIConversation`, `fromStreamMessage` and the message parts are available from version 14.15.0 of `stream-chat-react`, in the `stream-chat-react/ai-components` entry point. See the [Quickstart](https://getstream.io/chat/docs/sdk/react/v14/ai-components/) to install them.
>

## Messages

The components take each message as an `AIMessage`:

| Field           | Description                                                                                                                                       | Default |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | ------- |
| `id`            | The message's stable identity.                                                                                                                    | -       |
| `role`          | Who wrote it: `"user"`, `"assistant"`, or a role of your own, which shows as the agent's. Each of the user's messages starts a turn.              | -       |
| `text`          | What it says. A reply's text is Markdown.                                                                                                         | -       |
| `isGenerating`  | Whether the agent is still writing it.                                                                                                            | `false` |
| `parts`         | The [steps](#steps-of-a-reply) the agent took for it, in order.                                                                                   | `[]`    |
| `attachments`   | What it carries besides its text, such as images and files, as `AIMessageAttachmentData`.                                                         | `[]`    |
| `reasoningText` | `(stepId) => string`: the whole of a reasoning step's thinking, when your app has it. Otherwise the step's excerpt shows.                         | -       |
| `uiDocument`    | The UI the agent wrote for a reply, which shows in place of its text. See [Generative UI](https://getstream.io/chat/docs/sdk/react/v14/ai-components/generative-ui/). | -       |

A message the user sent that isn't in the channel yet, such as while its photos upload, is an `AIPendingMessage`, with an `id`, a `text` and `attachments`. [Show a message while it's sent](https://getstream.io/chat/docs/sdk/react/v14/ai-components/conversation-view/#show-a-message-while-its-sent) covers it.

### Messages from Stream Chat

`fromStreamMessage` turns a Stream Chat message into an `AIMessage`:

| `AIMessage`    | From the Stream Chat message                                                 |
| -------------- | ---------------------------------------------------------------------------- |
| `id`, `text`   | Its `id` and `text`.                                                         |
| `role`         | `"assistant"` when `ai_generated` is `true`, `"user"` otherwise.             |
| `isGenerating` | `generating`.                                                                |
| `parts`        | Its `ai_reasoning` and `ai_tool_call` attachments.                           |
| `attachments`  | Its `image` and `file` attachments. Attachments of other types are left out. |
| `uiDocument`   | `ai_ui`, on a reply only.                                                    |

Pass `isAssistant` to tell the agent's messages apart some other way, such as by the user who sent them, and `reasoningText` when your app has the whole of the reasoning:

```tsx
fromStreamMessage(message, {
  isAssistant: (message) => message.user?.id === "juno",
  reasoningText: (messageId, stepId) => reasoning.get(messageId, stepId),
});
```

[What your backend sends](#what-your-backend-sends) covers how the backend sets these fields.

## Steps of a reply

An agent writes each step it takes while replying, a round of reasoning or a tool call, as a custom attachment on its reply. `parseMessageParts(message.attachments)` reads them, skipping attachments that aren't steps, and `fromStreamMessage` does it for you. Each step is an `AIMessagePartData`:

| Field       | Description                                                                                                          |
| ----------- | -------------------------------------------------------------------------------------------------------------------- |
| `id`        | The step's stable identity.                                                                                          |
| `kind`      | The attachment type: `ai_reasoning`, `ai_tool_call`, or another `ai_` type.                                          |
| `reasoning` | A round of reasoning, read: its `status`, and its `summary`, `preview` and `durationMs`.                             |
| `toolCall`  | A tool call, read: its `name`, `status` and `executor`, its arguments, and the approval or sign-in it waits for.     |
| `payload`   | The attachment's fields, for a kind of your own or fields the SDK doesn't read.                                      |
| `version`   | The step's format version (`v`). A step in a newer version keeps its `payload` but has no `reasoning` or `toolCall`. |

The kinds and statuses are open sets, so new ones don't break your code. [Reasoning and Tool Calls](https://getstream.io/chat/docs/sdk/react/v14/ai-components/reasoning-and-tool-calls/) shows the steps, and its [Step format](https://getstream.io/chat/docs/sdk/react/v14/ai-components/reasoning-and-tool-calls/#step-format) lists what the backend writes.

## Follow what the agent is doing

`useAIState` from `stream-chat-react` returns what the agent is doing in a channel, as `aiState`:

```tsx
import {
  AIStates,
  useAIState,
  useChannelStateContext,
} from "stream-chat-react";

export const useIsAnswering = () => {
  const { channel } = useChannelStateContext();
  const { aiState } = useAIState(channel);

  return (
    aiState === AIStates.Thinking ||
    aiState === AIStates.ExternalSources ||
    aiState === AIStates.Generating
  );
};
```

`aiState` is one of the `AIStates` from `stream-chat-react`, or a state of your own that your backend sends. [Say what the agent is doing](#say-what-the-agent-is-doing) lists them, and the events that set them.

A few things to know about it:

- **Pass it the channel.** It takes the channel as its argument, and has no default. Without one, it never subscribes and always returns `AI_STATE_IDLE`. Inside `Channel`, get the channel from `useChannelStateContext()`.
- **It follows the events.** The hook listens for the channel's `ai_indicator.*` events, and starts from `AI_STATE_IDLE` when it mounts. So a user who opens the channel halfway through a reply sees `AI_STATE_IDLE` until the next event arrives.
- **Call it inside `Channel`.** `Channel` mounts its children again when the active channel changes, so the state starts from `AI_STATE_IDLE` for each channel. If you call the hook outside `Channel` and switch the channel yourself, the previous channel's state stays until the next event arrives.
- **Compare with `AIStates.ExternalSources`.** The `AIState` type in `stream-chat` suggests `AI_STATE_CHECKING_SOURCES`, but the backends send `AI_STATE_EXTERNAL_SOURCES`, which is the value of `AIStates.ExternalSources`.

To show the state, see [Show what the agent is doing](https://getstream.io/chat/docs/sdk/react/v14/ai-components/streaming-messages/#show-what-the-agent-is-doing). To stop the reply, call `channel.stopAIResponse()`, as [Stop the reply](https://getstream.io/chat/docs/sdk/react/v14/ai-components/composer/#stop-the-reply) in the composer shows.

## What your backend sends

With Stream Chat, your backend writes the agent's reply into the channel, and the components show what it writes. A typical backend posts an empty placeholder message as the agent, updates it as the model streams, and stores the final text. [Stream Agents](https://getstream.io/agents/docs/quickstart/), the [Stream Chat AI SDK](https://getstream.io/chat/docs/sdk/react/v14/ai-components/stream-chat-ai-sdk/) and the [LangChain SDK](https://getstream.io/chat/docs/sdk/react/v14/ai-components/stream-chat-langchain-sdk/) do all of this for you. With a backend of your own, follow the conventions below, which the components and Stream's backend SDKs share. The [AI message streaming](https://getstream.io/chat/docs/node/ai-message-streaming/) guide covers the whole backend flow.

### Mark the agent's replies

Set two custom fields on the agent's reply:

- `ai_generated: true`, when the backend creates the placeholder. Your app reads it to tell the agent's replies apart, and your backend checks it to skip the agent's own replies instead of answering them.
- `generating`, `true` on every update while the reply streams, and `false` on the final one. It's the only field that tells a client a reply is in flight.

```js label="Node.js"
const { message } = await channel.sendMessage({
  text: "",
  user_id: botId,
  ai_generated: true,
});
```

```py label="Python"
placeholder = channel.send_message(
    MessageRequest(text="", user_id=bot_id, custom={"ai_generated": True}),
).data.message
```

The client reads them as `message.ai_generated` and `message.generating`. Declare them for TypeScript as optional fields, so messages without them still type-check. [TypeScript and custom data types](https://getstream.io/chat/docs/sdk/react/v14/guides/typescript-and-custom-data-types/) explains how the declaration file is picked up:

```ts title="stream-custom-data.d.ts"
import "stream-chat";

declare module "stream-chat" {
  interface CustomMessageData {
    ai_generated?: boolean;
    generating?: boolean;
  }
}
```

The SDK uses them in these places:

- **`fromStreamMessage`** reads `ai_generated` as the message's `role`, and `generating` as `isGenerating`.
- **The message list.** Pass `isMessageAIGenerated={(message) => !!message.ai_generated}` to `Chat`. `MessageUI` then renders those messages with `StreamedMessageText`, which types them out, and hides their "Edited" label, which every update of the text would show. The channel list shows their preview as plain text instead of rendering its Markdown. [SDK integration](https://getstream.io/chat/docs/sdk/react/v14/ai-components/sdk-integration/#streamedmessagetext) shows the setup.

The message list doesn't read `generating`: a reply types out only the text it grows by after it shows, so a stored reply doesn't type itself out again. Read `generating` in UI of your own, for example to show a cursor while the reply streams.

### Say what the agent is doing

Before the first word of the reply arrives, the model can spend a while thinking or calling tools, and the message is still empty. The backend says what it's doing with `ai_indicator.*` custom events on the channel, which `useAIState` follows:

| Event type            | Sent by | `aiState` becomes      | Means                                                                                                                 |
| --------------------- | ------- | ---------------------- | --------------------------------------------------------------------------------------------------------------------- |
| `ai_indicator.update` | Backend | The event's `ai_state` | The agent's state changed. Carries the `ai_state`, the `cid`, the reply's `message_id`, and an optional `ai_message`. |
| `ai_indicator.clear`  | Backend | `AI_STATE_IDLE`        | The reply is done. Sent after the final text is stored.                                                               |
| `ai_indicator.stop`   | App     | `AI_STATE_STOP`        | The user stopped the reply. See [Stop the reply](#stop-the-reply).                                                    |

The `ai_state` of an update is one of these:

| `ai_state`                  | `AIStates`                 | The agent is...                                                  |
| --------------------------- | -------------------------- | ---------------------------------------------------------------- |
| `AI_STATE_THINKING`         | `AIStates.Thinking`        | Thinking.                                                        |
| `AI_STATE_EXTERNAL_SOURCES` | `AIStates.ExternalSources` | Calling a tool, such as a search.                                |
| `AI_STATE_GENERATING`       | `AIStates.Generating`      | Writing the reply, which streams into the message.               |
| `AI_STATE_ERROR`            | `AIStates.Error`           | Unable to answer. The backend writes the error into the message. |

A typical reply sends `AI_STATE_THINKING` right after the placeholder, `AI_STATE_EXTERNAL_SOURCES` while a tool call runs, `AI_STATE_GENERATING` with the first token, and `ai_indicator.clear` once the final text is stored. Use these names, so the components' states match your backend's. If your backend sends states of its own, read them from `aiState` the same way, and show them with components of your own.

The Stream backend SDKs don't send `ai_indicator.clear` after an error, so `aiState` stays `AI_STATE_ERROR` until the next reply starts. Anything you show for it stays up until then.

The indicator events reach only the clients connected when they're sent, and aren't stored, so a user who opens the channel halfway through a reply sees no indicator and no stop button until the next event arrives. Use them for the indicator only, and rely on the message's `text` and `generating`, which arrive with every update, for the reply itself.

### Stop the reply

When the user stops the reply, the app calls `channel.stopAIResponse()`, which sends `ai_indicator.stop`. The default composer's stop button does it, and [Stop the reply](https://getstream.io/chat/docs/sdk/react/v14/ai-components/composer/#stop-the-reply) shows a button of your own.

The event carries only the channel's `cid`, not a message id, so on the backend treat it as "stop whatever reply is in flight in this channel". Abort the model's stream, then finish the way a normal reply finishes: store the text written so far with a regular partial update and `generating: false`, then send `ai_indicator.clear`. Without that final update, the message stays as it was last stored.

### Stream with ephemeral updates

A regular partial update writes the message to the database on every call. A long reply flushed every few tokens turns into hundreds of writes, and with several conversations at once, you can hit the [rate limits](https://getstream.io/chat/docs/node/rate-limits/) for message updates. While the reply streams, update it with `ephemeralUpdateMessage` instead. It takes the same payload and sends the same `message.updated` event, so the reply types itself out the same way, but it stores nothing.

**The last update must not be ephemeral.** When the model finishes, store the complete text with a regular partial update, and `generating: false`. Otherwise anyone who opens the channel later loads the last stored version of the message: the empty placeholder, still generating. The same goes for whatever else ends the reply, such as an error or the user stopping it.

```js label="Node.js"
// While the model streams: send the latest text without storing it.
await client.ephemeralUpdateMessage(
  messageId,
  { set: { text, generating: true } },
  botId,
);

// Once the model is done: store the final text.
await client.partialUpdateMessage(
  messageId,
  { set: { text, generating: false } },
  botId,
);
```

```py label="Python"
# While the model streams: send the latest text without storing it.
client.chat.ephemeral_message_update(
    id=message_id,
    set={"text": text, "generating": True},
    user_id=bot_id,
)

# Once the model is done: store the final text.
client.chat.update_message_partial(
    id=message_id,
    set={"text": text, "generating": False},
    user_id=bot_id,
)
```

Ephemeral updates are server-side only: call them from your backend, not from the browser. In Node.js they ship in `stream-chat` 9.27.0 and later, and every server-side SDK has them: the [AI message streaming](https://getstream.io/chat/docs/node/ai-message-streaming/#ephemeral-partial-update) guide shows each one. Even ephemeral updates reach every device watching the channel, so don't send one per token: flush every 20 or so chunks, or at most every 50 to 100 ms, which keeps the typing smooth.

---

For the most recent version of this documentation, visit [https://getstream.io/chat/docs/sdk/react/v14/ai-components/conversation-state/](https://getstream.io/chat/docs/sdk/react/v14/ai-components/conversation-state/).