> ## Documentation Index
> Fetch the complete documentation index at: https://docs.flow.engineer/llms.txt
> Use this file to discover all available pages before exploring further.

# Stream replies

> Turn a model stream into complete chat bubbles while showing typing.

Messaging apps have no streaming text box, so a streamed model answer is best sent as a few natural **bubbles**, each sent as soon as it is complete, with the typing indicator on in between.

```ts TypeScript theme={null}
import Anthropic from "@anthropic-ai/sdk";
import { FlowMessaging, contentText } from "@flow-engineer/messaging";

const flow = new FlowMessaging();
const anthropic = new Anthropic();

for await (const event of flow.events.stream({ types: ["message.received"] })) {
  const stream = anthropic.messages.stream({
    model: "claude-sonnet-4-5",
    max_tokens: 1024,
    messages: [{ role: "user", content: contentText(event.data.message.content) }],
  });
  const sent = await event.conversation.reply(stream); // typing on, bubbles out, typing off
  console.log(`sent ${sent.length} bubbles`);
}
```

## What `reply()` does

`conversation.reply(input)` accepts a string, a kind object (`basic`, `buttons`, ...), a list of them, or a stream. With a stream it:

1. Shows the typing indicator, and keeps showing it (channels clear it after a few seconds).
2. Reads text out of the stream as it arrives.
3. Cuts a bubble at each natural break (below) and sends it while the model is still writing. Bubbles go out in order.
4. Stops refreshing typing when the stream ends, even if it failed. Typing has no "off": it clears itself when the next message goes or after a few seconds.

It reads these streams as they are, with no adapter:

| Source | Pass |
| - | - |
| OpenAI Chat Completions | `openai.chat.completions.create({ ..., stream: true })` |
| OpenAI Responses | `openai.responses.create({ ..., stream: true })` |
| Anthropic Messages | `anthropic.messages.stream(...)` or `create({ ..., stream: true })` |
| Vercel AI SDK | the `streamText(...)` result |
| OpenAI Agents SDK | the streamed `run(agent, input, { stream: true })` result |
| Claude Agent SDK | the `query(...)` iterator |
| LangChain | `model.stream(...)` or a runnable's `.stream(...)` |
| Mastra | `agent.stream(...)` |
| Anything else | any `AsyncIterable<string>` or `ReadableStream` |

Each bubble is a `basic` message, sent exactly as written: no markdown is rendered or stripped. Ask the model for plain text, or style text yourself in a kind object with a channel block (`telegram.parse_mode`, `imessage.styles`).

```ts theme={null}
await event.conversation.reply(stream, {
  idempotencyKey: event.id, // bubbles get event.id:0, event.id:1, ... so retrying the reply is safe
  split: true,              // false: one message, cut only at the channel's length limit
  onBubble: (message, i) => console.log(i, message.id),
});
```

## The bubble rule (for any language)

Without the SDK, apply this rule to the model's text and send each bubble as a basic message (`POST /v0/conversations/{conversation_id}/basic` with `{"text": ...}`), one after another, showing typing (`POST .../typing` with `{}`) before you start and every 4 seconds until you finish.

1. A bubble ends at a paragraph break (a blank line), except after a lead-in line that ends with `:` and between items of one list, unless the bubble is already past the channel's soft length.
2. Past the soft length, a bubble ends at the next sentence end (`. `, `! `, `? ` or `…` followed by a space).
3. Never cut inside a fenced code block, unless the bubble would pass the channel's hard limit; then cut at the last line break or space before it.

| Channel | Soft length | Hard limit |
| - | - | - |
| Telegram | 900 characters | 4096 |
| WhatsApp | 700 characters | 4096 |
| iMessage | 400 characters | 9999 |
| SMS | 300 characters | 670 (10 segments, even with an emoji) |

Use the event's ID plus the bubble's index as each send's `Idempotency-Key` (`evt_...:0`, `evt_...:1`), so retrying a failed reply never sends a bubble twice.

## Showing work while the agent thinks

For tool calls, retrieval or anything slow before the first word:

```ts theme={null}
const answer = await event.conversation.responding(() => agent.run(question)); // typing stays shown
await event.conversation.reply(answer);
```

Messages in one conversation go out one at a time, in the order they were accepted, across every endpoint, webhook replies and the stream, so bubbles never arrive out of order even when you send them quickly.

## Related

* [Message kinds](https://docs.flow.engineer/unified-messaging-api/concepts/content-types.md): typing, read receipts and text styles per channel.
* [Agent frameworks](https://docs.flow.engineer/unified-messaging-api/frameworks/vercel-ai-sdk.md): full examples per framework.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.