Streams and blocks
How a reply reaches the frontend
An agent's reply can reach your frontend in two ways. In stream mode, the reply appears while the agent is still writing it, piece by piece. In block mode, it arrives once, complete. Streaming suits a chat window, where a reply that starts at once feels faster. Block mode suits email, notifications, and any system that needs the whole text before it can act.
- The difference between stream mode and block mode, and when each fits
- What a streamed message event looks like as it grows
- How to read the event feed over long-polling or server-sent events
- How status events let your interface show progress
A few terms carry this page.
In Parlant, a session is one conversation between a customer and an agent. Parlant stores it as an ordered log of events: customer messages, agent messages, tool calls, and status updates. Your frontend reads that log and renders it.
Each event carries an offset (its position in the log), so your reader can resume from wherever it left off.
A turn is Parlant's handling of one customer message, from the moment it receives it to the moment it posts the reply.
Your agent delivers its replies in one of two ways. In stream mode, the default, the message event appears as soon as generation starts and fills in piece by piece. In block mode, the event appears once, complete, after generation finishes.
This choice is the agent's output mode. You set it once per agent with output_mode:
import parlant.sdk as p
agent = await server.create_agent(
id="support",
name="Support Agent",
instructions="Help customers with their orders.",
output_mode=p.OutputMode.BLOCK, # the default is p.OutputMode.STREAM
)
Streaming is right for chat. A reply that starts appearing at once feels faster than one that lands whole a few seconds later.
Block mode is right when the consumer needs the complete text before it can act. Think of a channel that sends one notification per message, a pipeline that post-processes replies, or a test harness. None of them can do anything useful with half a sentence.
How a streamed message looks
Once the mode is chosen, the next question is what your frontend actually receives.
Every event has a kind that says what it is and a source that says who produced it. Your agent's reply is an event with kind: "message" and source: "ai_agent", and its text lives under data.
A streaming message event carries chunks next to message. Chunks are text pieces in order, and a final null marks completion:
{
"kind": "message",
"source": "ai_agent",
"data": {
"message": "Hello there",
"chunks": ["Hello", " there", null]
}
}
message is always the text accumulated so far. This means a client that only reads message still works. It just re-renders the growing text.
A client that appends chunks instead should track how many it has already rendered and append only the new ones, because each snapshot includes the earlier chunks too. For example, if you've already rendered "Hello" and the next snapshot arrives as ["Hello", " there", null], you'd append only " there" and then mark the message complete.
Note that a block-mode message has no chunks field at all.
Reading a stream
With the shape of a streamed message clear, here is how your frontend receives it.
The event feed gives your frontend one place to read a session's events from. It's the HTTP endpoint GET /sessions/{id}/events. By default it long-polls: it waits up to wait_for_data seconds for new events past min_offset, and then returns them as a batch. With sse=true, it stays open as a server-sent event (SSE) stream instead.
The feed carries streaming events like any other. Open it with sse=true and each update to the message arrives as a new snapshot: the same event id with a longer chunks list. Your coding agent can start from this:
const source = new EventSource(
`${PARLANT_URL}/sessions/${sessionId}/events?min_offset=${offset}&sse=true&wait_for_data=60`,
);
source.onmessage = (message) => {
const event = JSON.parse(message.data);
render(event);
};
Following a single message
To follow one message on its own, read the event by id with the same flag: GET /sessions/{id}/events/{event_id}?sse=true streams updates until the event completes. For a non-streaming event, Parlant sends the event once and closes.
Treating a stream as a block
Sometimes you don't want the pieces at all. A backend may want the whole text and not care how it was produced. For that, ask for the finished event: wait_for_completion=true on the single-event read holds the response until the final null chunk, up to wait_for_data seconds.
That's the simplest path for a backend that just wants the whole text.
When a stream is held back
Sometimes a stream-mode agent's reply arrives whole anyway. Two features buffer a streamed reply before it goes out, and both are deliberate. Knowing about them saves you from debugging a stream that only looks stuck.
Message review
When review is active for a turn, Parlant reviews the message whole before delivery, because a rejected message must never be half-shown. On those turns, a stream-mode agent behaves like a block-mode one.
Message reviews explains when review runs.
Preambles
A preamble is a short line the model may say before a slow tool call, something like "Let me check that for you." Your agent's preamble_config setting decides whether Parlant allows them.
With preamble_config=p.PreambleConfiguration.discourage(), Parlant suppresses text that immediately precedes a tool call. So in stream mode, it briefly holds possible preamble text until it knows whether a tool call follows. If none does, the text is an ordinary reply and streams normally.
With encourage(), Parlant asks the model for one concise update before the first tool call, trimmed to a sentence and rate-limited to one per interval (15 seconds by default).
Note that if you leave preamble_config unset, Parlant passes whatever the model says straight through.
Status while the agent works
The feed also tells you what your agent is doing before the reply exists. This lets your interface show progress while the customer waits.
Every event a turn produces carries the same trace_id, an id Parlant assigns to the turn. It lets you group a customer message, the agent's work on it, and its reply together.
Status events are events with kind: "status" that report the turn's progress. Between a customer message and the reply, the feed carries them on the same trace_id: acknowledged, then processing, then typing once writing starts, and finally ready when the turn is done. Render them as your "thinking" indicator, and clear it on ready, error, or cancelled.
A tool with a narration (a short progress line you give the tool) adds its own processing status right after the generic "Running tools" one, so the customer reads "Looking up your order…" instead.
Customizing execution covers it.