# Streaming 101: when is a response actually finished?

You ask an assistant for a draft. Words begin appearing, then the answer stops
halfway through a sentence. Did it finish? Did the connection fail? Can you use
what appeared? Those are separate questions, and a forward-deployed engineer
needs an interface that tells the difference.

This original 40-minute lesson starts with the mechanism, then follows six
actual local HTTP responses. It uses fictional text and no model, credentials,
packages or cloud account. You need Node.js 24 and permission to listen on a
temporary loopback port on your own computer. Nothing is exposed to your network.

## 1. Name the pieces before following the stream

**Streaming** is a way to deliver a response incrementally while work proceeds,
instead of waiting to deliver the whole body at once. It is a delivery behavior,
not a language, model or agent framework. It can make progress visible sooner;
it does not by itself make generation faster or the answer correct.

Think of a download progress display. Seeing bytes arrive is different from
having a complete, usable file. The analogy has a limit: a generated response
usually does not have a known final size, and its content still needs review.

| Piece | Plain meaning | What it does not establish |
| --- | --- | --- |
| Byte chunk | A portion of the response body delivered to the reader | One complete event, character or model token |
| UTF-8 decoder | Code that reconstructs text from bytes, keeping unfinished characters between reads | A complete application message |
| Server-sent events (SSE) | A text format for sending events from a server over HTTP | The event names or success rules of a particular model API |
| Delta | An addition or change to the result being assembled | A complete answer or executable tool arguments |
| Completion event | An application-defined signal that a message has ended | A factual answer, approved action or acceptable stop reason |

In the supplied lab, an event has an `event:` line, a `data:` line containing
JSON and a blank line that ends the event. The reader must wait for that
boundary before parsing the JSON. The service invents `start`, `text_delta`
and `done` for this teaching exercise. They are not a provider's API contract.

## 2. Follow a draft from bytes to a reviewable result

Our fictional service writes three additions: `Café `, `pump: ` and `check seal.`
The client accumulates them into `Café pump: check seal.` while marking the
preview as partial. The accented character is intentional: one character can
occupy more than one byte. The same decoder keeps its state across reads.

Follow four decisions in order:

1. Confirm the HTTP status and content type before interpreting the body.
2. Decode bytes, retain unfinished text, then parse complete event records.
3. Update the preview as deltas arrive while keeping it visibly provisional.
4. Require the documented completion and acceptable stop reason, then validate
   the assembled result before the application's next action.

HTTP 200 only describes the response status. The body can subsequently break,
end early or contain invalid events. Our lab waits for normal transport end,
valid records and `done` with reason `normal` before marking a result
`ready-for-content-review`. That deliberately narrow result is not permission
to execute a tool or publish the text.

## 3. Predict and run six local responses

Download [fde-streaming-lab.mjs](/templates/fde-streaming-lab.mjs) into a new folder.
Use the same Node.js 24 command throughout:

```shell
node --version
node fde-streaming-lab.mjs
```

The file starts a server bound to `127.0.0.1` on an available port, makes six
requests and closes the listener. It writes no files and reads no credentials.
The server's short pauses make intermediate states observable; they are not
measurements of inference performance. It does not retry failed requests.

Before running it, predict the completion decision for each case:

| Case | What the server or reader changes | Expected observation |
| --- | --- | --- |
| complete | Three deltas followed by normal completion | Full text, completion observed, ready for content review |
| fragmented | Same response; client feeds its decoder one byte at a time | Same text and events despite many more parser slices |
| missing-done | HTTP body ends normally after two deltas, without completion | Partial text, transport ended, completion absent; withheld |
| connection-lost | Connection is destroyed after two deltas | Partial text, body-read-interrupted; withheld |
| limited | Full-looking text followed by a length-limit reason | Completion observed, but withheld by this app's contract |
| malformed | Final event contains invalid JSON | Invalid record; withheld |

Compare `httpStatus`, `transportEnded`, `events`, `completionObserved`, `reason`
and `decision`. All six begin with 200. In `previews`, find text that appeared
before any completion event. Do not infer a token count from text length,
event count or `receivedChunkBytes`: the lab runs no model or tokenizer.

`receivedChunkBytes` records actual body reads; their grouping can vary by
runtime and transport. `parserSlices` counts the pieces passed into our decoder.
The fragmented case deliberately re-slices received bytes on the client. It
does not claim that the network delivered one-byte packets. The controlled
experiment isolates the parser's ability to preserve boundaries.

## 4. Change one assumption and explain the consequence

Keep the original file. In a first copy, change `SLICE_BYTES = 1` to
`SLICE_BYTES = 7` and rerun. The fragmented case should have fewer parser calls
but the same five events and complete text. A reader that relies on each read
being a complete JSON object cannot meet that property.

Starting from the original, make a second copy and change `REQUIRE_DONE = true`
to `REQUIRE_DONE = false`. This deliberately weakens the example's acceptance
rule. The missing-done case now promotes `Café pump: ` even though no completion
arrived. The lost connection, invalid record and explicit length limit should
still be withheld. Explain why a check that only asserts a nonempty response
would miss this defect. Do not carry this broken rule into an application.

This is a small protocol lesson, not a general-purpose SSE implementation. The
reader supports only the two-field, LF-delimited records emitted by this server.
A complete SSE parser also handles other line endings, comments, multiple data
lines and additional fields. Use the maintained SDK or parser appropriate to
your production service, then test its behavior at your application's boundary.

## 5. Connect this to Claude and an agent workflow

Claude's documented stream has message and content-block events, text or tool
input deltas, a stop reason and final `message_stop`. These names differ from
our teaching protocol. Follow the provider's current SDK and event contract.
Tool input can arrive as JSON fragments: a fragment is not yet validated tool
arguments. Assemble the intended input, validate its schema and authorize the
operation in trusted application code before executing it.

If the stream fails, retain an explicitly incomplete preview and the request's
safe diagnostic metadata. A missing final event does not tell you whether a
remote operation continued, how much it cost or whether an external effect
occurred. Reconnection and safe retry are separate designs. This lab performs
one attempt per case and has no continuation, replay or effect reconciliation.

In the UI, separate “receiving,” “ready for review,” “stopped” and “interrupted.”
Do not let partial markup run as HTML or partial tool input trigger an action.
When a user cancels, stop consuming and request supported cancellation; verify
what the provider actually promises about remote work and billing. The local
connection-lost case does not test provider cancellation.

For a real deployment, inspect buffering at the application, proxy and gateway.
Measure first useful output and completed useful output separately. A responsive
animation cannot establish either result. Do not add timing claims based on this
fixture's deliberate pauses.

## 6. Hand over a response contract

Produce a small integration card with:

- The actual endpoint, content type, event schema and supported client version.
- Which preview states are visible and what qualifies as completed input.
- The acceptable stop reasons and content checks before downstream work.
- What happens after malformed data, a disconnect, timeout or user cancellation.
- How request identity, retry policy and unknown usage are recorded safely.

Keep your six predictions, observed report and two independent changes. Ask a
receiver why both a clean HTTP end and a visible sentence can still be
insufficient. Then give them a tool-input fragment and ask what must happen
before an agent may use it. A good explanation names assembly, validation,
authorization and the unresolved remote outcome without inventing certainty.

Continue in [the guided streaming lesson](/app/academy?course=agent-engineering&lesson=streaming-first-response&view=focus)
with existing training access, then build the first bounded LangChain agent.

Primary references checked 22 October 2026:

- [MDN: using server-sent events](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events)
- [Anthropic: streaming messages](https://platform.claude.com/docs/en/build-with-claude/streaming)
- [Node.js 24: HTTP](https://nodejs.org/docs/latest-v24.x/api/http.html)
