Skip to content
agentFast

Streaming

Watch a run live over SSE — tokens, tools, approvals, re-attach.

In short: you watch the agent work in real time instead of staring at a loading spinner — what it's looking up, what it's doing, what it's writing, and the moment it needs a person. If your connection drops, you rejoin the run already in progress rather than starting it over.

Every run is watchable live over Server-Sent Events, using the open agent-stream protocol. Not just tokens — tool calls starting and finishing, turn economics, the production layer's own work, and the moment a run pauses for a human.

The events

EventFires when
run_startedThe run is created — carries the run_id
tokenAssistant text, as it's produced
thinkingExtended reasoning, where the SDK exposes it
tool_useA tool is about to execute
tool_resultIt finished — with duration and status
turnA model call completed — tokens, cost, latency
progressProduction-layer work: complexity assessed, memory recalled, guardrail applied
approval_requiredThe run paused for a human (agentFast extension)
errorStructured failure
doneAlways last

tool_use and tool_result share a tool_use_id, so a client can pair them even when one turn calls the same tool twice.

Consuming it

curl -N -X POST localhost:8321/api/chat/stream \
  -H 'Content-Type: application/json' \
  -d '{"message":"I need a refund for ord_1001 — it arrived damaged."}'
event: run_started
data: {"run_id": "run_b4110ebd…", "agent": "support", "sdk": "langgraph"}

event: progress
data: {"step": "planning", "message": "assessed task complexity: medium"}

event: tool_use
data: {"tool_name": "kb_search", "tool_use_id": "call_kb_1", "status": "running"}

event: tool_result
data: {"tool_name": "kb_search", "tool_use_id": "call_kb_1", "duration_ms": 6, "status": "done"}

event: approval_required
data: {"run_id": "run_b4110ebd…", "approval_ids": ["appr_…"], "tool_name": "refund_request"}

event: done
data: {"status": "paused", "paused": true, "run_id": "run_b4110ebd…"}

In React, the vendored hook handles parsing, reconnection and tool state:

const { text, activeTools, pausedForApproval, startStream } = useAgentStream();

await startStream("/api/chat/stream", { message }, {
  onApprovalRequired: (e) => showApprovalCard(e.approvalIds),
});

Two things worth knowing

A pause is never the last event. approval_required is always followed by a terminating done with paused: true. agentFast's own client reads the first and renders an approval card; a stock agent-stream client — which has no case for the event — ignores it and terminates cleanly on the done. Without that, a paused run would render as finished.

Disconnecting does not cancel the run. A run may be mid-tool-call with real side effects, and killing it because a browser tab closed is exactly the failure agentFast exists to prevent. The run continues; re-attach with GET /api/runs/{id}/stream, which replays the step tree so far and then follows live. To actually stop a run, use the explicit cancel path.

CarefulNever reconnect by re-POSTing

Retrying POST /api/chat/stream after a dropped connection starts a second run — re-executing tools and re-spending money. Reconnect against the run's own stream endpoint instead. The vendored client does this for you; if you write your own, this is the bug to avoid.

Redaction is structural

The stream is fed from the same redacted step records the durable trace is built from — never from raw hook arguments. A tool called with a customer's email address puts [EMAIL_1] on the wire, not the address, because the only values the stream can see are values the stored trace already saw.

What each SDK gives you

Step-level events — tools, approvals, progress, turn economics — come from the runtime hooks and are identical on all five SDKs. Token-level deltas are adapter-specific:

SDKToken streaming
VanillaYes — messages.stream
LangGraphYes — astream
Claude Agent SDKYes — partial messages
OpenAI Agents SDKYes — Runner.run_streamed
CrewAINo native seam — text arrives one chunk per turn

CrewAI's kickoff() returns only when the whole crew is done, so there's nowhere to publish deltas from. It still streams everything else, and the assistant text is backfilled from the model step — so every adapter produces readable streaming text, only the granularity differs.

Scope limit

The event bus is in-process. Under uvicorn --workers N, GET /api/runs/{id}/stream only follows runs owned by the worker handling the request. That's safe rather than silently wrong — the replay still comes from Postgres and the client falls back to polling GET /api/runs/{id}. Redis pub/sub is the multi-worker upgrade and needs no change to callers.