Streaming answers that show their work
Why a chat that streams its reasoning before its answer feels faster, earns more trust, and is easier to correct. Plus how the stream is wired end to end.
AgentJam Team4 min read

The first version of most chat products waits for the whole answer, then prints it. It feels slow even when it is not, because nothing happens for eight seconds and then everything happens at once. Streaming fixes the waiting. Streaming the reasoning first fixes something more important: it tells you what the model is about to do while there is still time to stop it.
This post walks through how AgentJam streams a reply, why the reasoning arrives before the answer, and the small interface decisions that make the stream readable rather than noisy.
Two streams, one message
A reply is made of two streams that share a socket connection.
- Reasoning. Short planning notes from the model: what it understood, what it will look up, how it intends to structure the answer.
- Output. The answer itself, as Markdown, with citations attached as they are resolved.
They are sent as separate event types so the client can render them differently. Reasoning lands in a collapsible block above the answer. The answer lands in the message body. When the reply finishes, the reasoning block clears and the answer stays.
socket.on("chatReasoningStart", ({ id, chatId }) => {
addElement(chatId, { type: "reasoning", id, content: "", isExpanded: true });
});
socket.on("chatReasoningDelta", ({ id, chatId, delta }) => {
appendReasoning(chatId, id, delta);
});
socket.on("chatOutputDelta", ({ id, chatId, text, annotations }) => {
upsertAssistantMessage(chatId, id, { content: text, annotations });
});The id on each event matters more than it looks. It lets the client match deltas to the right block even when two chats are generating at once, and it is what makes the final persisted message line up with what the user watched being written.
Why reasoning goes first
Putting the plan before the answer does three things.
It shortens the perceived wait. The first reasoning token usually arrives well before the first answer token, because the model has not started retrieving or reading anything yet. Something is on screen within a few hundred milliseconds.
It makes the answer checkable. If the plan says “I will compare the three most cited papers,” and the answer only covers two, you notice. If the plan says “searching the web,” you know the numbers are not from memory.
It makes stopping cheap. The stop button is live from the first token. If the plan is heading the wrong way, you press stop, rewrite the question, and lose two seconds instead of thirty.
Keeping the stream readable
A raw token stream is hard to read. Words appear mid-sentence, Markdown syntax flickers as it resolves, and the page jumps as content grows. A few rules keep it calm.
Render Markdown per line
The answer is split on newlines and each line is rendered as its own Markdown fragment. A half-finished line only re-renders itself, not the whole message. Headings, lists, and inline code resolve the moment their line completes, which reads as the document assembling itself rather than a cursor typing.
Pin the bottom without fighting the reader
The scroll container pins to the newest content while the user is at the bottom. The moment they scroll up to re-read something, the pin releases and a small “scroll to latest” button appears instead. The stream never yanks the page out from under a reader.
Show state, not spinners
While the model is working there is a single shimmering “Thinking…” line under the last message. It replaces the spinner most chats use, it never overlaps content, and it disappears the moment the first output token arrives.
What gets saved
The server saves the exchange before it announces the end of the stream, under the same id the streamed message used. That final event carries the full record: the answer text, any tool calls, and the citation annotations. The client swaps the streamed message for the canonical one by that id, so nothing visibly changes, and a client that reconnects or reloads mid-reply converges on the same message instead of a duplicate. Reasoning is not persisted; it was useful while the answer was being written, and the answer is what the conversation keeps.
Try it
Open a new chat and ask something that needs a plan, like a comparison or a multi-step calculation. Watch the reasoning block, then collapse it and read the answer. If the plan is wrong, stop it. That loop is the whole point.