Agent Event Streaming: Consumer Contract Above the Tokens¶
A typed event stream the harness emits at decision points. UIs subscribe to this contract instead of raw token deltas, surviving model and harness swaps.
An agent event stream is the typed, ordered sequence of events the harness emits at decision points: tool dispatched, tool returned, sub-agent spawned, state updated, run finished. It sits above the LLM's token-level SSE (Claude API streaming) and below the app's domain model — the contract lives where the agent decides, not where the model emits letters.
Token stream versus agent stream¶
| Stream | Producer | Granularity | Vocabulary | Stability across model/harness swap |
|---|---|---|---|---|
| Token | LLM SDK | One token chunk per delta | Provider-specific (content_block_delta, delta.text) |
Breaks on model swap (Claude API streaming) |
| Agent | Harness | One event per decision | Semantic verbs (RunStarted, ToolCallStart, StateDelta) |
Survives model swap (AG-UI events) |
LangGraph names both modes: stream_mode="messages" yields token chunks; stream_mode="updates" "emits an event after every agent step" (LangChain streaming docs). Production deployments subscribe to both. Tokens feed the typing animation; updates feed tool indicators, sub-agent tabs, and approval prompts. LangChain describes the shift from token streams to agent streams: higher-level events for steps, tool calls, and state transitions, separate from the raw token deltas underneath (LangChain, from token streams to agent streams).
The event vocabulary¶
The AG-UI Protocol — an open standard with integrations across LangGraph, CrewAI, Google ADK, Pydantic AI, and others — groups events into seven categories (AG-UI events):
- Lifecycle:
RunStarted,StepStarted,StepFinished,RunFinished,RunError— bounds and progress markers carryingthreadId,runId, and an optionalparentRunId. - Tool call:
ToolCallStart,ToolCallArgs,ToolCallEnd,ToolCallResult. The Vercel AI SDK Data Stream Protocol uses the same shape with different names —tool-input-start,tool-input-delta,tool-input-available,tool-output-available(Vercel AI SDK). - Text message:
TextMessageStart,TextMessageContent,TextMessageEnd— message-level streaming above tokens. - State:
StateSnapshot,StateDelta— typed shared store between agent and frontend, event-sourced. - Reasoning:
ReasoningStart,ReasoningMessageContent,ReasoningMessageEnd— LangChain normalizes Anthropicthinkingand OpenAIreasoningblocks into onereasoningcontent-block type (LangChain streaming docs). - Activity and Special (Custom, Raw) — escape hatches for harness-specific work.
The categories are the load-bearing design choice. Subscribe by category — "render all tool-call events" — and the consumer survives new event types within it. Hardcode names and it does not.
Why it works¶
The agent stream inverts consumer stability. A token-stream consumer commits to LLM-output deltas. A sub-agent spawn, guardrail, or tool retry never appears there, so the consumer infers them from the assembled message. An agent-stream consumer commits to harness-decision verbs and renders affordances — approval modals, sub-agent tabs, retry indicators — directly (AG-UI events). Swapping the LLM (Anthropic → Gemini → OpenAI) replaces the token stream but leaves the verbs intact — the abstraction inversion event-sourcing applies to databases (Fowler, EventSourcing), at the harness/LLM boundary.
Versioning the event schema¶
Event-driven consumers outlive the producer code, so the vocabulary needs additive-only evolution (theburningmonk, event versioning strategies). The Confluent compatibility taxonomy applies directly: new event types and optional fields are safe, because consumers ignore unknowns; renames and removals break every consumer at once (Confluent schema compatibility). When a shape must change, ship an upcaster at the consumer boundary. This discipline matters more than in a Kafka pipeline because each event renders in a user-facing UI.
When this backfires¶
The pattern adds a vocabulary-design and versioning obligation that does not pay off everywhere. Stay with raw token streams when:
- Pure conversational chat UIs — step events at the bubble layer hide the time-to-first-token signal users expect. Reported TTFT with token streaming is typically 200–500 ms versus a 5–30 s wait for the full response without it (thefrontkit, streaming UI guide); dropping that channel makes the interface feel broken.
- Single-harness, single-vendor stacks — the portability benefit disappears, and raw SDK events (Anthropic SSE, OpenAI deltas) are cheaper.
- Ad-hoc payloads that mirror harness internals — if
tool_call_startedcarries the harness's node name, retry count, or framework-specific tool ID, the UI couples to the harness; the vocabulary must be designed semantically, not echoed from the runtime. - Renames are tolerated — without additive-only discipline, event-stream consumers degrade more than token-stream ones because each event carries semantic weight (theburningmonk).
The dominant production shape runs both streams in parallel (LangChain streaming docs). The agent stream replaces tokens only when the product is agent-as-coworker — IDE pair, research dashboard, ops console — not agent-as-chatbot.
Example¶
In LangGraph v1.2+, subscribing to both streams yields step events from the update channel and token chunks from the message channel, distinguished by chunk["type"] (LangChain streaming docs):
async for chunk in graph.astream(
{"messages": [{"role": "user", "content": "What is the weather in SF?"}]},
stream_mode=["updates", "messages"],
):
if chunk["type"] == "updates":
# Agent stream: one event per node — model, tools, model again
for step, data in chunk["data"].items():
print(f"step={step}")
elif chunk["type"] == "messages":
# Token stream: AIMessageChunk and tool_call_chunk deltas
token, metadata = chunk["data"]
The Vercel AI SDK Data Stream Protocol carries the same separation over SSE — tool-call lifecycle events are explicit parts of the stream, separate from text deltas (Vercel AI SDK Stream Protocol):
data: {"type":"tool-input-start","toolCallId":"call_abc","toolName":"getWeather"}
data: {"type":"tool-input-delta","toolCallId":"call_abc","inputTextDelta":"San Francisco"}
data: {"type":"tool-input-available","toolCallId":"call_abc","toolName":"getWeather","input":{"city":"San Francisco"}}
data: {"type":"tool-output-available","toolCallId":"call_abc","output":{"weather":"sunny"}}
data: {"type":"finish-step"}
A frontend subscribing only to tool-* parts renders a tool-call indicator; a frontend subscribing only to text deltas renders the assistant bubble. The same backend feeds both — the discipline is keeping the event names and field shapes additive over time.
Key Takeaways¶
- An agent event stream is a typed event sequence at harness decision points (
RunStarted,ToolCallStart,StateDelta,RunFinished); the consumer contract one layer above LLM token SSE (AG-UI events, Claude API streaming). - The convergent vocabulary across AG-UI, LangGraph, and the Vercel AI SDK groups events into lifecycle, tool-call, text-message, state, reasoning, and custom/special categories — subscribe by category, not by individual event name.
- The mechanism is abstraction inversion: the harness is the event source of truth; token streams and DOM updates are projections. Model swaps change the projection, not the contract.
- Versioning is the load-bearing obligation. Additive-only changes survive; renames and removals break every consumer at once because each event carries product meaning (theburningmonk, Confluent schema compatibility).
- Most production deployments run both streams — token deltas for the chat bubble, step events for everything else. Replace tokens with events only when the product is agent-as-coworker, not agent-as-chatbot (LangChain streaming docs, thefrontkit, streaming UI guide).
Related¶
- Model a Single Agent Turn as Many Inference and Tool-Call Iterations — the underlying loop whose step boundaries the agent stream exposes
- Delta Channels: Bounded Checkpoint Storage for Append-Only Agent State — runtime-side delta primitive; the agent stream is the consumer-side counterpart
- Event Sourcing for Agents — server-side append-only event log; the agent stream is its consumer-facing projection
- Agent Debug Log Panel: Chronological Event Inspection — operator UI built on the same event stream, separate from the user-facing transcript
- Agent Loop Middleware — where harness-decision events are emitted from in practice — middleware nodes at loop boundaries