brainstorming
brainstorming
You MUST use this before any creative work - creating features, building components, adding functionality, modifying behavior, or planning anything new, in software or out of it (a talk, a business, a renovation).
agentsop-streaming-outputEnhancement-overlay decision protocol for STREAMING the output of long-running LLM / agent runs from the *backend*, not just wiring a typing animation in the UI. Activates when a coder agent must stream final tokens to a chat client, surface intermediate agent steps (which tool, which node, partial reasoning), emit custom tool-progress events, choose a tran…
Independent GitHub snapshot · Automated classification · Original documentation governs
Source posture: every non-trivial claim is cited inline. Short tags like
[lg/stream],[lc/astream-events],[oai/stream],[anthropic/stream],[mdn/sse]resolve againstreferences/R1-source-evidence.md.This is an ENHANCE overlay: it sits on top of
[[agentsop-langgraph]](which names the four stream modes but treats streaming as one of ten operations) and[[langchain]]. Read those for the orchestration; read this for the streaming SOP. Cross-link:[[agentsop-langgraph]]OP-8.
Activate when any of these fire:
graph.stream(...) / astream_events / OpenAI stream=True /
Anthropic client.messages.stream and need to know which mode and what to
forward to the client.Do not activate for: a single fast (<1s) completion, a batch/offline job with no waiting human, or a pure front-end animation question (that's CSS, not a backend SOP). Streaming a 300ms call adds protocol overhead for zero UX gain — see 反模式.
Stream what the user needs to see, not everything the engine emits. A backend stream is a curated projection of the run's internal event firehose onto exactly three audiences:
Chat audience → final tokens. A human reading prose wants character-by-
character output of the final assistant message. In LangGraph this is
stream_mode="messages" (LLM tokens + metadata); in raw SDKs it's
stream=True / .messages.stream [lg/stream] [oai/stream]
[anthropic/stream]. They do not want to see tool JSON or scratch nodes.
Agent audience → intermediate updates. A developer (or a power-user UI)
watching an agent work wants "entered node planner", "calling tool
search", "got 5 results" — the state diffs between steps. LangGraph:
stream_mode="updates" (per-node diffs) [lg/stream]. LangChain LCEL:
astream_events (a typed event stream: on_chat_model_stream,
on_tool_start, on_tool_end) [lc/astream-events].
Progress audience → custom events. Work happening inside one tool/node
(a loop, a long embed, a download) is invisible to the framework's automatic
events. You must emit progress yourself: LangGraph stream_mode="custom"
via get_stream_writer() [lg/stream]; LCEL via custom callback / dispatched
events [lc/astream-events].
The load-bearing insight from the LangGraph docs: stream modes are composable —
pass a list (stream_mode=["messages","updates","custom"]) and demultiplex on
the client by the tuple tag [lg/stream]. So the real design question is never
"can I stream" but "which projection(s) does this surface need, and how do I tag
them on one wire?"
Second axiom: a stream is a contract with a client that can vanish. Networks drop, users close tabs, browsers cap connections. The backend must decide, up front, whether a disconnect should cancel the run (stop burning tokens) or detach and let it finish (so a reconnect can replay). That decision is part of the design, not an afterthought — see 困境 Case 2.
Walk top-down. Each step has a gate.
Gate: is a human waiting on a run that takes >~1–2s? If no (batch job, sub-
second call), don't stream — return the whole payload. Streaming a fast call
adds SSE/WebSocket framing, reconnect logic, and partial-parse bugs for no UX win
[mdn/sse]. Exit here for fast paths.
Map the surface to one (or more) of the three audiences:
| Surface | Primary projection | LangGraph mode | LangChain |
|---|---|---|---|
| Chat / prose | final tokens | messages | astream_events → on_chat_model_stream |
| Agent inspector / dev UI | step updates | updates | astream_events (on_tool_*, on_chain_*) |
| Full-state replay / resume | snapshots | values | n/a (rebuild from events) |
| Long in-tool work | custom progress | custom | dispatched custom events |
| Debug everything | raw firehose | debug | astream_events (all) |
values emits the full state after each step (heavy, good for resume);
updates emits only the diff (light, good for live UI) [lg/stream]. Default
a chat agent to ["messages","updates"] and add "custom" only when a tool has
internal progress worth surfacing [lg/stream] (= [[agentsop-langgraph]] OP-8).
Gate questions: does the client only receive (server→client), or also need to send mid-stream (interrupt, steer)?
text/event-stream, auto-reconnect + Last-Event-ID built into the browser
EventSource [mdn/sse]. This is what most "stream the agent" use cases need.Use the multi-mode form so one connection carries everything; tag each chunk so the client routes it:
async for mode, chunk in graph.astream(
inp, stream_mode=["messages", "updates", "custom"], config=cfg):
if mode == "messages":
token, meta = chunk
yield sse("token", token.content) # → append to bubble
elif mode == "updates":
yield sse("step", chunk) # → "running tool X"
elif mode == "custom":
yield sse("progress", chunk) # → progress bar
[lg/stream]. SSE event: field is exactly the demux key; the browser's
EventSource.addEventListener("token"|"step"|"progress", …) splits it client-side
[mdn/sse]. Never interleave two semantic streams on one untagged channel — the
client can't tell a token from a tool name.
For each surface answer: on client disconnect, cancel or detach?
[oai/stream].[[agentsop-langgraph]] Step 6) and a resumable event log so reconnect replays via
Last-Event-ID [mdn/sse].
Default for a chat agent: cancel (cheap, stateless). Default for a long
side-effecting pipeline: detach + persist.: keep-alive\n\n) every ~15s during long quiet stretches [mdn/sse].updates (keep
messages) or apply flow control. Tokens are the audience-critical stream;
progress events are droppable.X-Accel-Buffering: no for nginx) or the
proxy batches your tokens and kills the "streaming" feel.Format: Trigger → Action → Output → Evidence.
graph.astream(inp, stream_mode="messages") → yield each
(token, metadata)'s token.content; filter by metadata so you only stream
the final node's LLM, not sub-agent chatter. Raw: OpenAI stream=True iterate
chunk.choices[0].delta.content; Anthropic with client.messages.stream(...) as s: for t in s.text_stream.[lg/stream] messages mode; [oai/stream]; [anthropic/stream].stream_mode="updates" → each chunk is {node_name: state_diff}; render as a step log. LCEL: astream_events(version="v2") and
switch on event["event"] (on_tool_start/on_tool_end/on_chain_*).values.[lg/stream] updates mode; [lc/astream-events].w = get_stream_writer(); w({"progress": i/n}); consume on stream_mode="custom". LCEL — dispatch a custom event /
callback that astream_events surfaces.[lg/stream] custom mode + stream writer.stream_mode=["messages","updates","custom"]; map each (mode, chunk) tuple to a distinct SSE event: name; client addEventListener per
name (Step 4 snippet).[lg/stream] (list form returns (mode, chunk) tuples);
[mdn/sse] (named events).Last-Event-ID). Client must push mid-stream (cancel, steer, collaborate) →
WebSocket. Both ends yours / non-HTTP → async generator.[mdn/sse] (EventSource auto-reconnect, server-push only).[oai/stream].
Detach: keep running under a checkpointer, log events with monotonic IDs so a
reconnect replays from Last-Event-ID.[oai/stream] cancellation; [mdn/sse] Last-Event-ID;
[[agentsop-langgraph]] Step 6 (checkpointer for durability).: ping\n\n comments every ~15s; set X-Accel-Buffering: no /
disable proxy buffering; flush after each event.[mdn/sse] (comment lines ignored by client, keep socket warm).messages mode, inspect metadata (langgraph_node, tags) and
forward only tokens whose node is the user-facing responder; route the rest to
updates (dev view) or drop.[lg/stream] (messages chunks carry node metadata for filtering).messages only, the user stares at a frozen spinner for
40s, then sees text. If you stream updates only, they see "calling tool X" but
the final answer dumps all at once, losing the typing feel.[lg/stream].stream_mode=["updates","messages"]. During tool work, updates chunks
drive a live step list ("Searching… Reading 5 docs… Synthesizing"). When the
final responder node starts emitting, messages tokens stream into the
bubble (OP-4 demux).messages to the final node only (OP-8) so the tool-call LLMs don't
leak into the answer.custom progress from inside it (OP-3)
so the step list isn't itself frozen.[[agentsop-langgraph]] Step 6 / HITL ordering — side
effects in their own committed step).Last-Event-ID so the user sees the outcome [mdn/sse].[oai/stream] — that's the cheaper, correct default for chat.debug/values firehose to a
chat UI floods the client with full-state snapshots and sub-agent tokens. Project
to the audience (Step 2); values is heavy by design [lg/stream].[mdn/sse].[oai/stream].event:
/ the (mode, chunk) tuple (OP-4) [lg/stream] [mdn/sse].messages by node
metadata (OP-8) [lg/stream].[mdn/sse].[mdn/sse]. Reserve WS for true
bidirectional needs.Hard boundaries (streaming is the wrong tool when):
| Concern | LangGraph | LangChain (LCEL) | OpenAI SDK | Anthropic SDK |
|---|---|---|---|---|
| Final tokens | stream_mode="messages" → (token, meta) [lg/stream] | astream_events → on_chat_model_stream [lc/astream-events] | stream=True, iterate delta.content [oai/stream] | client.messages.stream(...), text_stream [anthropic/stream] |
| Intermediate steps | stream_mode="updates" (per-node diff) [lg/stream] | astream_events (on_tool_*, on_chain_*) [lc/astream-events] | manual: detect tool_calls deltas [oai/stream] | manual: handle content_block_* / tool-use events [anthropic/stream] |
| Full state | stream_mode="values" (snapshot) [lg/stream] | rebuild from events | n/a | n/a |
| Custom progress | stream_mode="custom" + get_stream_writer() [lg/stream] | dispatch custom event / callback [lc/astream-events] | hand-rolled out-of-band | hand-rolled out-of-band |
| Mix modes | list form → tagged (mode, chunk) tuples [lg/stream] | one typed event stream, switch on event [lc/astream-events] | one delta stream, branch on field | one event stream, branch on type |
| Granularity | node-level + token-level + custom | event-level (richest typed taxonomy) | token + tool-call deltas | event + token (typed blocks) |
Heuristics:
[lg/stream] modes are the cleanest projection model. See
[[agentsop-langgraph]] OP-8 for the orchestration side.astream_events gives the richest typed event taxonomy
(every on_* lifecycle hook); reach for it when you need fine-grained event
routing without a full graph [lc/astream-events].[oai/stream]
[anthropic/stream].Transport is orthogonal to all four: SSE (default, receive-only), WebSocket
(bidirectional), or async generator (internal) wraps any of them. Pick the SDK for
what to stream, the transport for how the client consumes it [mdn/sse].
Short tags → full sources in references/R1-source-evidence.md:
[lg/stream] = LangGraph streaming concept (values / updates / messages / custom
/ debug; multi-mode list; get_stream_writer) — distilled in
[[agentsop-langgraph]] OP-8 + references/R1.[lc/astream-events] = LangChain LCEL astream_events typed event stream.[oai/stream] = OpenAI streaming (stream=True, deltas, cancellation).[anthropic/stream] = Anthropic Messages streaming (client.messages.stream,
text_stream, content-block events).[mdn/sse] = MDN Server-Sent Events (EventSource, named events, auto-reconnect,
Last-Event-ID, comment heartbeats).