Stream Protocol Contract
The canonical StreamPart contract and the provider SSE invariants locked by tests.
문서 본문은 영어입니다. 탐색, 검색, UI는 선택한 언어를 따릅니다.
Provider bytes are never exposed directly. Each adapter parses its wire stream and emits the same additive StreamPart union. Consumers can therefore switch providers without changing text, reasoning, tool-call, source, usage, or finish handling.
The SSE parser is regression-tested for:
- UTF-8 code points split across arbitrary byte boundaries;
- LF, CRLF, and bare CR line endings, including a CRLF pair split across chunks;
- UTF-8 BOMs, comments/keep-alives, named events, and multi-line
data:fields; - a final event without a trailing blank line;
- cancellation of the underlying reader when a consumer stops early.
Every successful provider stream must produce exactly one finish part. Provider usage shapes are normalized into Usage; provider finish values are normalized into the locked FinishReason union. A provider error event produces one typed error part and rejects usage and finishReason with the same error object.
Compatibility rule
StreamPart is additive. Applications should handle the variants they need and retain a default branch so a new non-breaking part does not break an older consumer.
for await (const part of result.fullStream) {
switch (part.type) {
case 'text-delta':
process.stdout.write(part.text);
break;
case 'error':
throw part.error;
default:
break;
}
}Additive parts since 1.8
type | When | Notes |
|---|---|---|
verify | verifyStep evaluated on a natural completion | stepIndex, attempt, ok, willRetry, optional feedback. Since 1.9 it is also serialized onto the UI wire (v2 only), journaled to the StreamStateStore and replayed on resume — before that it was dropped by toDeuzStreamResponse, so a verified run looked identical to an unverified one client-side. |
plan-update | Emitted via emitPlanUpdate from a tool | Live TaskList snapshot for a to-do panel. Also a UI v2 part. |
activity | Emitted via emitActivity from a tool | "Computer" feed line (message, optional level / data / agentPath). Also a UI v2 part. |
Additive parts and fields in 1.9
type / field | When | Notes |
|---|---|---|
warning | A lossy mapping was applied (a stripped sampling param, an unknown-slug capability fallback, a tool or document the wire could not carry) | { warning: CallWarning }. Emitted as it is discovered — always before the output of the step that produced it, because every warning site runs during capability resolution and request building. In a loop that means a later step can still emit one, after earlier text. Also serialized onto the UI wire (v2 only, message re-redacted), journaled to the StreamStateStore and replayed on resume; folds into AssistantTurnState.warnings. The same set resolves in bulk on StreamChatResult.warnings. CallWarning['type'] is an open union — keep a default. |
false-finish | The doneWhen guard rejected a natural completion | { stepIndex, attempt, willRetry }. One part per rejection, always before the terminal finish; willRetry: false is the rejection that spent the falseFinishGuard budget, after which finish carries providerMetadata.deuz.stoppedBy = 'false-finish'. Streaming loop only. On the UI wire (v2 only), journaled and replayed, and folded into AssistantTurnState.falseFinishes — the same treatment verify got. |
tool-state.denied / .deniedReason | An approval refused a gated call | Optional fields qualifying a terminal state: 'error' whose cause was a verdict, not a thrown tool. Set by the streaming loop at both terminal sites; deniedReason is the denier's own words (absent for a server-mode approveToolCall, whose verdict is a bare boolean). ToolRunState deliberately gains no 7th member, so exhaustive switches keep compiling. |
data.id | writeData(name, payload, { id }) | Reconciliation key. The wire stays strictly append-only; last-write-wins is the client's job. |
Wire v1 output is unchanged in every byte. The tool-state fields and the data-part id ride carriers a v1 client already drops wholesale, and the two brand-new part types are v2-only — including when they sit inside a sub-agent frame, which is filtered recursively.
Run the focused protocol suite with npm run test:protocol.