Streaming

This guide covers SSE streaming for real-time task updates and chunked artifact delivery.

Opening a Stream

SSE streaming works natively with Falcon — no threads, no polling. Every request gets a stream builder at env["a2a.stream"]. Call open to start streaming; it auto-selects the right wire format (JSON-RPC or REST) for the current request, runs your block inside an Async fiber, and finishes the stream automatically when the block exits — even on exception.

in "SendStreamingMessage"
  env["a2a.stream"].open(task_id: task_id, context_id: context_id) do |s|
    s.task(status: { state: "TASK_STATE_WORKING", timestamp: now })
    # ... do the work ...
    s.status_update(status: { state: "TASK_STATE_COMPLETED", timestamp: now })
  end

If you never call open, the request is handled as a normal (non-streaming) response.

Typed Event Emitters

The stream exposes one method per Stream Response event type. Each builds a validated schema object and injects the stream’s task_id and context_id automatically — you never pass them per event:

s.task(status: { state: "TASK_STATE_WORKING" })

s.message(message_id: "m1", role: "ROLE_AGENT", parts: [{ text: "Hello" }])

s.status_update(status: { state: "TASK_STATE_COMPLETED", timestamp: now })

s.artifact_update(
  artifact:   { artifact_id: "a1", name: "output.txt", parts: [{ text: "..." }] },
  append:     false,
  last_chunk: true,
)

Streaming Artifacts in Chunks

Chunk semantics: append: false creates the artifact, append: true extends it, last_chunk: true closes it. From the streaming-artifacts example:

in "SendStreamingMessage"
  env["a2a.stream"].open(task_id: task_id, context_id: context_id) do |s|
    s.task(status: { state: "TASK_STATE_WORKING", timestamp: now })

    artifact_id = SecureRandom.uuid

    chunks.each_with_index do |chunk_text, i|
      s.artifact_update(
        artifact: {
          artifact_id: artifact_id,
          name:        "output.txt",
          parts:       [{ text: chunk_text }],
        },
        append:     i > 0,
        last_chunk: i == chunks.length - 1,
      )
    end

    s.status_update(status: { state: "TASK_STATE_COMPLETED", timestamp: now })
  end

Status updates can be interleaved between artifact chunks to report progress.

Subscribe to Task Updates

SubscribeToTask is just another operation — open a stream and relay whatever event source your app maintains (state management is your application’s concern):

in "SubscribeToTask"
  id   = env["a2a.request"].id
  task = TASKS[id] or raise A2A::TaskNotFoundError.new(id)

  env["a2a.stream"].open(task_id: id, context_id: task["contextId"]) do |s|
    # Initial snapshot
    s.task(status: task["status"])

    # Relay live events from your own pub/sub (e.g. an Async::Queue per task)
    while (event = SUBSCRIPTIONS[id].dequeue)
      s.status_update(status: event["status"])
      break if TERMINAL_STATES.include?(event.dig("status", "state"))
    end
  end

Consuming a Stream from the Client

Streaming client methods take a block; each SSE event is parsed into a validated Stream Response object:

client = A2A::Client.new("http://localhost:9292")

client.send_streaming_message(
  message: { message_id: "m1", role: "ROLE_USER", parts: [{ text: "Hello" }] }
) do |event|
  event.task            # set on task snapshot events
  event.status_update   # set on status update events
  event.artifact_update # set on artifact update events
  event.message         # set on message events
end

This site uses Just the Docs, a documentation theme for Jekyll.