The streaming module handles three distinct concerns:
- SSE parsing (
llm/_sse.py) — parses raw Server-Sent Events HTTP stream lines into JSON payloads - Event mapping (
streaming/events.py) — translates internalAuditEventobjects into externalStreamEventobjects for CLI consumers - Content filtering (
streaming/content_filter.py) — extracts only user-visible text from partial decision JSON blobs - Handler wiring (
streaming/handlers.py) — connects CLI arguments to the appropriate stream pipeline (JSON stream vs. plain progress vs. text-only)
- Guarantees:
iter_sse_data_linesyields only the data payload ofdata: <payload>lines. It stops iteration immediately whendata: [DONE]is encountered. - Guarantees:
consume_sse_json_chunksalways callslines.close()in itsfinallyblock if the iterator has aclosemethod — even if JSON parsing fails on individual lines. - Silently skips: Lines that cannot be parsed as JSON are skipped without raising.
- Encoding: Bytes lines are decoded as UTF-8 with
errors='replace'. Non-decodable bytes produce replacement characters, not exceptions.
- Guarantees:
StreamEvent.to_dict()always returns a dict with at least atypekey equal toStreamEvent.type. - Immutable:
StreamEventis afrozen=Truedataclass.
- Guarantees: Returns
Nonefor anyAuditEvent.event_typethat is not in the handled set:iteration_started,tool_call_started,tool_call_completed,tool_call_failed,run_failed,run_completed,approval_required. - No side effects: Pure function; reads from
event.payloadand returns a newStreamEventorNone.
- Guarantees: Every call produces exactly one newline-terminated JSON line to
out(defaultsys.stdout). Output is flushed after each write. - Guarantees: Output is JSON-serialized with
ensure_ascii=Falseand keys sorted alphabetically.
- Guarantees: Only emits text that was not already emitted. The
_emittedcounter tracks characters forwarded to_sink. Eachfeed()call emits at most the delta between newly decoded characters and previously emitted characters. - Precondition for emission: The streamer does not call
_sinkuntil it has seen both a"type": "final"marker and a"content": "key marker in the accumulated buffer. - Monotonic forwarding:
_emittedonly ever increases; content is never re-sent. - Partial JSON handling: The internal JSON string decoder (
_decode_json_string_prefix) handles escape sequences (\n,\t,\r,\",\\) and stops at an unescaped closing". It tolerates truncated escape sequences at end of buffer.
- Guarantees: Always returns a
RunStreamHandlersinstance; never raises. - Routing logic:
json_stream=True, stream=True, stream_text_only=True→ JSON events on stdout +DecisionContentStreamerfedtext_deltaeventsjson_stream=True, stream=True, stream_text_only=False→ JSON events + raw chunk stdoutjson_stream=True, stream=False→ JSON events only (no chunk handler)json_stream=False, stream=True, stream_text_only=True→ plain text filtered throughDecisionContentStreamerjson_stream=False, stream=True, stream_text_only=False→ raw text directly to stdoutjson_stream=False, stream=False→ progress only (if enabled), no text chunks
DecisionContentStreamer._emittednever decreases.DecisionContentStreamer._bufferonly grows until the firstfeed()that locates_content_start; after that point, the buffer still grows (new feed appends), but only characters after_content_startare re-decoded.emit_stream_eventalways writes valid JSON per call.iter_sse_data_linesterminates when[DONE]is seen; callers must not rely on the generator continuing after that.audit_event_to_stream_eventis stable: the sameAuditEventalways maps to the sameStreamEvent(orNone).
[INITIAL]
│ feed(chunk): _FINAL_TYPE_RE not found in buffer
│ (keep buffering)
▼
[WAITING_FOR_FINAL_TYPE]
│ _FINAL_TYPE_RE found in buffer
│ + _CONTENT_KEY_RE found → _content_start set
▼
[STREAMING]
│ feed(chunk): decode buffer from _content_start
│ emit delta to _sink if decoded len > _emitted
└──► (loops back to STREAMING for each subsequent feed)
args.json_stream ?
├── YES:
│ progress enabled? → audit.add_sink(audit_sink_for_json_stream(emit))
│ args.stream?
│ ├── YES + stream_text_only → on_chunk = DecisionContentStreamer(text_sink).feed
│ └── YES + raw → on_chunk = text_sink (direct)
└── NO:
progress enabled? → audit.add_sink(audit_sink_for_progress())
args.stream?
├── YES + stream_text_only → on_chunk = DecisionContentStreamer(text_stdout).feed
└── YES + raw → on_chunk = text_stdout