Skip to main content

Streaming Idioms

All CRAFT agents stream their responses via Server-Sent Events (SSE). This page covers the practical patterns for producing well-formed SSE streams, handling partial results, and implementing cooperative cancellation.
A2A SSE vs. MCP Streamable HTTP: This page covers A2A streaming — the JSON-RPC events that flow between agents via Server-Sent Events (SSE). This is different from the MCP Streamable HTTP transport used to connect to the CRAFT tool gateway. A2A’s message/stream uses SSE; MCP tool connections use the MCP Streamable HTTP transport (e.g., ADK’s StreamableHTTPConnectionParams, FastMCP’s StreamableHttpTransport, the MCP Python SDK’s streamablehttp_client, or LangGraph’s MultiServerMCPClient with transport="http"). Both A2A SSE and MCP Streamable HTTP are current standards — they serve different protocol layers.

The Streaming Contract

The A2A protocol requires every message/stream response to follow a strict event ordering:
  1. First event: Task (state = submitted)
  2. Zero or more: TaskStatusUpdateEvent (state = working) with intermediate progress messages
  3. Zero or more: TaskArtifactUpdateEvent with result artifacts
  4. Final event: TaskStatusUpdateEvent with final: true and a terminal state (completed, failed, canceled)
Every event is a JSON-RPC 2.0 response object sent as a data: line in the SSE stream.
Never close the SSE connection without emitting a final event with final: true. Clients that receive an unexpected connection close will attempt to resubscribe via tasks/resubscribe, and if the task store has no record of the task, they will error. Always emit a final status event before the connection closes, even on error.

Streaming Text Output

Pydantic AI — run_stream with stream_text()

The following pattern demonstrates streaming text output from a Pydantic AI agent. Each text chunk is forwarded as a TaskStatusUpdateEvent with state=working:
stream_text() returns the accumulated text at each yield, not a delta. Each chunk is the full response so far. This means the client always has a coherent partial response it can display immediately, without needing to concatenate chunks.

Google ADK — SSE via to_a2a()

When using to_a2a(), ADK handles all SSE event emission automatically. Your agent code just runs normally:
ADK’s to_a2a() uses TaskStatusUpdateEvent with state=working for streaming intermediate text chunks, then emits a TaskArtifactUpdateEvent for the final text. This differs from the Pydantic AI pattern (which uses only status events). Both are valid A2A; clients must handle both patterns.

Claude Agent SDK — Streaming via A2A

The Claude Agent SDK doesn’t wrap natively in A2A. Build a thin A2A executor that streams from the Anthropic API and emits the correct A2A events:

LangGraph — Streaming via A2A

Use stream_mode="messages" which yields (chunk, metadata) tuples for token-level streaming. Use stream_mode="updates" for node-completion events instead.
For graphs without a messages state key, switch to stream_mode="updates" and iterate for node_name, payload in chunk.items(): to detect completed nodes.

Streaming Intermediate Status Messages

Use intermediate status messages to keep users informed during long-running tasks. Emit an intent acknowledgment before the main agent loop:
For long database queries, emit named step messages:

Artifact Streaming

Artifacts (charts, data files, analysis results) are emitted as TaskArtifactUpdateEvent. For multi-artifact agents (like a text-to-SQL agent that produces intermediate SQL plans followed by query results), artifacts are emitted in a specific order:
  1. sql_plan — intermediate SQL planning artifact (TextPart, JSON)
  2. query_summary — human-readable summary of results (TextPart)
  3. sql_query — the generated SQL (DataPart with artifact URI)
  4. query_results — Parquet results file (DataPart with artifact URI, last_chunk=true)

Cooperative Cancellation

CRAFT supports cooperative cancellation: a client sends tasks/cancel, and the agent should stop its current work and emit a canceled terminal event.

Pydantic AI — asyncio.CancelledError

Implement cancellation via asyncio task cancellation. The cancel() method updates the task state in the database before the running asyncio task receives the cancellation signal:

Cross-Instance Cancellation

In Kubernetes with multiple agent replicas, the cancel request may hit a different pod than the one running the task. Use a background polling watcher to detect cross-instance cancellation:
Start the watcher at the beginning of execute() and cancel it in the finally block:

Reconnection Handling

If an SSE connection drops mid-stream, the client sends a tasks/resubscribe request. The DefaultRequestHandler from the a2a library handles this automatically when used with a persistent task store.
InMemoryTaskStore does not survive pod restarts. In production, implement a database-backed TaskStore (e.g., backed by PostgreSQL). If the task store loses state, clients that attempt tasks/resubscribe will receive a TaskNotFoundError.

LLM Error Retry

Transient LLM errors (HTTP 5xx, 429) should be retried with exponential backoff. A well-behaved executor retries up to 2 times:

First-Token Latency

Track time-to-first-token to detect model cold starts and MCP connection overhead:
Typical first-token latencies on CRAFT: 500ms–2s for Gemini Flash, 1s–4s for Gemini Pro with cold MCP connections.

Next Steps

Debugging Agents

Inspect streaming traces and diagnose SSE issues.

Eval Harness

Set up Langfuse to evaluate streaming agent behaviour.