Quick example
Stream modes
Control what data you receive by settingstream_mode on the run. You can pass a single mode as a string or multiple modes as a list.
debug and updates are used internally on every stream for checkpoint tracking and interrupt detection, but their events are only sent to the client if you explicitly request them in stream_mode. When updates is not requested, only interrupt events (human-in-the-loop) are forwarded — remapped as values events for compatibility.Custom streaming
Send custom data from inside graph nodes usingget_stream_writer():
custom in the stream modes:
Using multiple modes
event field tells you which mode each chunk comes from.
Event types
During streaming, you’ll receive these event types:Serialized graph values
Graph state and streamed events are converted to JSON-compatible values before they are sent over SSE or stored in the run output. Dataclasses are serialized recursively by field, and common Python values use these representations:
Nested values inside Pydantic or LangChain model dumps are normalized using the
same rules. Mapping values are serialized recursively, and non-JSON-native keys
such as
UUID or Path are converted to JSON property names with the same
scalar rules. Key collisions after conversion raise a serialization error.
Streaming endpoints
Create and stream
The most common pattern — create a run and stream its output in one call:POST /threads/{thread_id}/runs/stream under the hood.
Stream an existing run
If you created a background run, you can stream it later:Last-Event-ID header. If the connection drops, the client can reconnect and receive events from where it left off.
Wait for completion
If you don’t need streaming but want to wait for the result:POST /threads/{thread_id}/runs/wait and returns the final output.
If the run fails, the body is an error envelope instead of an output:
client.runs.wait() raises on that envelope by default; pass raise_error=False to get it
back as a dict. The same envelope is returned when the wait gives up before the run reaches a
terminal state (TimeoutError or IncompleteRun), and by client.runs.join(), which returns it
rather than raising. An interrupted run is not a failure — it returns its partial output with
__interrupt__.
Subgraph streaming
If your graph uses subgraphs, you can stream events from them too:Disconnection behavior
By default, when a client disconnects during streaming, the run is cancelled. You can change this:
The server sends SSE keepalive comments (
: heartbeat) every KEEPALIVE_INTERVAL_SECS (default 5s, truncated to whole seconds, minimum 1s), so idle proxies (Nginx 60s, AWS ALB, Cloudflare) won’t drop long-running silent agents — for example, a graph node holding an upstream WebSocket without emitting events. The wire-format matches LangGraph Platform, so existing SSE clients (the LangGraph SDK, browser EventSource, httpx-sse) silently ignore these lines per the W3C spec. Only a real client disconnect — not an idle proxy timeout — triggers automatic cancellation.
Background runs
For long-running tasks, you can create a run in the background and check on it later:Cancelling runs
Cancel or interrupt a running execution:pending or running; poll, join, or stream the run until it reaches a terminal status.
Use the REST endpoint with wait=1 when the caller needs the request to wait briefly for the worker to settle.
Cancelling several runs
POST /runs/cancel cancels many runs in one call. Select them by thread_id plus run_ids, or by status (pending, running, or all). The SDK exposes it as cancel_many:
action accepts cancel and interrupt, the same values as the per-run endpoint. The response is 204 No Content.
SSE reconnection
Aegra stores streaming events in a replay buffer (Redis Lists whenREDIS_BROKER_ENABLED=true, in-memory list in dev mode) for replay. If your connection drops:
- Track the last event ID you received
- Reconnect to
GET /threads/{thread_id}/runs/{run_id}/streamwithLast-Event-IDheader - You’ll receive all events from where you left off
In production deployments, SSE events are delivered via Redis pub/sub from workers — the client’s SSE connection and the worker executing the run can be on different instances. See the worker architecture guide for details on how this works.
Agent Protocol v2 event streaming
The latest LangGraph JS and Python SDKs (including@langchain/langgraph-sdk and the Vue/React useStream() composables) speak a newer streaming protocol with a dedicated event envelope and a content-block message model. Aegra serves this protocol natively.
v2 streaming is on by default — it’s a new endpoint set the SDK targets and has no v1 to break; the legacy
runs/stream endpoints are unchanged. FF_V2_EVENT_STREAMING is a kill switch: set it to false to disable v2 serving (requests return 503 with an enable hint) and roll back without a redeploy. The endpoints also require a langgraph / langchain-core new enough to emit native v3 events; the server returns 503 with an upgrade hint if the runtime is too old.Using the SDK
The stock LangGraph SDK drives both endpoints for you. Streaming is thread-scoped: open the stream, start a run, and consume the events:useStream() composables.
Endpoints
The stream is scoped to the thread, not a run — you open it, then issue
run.start, and the events of whatever run executes flow through. The client mints the thread id, and run.start creates the thread if it doesn’t exist yet.
Starting a run
context is forwarded to both the graph factory through ServerRuntime[T] and graph nodes through Runtime[T]. It is an Aegra extension: the Agent Protocol run.start params are assistant_id, input, config, metadata, and langsmith_tracer. For a payload that also works against LangGraph Platform, put the same values under config.configurable; when context is absent the server copies configurable into it.
To fork or time travel, set config.configurable.checkpoint_id to a checkpoint of the thread. The run replays from that checkpoint, and input is optional in that case. The JS SDK forkFrom option (the regenerate button in useStream and assistant-ui) sends this field. A value that is not a UUID string returns invalid_argument.
langsmith_tracer is accepted and ignored. Aegra traces through OpenTelemetry, see Observability.
Params outside the documented set are logged at warning level with their key names and otherwise ignored.
Streaming events
The SSE filter is a POST body listing the channels you want — no run id. Passsince (the last seq you saw) to resume after a dropped connection.
data: line — a protocol event envelope. seq is the cursor you echo back as since; event_id dedups across reconnects; params.data is the payload and params.namespace the subgraph path:
Channels
values, updates, messages, tools, lifecycle, input, checkpoints, tasks, and custom (plus custom:<name>). Message streams arrive as content-block events (message-start → content-block-delta → message-finish); lifecycle reports started / completed / failed / interrupted.
Human-in-the-loop interrupts
When you subscribe to theinput channel, a graph interrupt arrives as an input.requested event. Its params.data includes the interrupt id and two identical content fields:
payload is the Agent Protocol field consumed by the JavaScript SDK and Vue/React useStream() integrations. value mirrors it for current Python SDKs, including ts.interrupts[].value. If the source interrupt has no value, both content fields are omitted; explicit null, empty, and falsy values are preserved under both fields.
Interrupt IDs are deduplicated within each run so the same interrupt is not emitted twice when it appears in both updates and values. A resumed run may reuse an earlier run’s ID; its input.requested event is emitted again because it belongs to a new run.
Responding to an interrupt
input.respond resumes the interrupted run. Only response is required; interrupt_id targets one interrupt when several are pending.
update and goto fold into the same Command as the resume, so the resumed run writes a single checkpoint.
This first release covers the HTTP SSE + commands path the JS/Python SDKs use, verified end-to-end against the stock
langgraph-sdk. The WebSocket transport and the agent.getTree / state.fork / subscription.* commands are not yet implemented; SSE filtering via the POST body covers the useStream() path without them.