A Closed AI Stream Is Not a Completed Answer

Decide when streamed AI output may become a final answer by separating byte decoding, event boundaries, stop reasons and application acceptance.

A Closed AI Stream Is Not a Completed Answer
Written by
ZharfAI Team
Published
October 8, 2026
Reading time
12 minutes

An assistant writes, “Approve the order, but only if…” and falls silent. The spinner disappears. The request received HTTP 200. The conversation history saves the fragment as an ordinary answer. The interface has removed every visible sign that the missing condition might matter.

This is a constructed design example, not a reported product incident. It exposes a decision every streamed-answer implementation must make: when should received text become a final answer, remain an incomplete draft, or trigger a separate attempt? Neither fluent wording nor a large volume of text provides the necessary evidence.

There are four different completion boundaries

Receiving bytes, assembling an event, finishing a model turn and accepting an answer are separate operations. A connection can end without the model's terminal event. A terminal event can arrive after generation hits its output limit. A naturally finished answer can still contain an invalid amount or an unauthorized instruction.

A single loading flag cannot represent these distinctions. Turning it off says the current wait has ended, not that the result succeeded. Give the attempt an explicit outcome so a transport failure, user cancellation and accepted answer do not become indistinguishable in history.

The protocol facts below come from official references. The storage design, test cases and acceptance policy are ZharfAI engineering analysis, not universal provider requirements or results from customer deployments. The objective is a testable boundary between “we displayed something” and “we accepted something.”

That boundary also changes performance measurement. Our inference latency engineering guide separates the stages of a request. Here, time to first visible text and time to accepted answer need separate clocks; fast initial output can coexist with slow validation or a response that never becomes usable.

Preserve characters before interpreting events

A network chunk is not necessarily a character. In a custom consumer, the bytes for one Persian letter can straddle two reads. Decoding each chunk independently is therefore a poor reconstruction strategy, even when tests containing only simple ASCII text appear healthy.

The WHATWG Encoding Standard defines fragmented decoding through repeated calls to one TextDecoder with stream: true, followed by a final flush. Its fatal option throws on decoding errors. Choosing that stricter behavior is an application policy, not a description of every library's default.

This small fixture tries every two-part byte split of a Persian word. It tests decoding only; it is not a model client or event parser:

const expected = "ژرف";
const bytes = new TextEncoder().encode(expected);

for (let cut = 0; cut <= bytes.length; cut++) {
  const decoder = new TextDecoder("utf-8", { fatal: true });
  const actual =
    decoder.decode(bytes.slice(0, cut), { stream: true }) +
    decoder.decode(bytes.slice(cut), { stream: true }) +
    decoder.decode();
  if (actual !== expected) throw new Error(`Bad split: ${cut}`);
}

If an official SDK already provides decoded events, do not rebuild this layer above it. Instead, test the installed version with fragmented and invalid input. Establish which component detects corruption and which component prevents a final save. The responsibility matters more than whether your own code contains a decoder.

Keep the fixture small enough to understand. A passing character test proves nothing about the missing terminal event in the opening example; it closes only the first boundary. Conversely, a correct event state machine cannot repair text that an earlier decoder silently corrupted.

End of input does not finish a half-received event

For Server-Sent Events, decoded text still needs framing. The WHATWG HTML specification uses a blank line to dispatch an event; end of input does not dispatch an unfinished one. The format also supports multiple line-ending forms and multiple data lines. A simplistic double-newline string split is not the entire contract.

Build a fixture whose final event is missing its terminating blank line. If your consumer “repairs” it when the connection closes and then finalizes the answer, it has invented completion evidence. Readable content is not permission to supply missing protocol structure.

Prefer a tested parser for a custom SSE consumer. Put explicit limits around event size, aggregate response size, waiting time and buffered memory. Crossing a limit should produce a distinct failure, not silently discard the remainder and report success. A limit protects the consumer only if its effect remains visible to the acceptance logic.

This discussion applies to SSE framing, not every API that uses the word streaming. Keep transport parsing inside the adapter for the actual API. Your application's normalized event names should not tempt you to assume identical wire formats underneath them.

A content block ending is not the message ending

Anthropic's streaming guide distinguishes content-block events from a final message_stop. It also documents pings and in-stream errors. A block stop or a keepalive therefore cannot substitute for message completion.

Amazon Bedrock's ConverseStream reference exposes separate members for messageStop, metadata and stream exceptions, with a stop reason in the message-stop event. An adapter must interpret that API's response contract rather than search for a vaguely similar word such as “stop.”

Have each adapter produce an explicit internal outcome: the provider's stopping reason, completed blocks, any detected error, and the evidence used to establish message completion. Keep the API and SDK versions with the record. The caller should consume this outcome, not infer success from socket closure.

Follow the documented SDK or protocol lifecycle, including any required trailing processing, before resolving that outcome. Do not swallow an iterator exception in a cleanup handler that unconditionally calls saveFinal. Equally, do not impose an invented rule that every provider requires a connection close after its documented terminal signal. The adapter's job is to encode the actual contract.

Ask why generation stopped

Claude's stop-reason reference distinguishes a natural turn ending, end_turn, from an output limit, max_tokens, and a tool request, tool_use. These describe different generation outcomes. Even a natural turn ending is not a business-correctness guarantee.

The following is our proposed policy for an answer-oriented assistant, not an official cross-provider mapping:

Received evidenceApplication interpretationTreatment of the text
Text fragments without message-completion evidenceIncomplete attempt or unknown outcomeRetain as an incomplete draft, not a final answer
Valid terminal sequence with an output-limit reasonGeneration was limitedExpose the limit; decide separately about another attempt
Valid terminal sequence requesting a toolControl returns to the workflowNot yet the user's final answer
Natural ending with empty or invalid contentGeneration ended, acceptance failedRecord the rejection without reporting success
Valid ending and accepted contentResult is eligible for finalizationCommit with concurrency checks

Give refusals their own treatment as well. A product may regard a refusal as a valid conversation ending without counting it as a useful answer to the request. Write that measurement definition before reading the success chart. Otherwise, a change in outcome classification can masquerade as a quality improvement.

Separate the visible draft from the final record

Create an attempt identifier and a provisional draft before consuming output. Incoming fragments update that draft. Only completion evidence and the required content checks make it eligible for promotion. Show “receiving” while appropriate and “incomplete response” after failure; do not hide those distinctions exclusively in developer logs.

Saving a draft is not inherently wrong. It may be necessary for recovering a session. The dangerous step is allowing an unlabeled draft into a conversation summarizer, downloadable answer or enterprise search index. Each downstream consumer needs to declare whether it accepts provisional material or only finalized answers.

Make promotion conditional: the attempt is still active, the draft revision has not changed, cancellation has not invalidated it, and the acceptance result belongs to that exact revision. A delayed callback from an obsolete attempt must not overwrite the newer answer. Repeated finalization requests must not produce duplicate final records.

Retain only what operations and policy justify. Attempt IDs, outcomes, stop reasons and milestone timestamps may be enough to investigate structural failures. Raw text retention needs its own purpose, access controls and deletion policy. Dumping sensitive answers into logs is not the default solution to a completion bug.

Completion is still not permission to act

Consider a tool request that gradually supplies an item identifier and quantity for an order amendment. Do not execute a business action merely because the first readable field appears. The conservative policy in this guide waits for the relevant message to complete, checks the final contents and then applies execution authorization. An interface may display provisional tool input earlier without giving it authority.

A closing JSON brace does not change this rule. A negative quantity, an identifier belonging to another customer or a value in the wrong currency can all fit inside syntactically valid output. Our structured-output contract guide develops the separation between valid structure, valid meaning and permission to execute.

For display-only work, delaying every word until validation may undermine the reason for streaming. The explicit compromise is to show provisional text while preserving its status and blocking sensitive downstream uses. If a product requires screening before disclosure, raw incremental display may be unsuitable altogether. Validation performed after display cannot take back what the user already saw.

This distinction prevents an attractive but misleading implementation: streaming into the final-answer field and trying to revoke it later if something goes wrong. Draft-first storage makes the normal success path a promotion, while failure leaves the record in its honest original state.

A second attempt is not automatically a continuation

Suppose the first attempt delivers three fragments before interruption. A retry returns a complete answer. Appending that second answer to the first draft creates a text that neither attempt actually generated. Similar openings do not establish a shared continuation, and a fluent combined result does not make the splice valid.

Use a new attempt identifier and make replacement visible to the user. If a service documents recovery of the same attempt, use that specific contract. Generic transport reconnection support does not, by itself, establish resumable model generation.

A “continue” request is also a new attempt with explicit context, not proof that the earlier text was complete. If the earlier workflow caused an external action, reconcile its result before retrying that action. The retry and idempotency guide explains why the user's logical intent and the individual attempts need different identifiers.

Cancellation introduces another race. A local user can cancel just before a queued completion callback runs. The final write must check the current attempt status, rather than rely on the callback's earlier view. Otherwise, a correctly interpreted provider response can still become an incorrectly accepted local result.

Put failures at the boundaries in your test suite

Create a small, authorized synthetic response fixture; these tests need not call a live model. Feed its events to the adapter under different chunk boundaries: everything in one read, one byte per read, and line endings split across reads. An intact fixture should yield the same accepted result in every partition.

Then change the evidence itself. Remove the terminal event, replace natural completion with an output-limit reason, insert a stream error after visible text, and cancel immediately before finalization. A passing test asserts the correct outcome for each case; “no uncaught exception” is much too weak.

Keep three fixtures visibly separate: valid completion with valid content, valid completion with invalid content, and plausible content without valid completion. The third is especially important because a short happy-path response on a local connection does not exercise it. Also repeat finalization and deliver a stale fragment from a superseded attempt. Neither should increase the final-answer count.

Exercise capacity failures too. A slow consumer, full buffer or size cap must not turn into silent text loss. When the system cannot preserve the response, an explicit failed attempt is more trustworthy than an apparently complete but truncated answer. Check that failure status reaches every consumer, not just the foreground interface.

Measure accepted answers, not just quiet connections

Record time to first text and time to accepted answer separately. Count started attempts, protocol-complete messages and accepted answers, with failures divided by cause. Report protocol completion against all started attempts; do not remove requests that failed before producing visible text. Acceptance among completed messages is a different denominator and should remain a separate metric.

After changing a model, SDK or network intermediary, rerun the partition and stopping tests. An error spike is not the only warning. A sudden rise in apparent success after removing a terminal-event check deserves investigation too. Observe changes by adapter version so a contract regression is not misdiagnosed as a model-quality shift.

The acceptance criterion is practical: for every finalized answer, the application should be able to identify that attempt's completion evidence, stop reason and acceptance result. If it cannot, it is presenting a draft with more confidence than the evidence supports. The stream becoming quiet is a fact about delivery; a usable answer is a decision your application must justify.

Source Notes — reviewed October 8, 2026

These are living standards or current technical references. The review date belongs to this article, not to the original publication of every source. The fixtures and acceptance policy are proposed engineering designs, not product guarantees or reported customer outcomes.

#Streaming Responses#Reliability#Software Engineering#Validation#APIs

Related Posts

Run this inside your own team

If you want the agents and automation in this guide working for your engineering team or business processes, start with a small pilot you can measure.