Streaming

In shortWhy and how to stream agent responses. Python execution modes, StreamChunk, ResponseGranularity, and transport options.

  • 5 min read
  • 11 sections
  • Updated
  • v0.10.0
  • Markdown

10xGraph provides three execution modes for agents: invoke to wait for the final result, stream (sync generator) for incremental chunks in blocking code, and astream (async generator) for incremental chunks in async contexts. Streaming is essential for chat and realtime UIs where users expect to see model output appear token by token and tool results arrive as they complete.


Why stream

Invoking a graph end-to-end blocks the caller until every node finishes. For interactive experiences, streaming reveals progress to the user while the graph is still running. The stream emits structured chunks as the model generates text, tools execute in parallel, and nodes complete. Your UI can show tokens as they arrive, tool calls as they’re made, and state updates as they occur, rather than waiting for silence.

Streaming also bounds memory and latency on long-running agents: you don’t buffer a 50,000-token context in RAM before handing it back.


How it works

sequenceDiagram
  participant Client
  participant Graph

  Note over Client,Graph: invoke, wait for the full response
  Client->>Graph: invoke(input, config)
  Graph->>Graph: run all nodes
  Graph-->>Client: final result dict

  Note over Client,Graph: stream / astream, incremental chunks
  Client->>Graph: stream(input, config)
  Graph-->>Client: StreamChunk (message)
  Graph-->>Client: StreamChunk (message)
  Graph->>Graph: run remaining nodes
  Graph-->>Client: StreamChunk (state)

Use invoke or ainvoke when you need the full result before proceeding. Use stream or astream when the client should see partial responses immediately as they arrive.


StreamChunk model

Every streaming event is a StreamChunk Pydantic model with a discriminated event type:

Python
from tenxgraph.core.state.stream_chunks import StreamChunk, StreamEvent

class StreamChunk(BaseModel):
    event: StreamEvent          # "message" | "state" | "error" | "updates"
    message: Message | None     # populated for StreamEvent.MESSAGE
    state: AgentState | None    # populated for StreamEvent.STATE
    data: dict | None           # populated for StreamEvent.ERROR / UPDATES
    thread_id: str | None
    run_id: str | None
    metadata: dict | None
    timestamp: float            # UNIX timestamp

The event field determines which of the four data fields is populated. Match on it to handle each event type:

StreamEvent Meaning Populated field Use
MESSAGE Model or tool output message Render text tokens, display tool calls
STATE State after a node’s messages are added state Show execution progress, update context
ERROR Execution failed data Handle errors in the stream
UPDATES Custom node emission data Receive progress, metrics, or custom data from tools

Response granularity

Both stream() and astream() accept a response_granularity parameter that controls what state is included in each StreamChunk. This lets you tune the amount of data streamed: less granularity saves bandwidth and parsing; higher granularity gives the client a complete view of execution state:

Python
from tenxgraph.utils import ResponseGranularity
Value Includes Use when
LOW (default) Latest messages only You only need the model’s output, not internal state
PARTIAL Messages, context, summary You need context to render memory or summaries alongside output
FULL Full state plus all messages You’re rebuilding the complete execution state on the client

Synchronous streaming

Use stream() from blocking code (non-async libraries, CLI scripts):

Python
from tenxgraph.core.state import Message
from tenxgraph.utils import ResponseGranularity
from tenxgraph.core.state.stream_chunks import StreamEvent

for chunk in app.stream(
    {"messages": [Message.text_message("Tell me a short story.")]},
    config={"thread_id": "stream-1", "recursion_limit": 10},
    response_granularity=ResponseGranularity.LOW,
):
    if chunk.event == StreamEvent.MESSAGE and chunk.message is not None:
        print(chunk.message.text(), end="", flush=True)

print()  # trailing newline

Asynchronous streaming

Use astream() inside async contexts (FastAPI, async tests, async notebooks):

Python
import asyncio
from tenxgraph.core.state import Message
from tenxgraph.utils import ResponseGranularity
from tenxgraph.core.state.stream_chunks import StreamEvent

async def main():
    inp = {"messages": [Message.text_message("Call get_weather for Tokyo.")]}
    config = {"thread_id": "astream-1", "recursion_limit": 10}

    async for chunk in app.astream(inp, config, ResponseGranularity.LOW):
        if chunk.event == StreamEvent.MESSAGE and chunk.message is not None:
            print(chunk.message.text(), end="", flush=True)
        elif chunk.event == StreamEvent.STATE and chunk.state is not None:
            print(f"\n[state received, step={chunk.state.execution_meta.step}]")

asyncio.run(main())

Handling all event types

Use pattern matching to process each chunk type without repeated conditionals:

Python
async for chunk in app.astream(inp, config):
    match chunk.event:
        case StreamEvent.MESSAGE:
            print("message:", chunk.message.text())
        case StreamEvent.STATE:
            print("state step:", chunk.state.execution_meta.step)
        case StreamEvent.ERROR:
            print("error:", chunk.data)
        case StreamEvent.UPDATES:
            print("custom update:", chunk.data)

Non-streaming execution

When you don’t need incremental results, use invoke() or ainvoke() to run the graph to completion and return the final state dict:

Python
# Synchronous
result = app.invoke(
    {"messages": [Message.text_message("Hello!")]},
    config={"thread_id": "t1"},
    response_granularity=ResponseGranularity.LOW,
)
messages = result["messages"]   # list of Message

# Asynchronous
result = await app.ainvoke(
    {"messages": [Message.text_message("Hello!")]},
    config={"thread_id": "t1"},
)

The returned dict keys depend on response_granularity:

Granularity Keys returned
LOW messages
PARTIAL messages, context, summary
FULL messages, state

Stopping a stream

Request graceful cancellation of a running stream with stop() or astop(). The graph checks the stop flag after each node and exits cleanly. astop() itself returns a small status dict such as {"ok": True, "running": True}:

Python
# Stop from another coroutine or thread
await app.astop({"thread_id": "stream-1"})

The stopped stream does not emit further chunks after the cancellation takes effect.


Transports

Streaming is available over multiple transport layers. Choose based on your deployment and client type:

REST (NDJSON)

The HTTP /v1/graph/stream endpoint streams responses as newline-delimited JSON (NDJSON), one StreamChunk per line. Use this when you need a simple HTTP transport that works with any HTTP client (curl, fetch, axios) and no bidirectional connection. Not ideal for high-frequency token streams due to HTTP framing overhead.

See Invoke and stream over REST for details and examples.

WebSocket (turn-based)

The WebSocket /v1/graph/ws endpoint streams individual StreamChunk frames over a persistent WebSocket connection. Use this for lower-latency bidirectional communication on one persistent connection. Suitable for most chat UIs.

See WebSocket streaming for configuration, authentication, and close codes.

WebSocket (realtime audio)

The WebSocket /v1/graph/live endpoint is a provider-neutral audio-to-audio bridge: binary frames carry PCM16 audio and JSON frames carry control messages and events, not StreamChunk objects. Use it only if your agent is built for continuous audio I/O.

See Realtime audio agent for integration details.

AG-UI

The POST /v1/ag-ui endpoint implements the AG-UI (agentic UI) protocol for frameworks like CopilotKit. It streams events and tool results in the AG-UI format, and handles browser tool execution.

This transport is off unless ag_ui.enabled is set in your 10xgraph.json, and needs the ag-ui extra (pip install "10xgraph-api[ag-ui]").


TypeScript client

From TypeScript, TenxGraphClient.stream() (HTTP) and TenxGraphClient.wsStream() (WebSocket) return async iterators of stream chunks, so you do not parse NDJSON or manage WebSocket frames yourself.

See Stream responses in TypeScript for code examples and React integration patterns.


Last updated for v0.10.0Edit this page on GitHubReport an issue