Skip to main content
This guide shows you how to stream agent responses in real time using Server-Sent Events (SSE). You will learn about the two streaming endpoints, the SSE event format, token-by-token streaming of the answer as artifact chunks, and how to resume interrupted streams.

Prerequisites

When to stream

Use streaming when you need real-time updates as the agent processes a message:
  • Showing incremental progress to users
  • Displaying partial results as they arrive
  • Monitoring long-running tasks
For simple request/response interactions, use message:send instead.

Stream a new message

Use message:stream to send a message and receive the response as an SSE stream:
The -N flag disables cURL’s output buffering, which is necessary for receiving SSE events in real time.

SSE event format

Each event follows the W3C SSE wire format. The server sends id and data lines. The event and retry fields are declared for forward compatibility but are not sent:

Event types

Each SSE event carries an A2AStreamResponse with exactly one field:

Token-by-token streaming

As the model generates its answer, the answer text streams as a sequence of artifactUpdate events (one text fragment per event) rather than as status updates. This is the token-by-token streaming model: the answer is a first-class artifact, and each token delta is one chunk of that artifact.

Why the answer streams as an artifact

A2A models the deliverable of a task as an artifact, not as a sequence of working status messages. Streaming the answer into the artifact keeps it consistent with the non-streaming message:send result and with tasks/{id}/artifacts/{artifactId} retrieval. Streamed deltas are later consolidated into a single text part by a closing event, so the final artifact is clean rather than one part per token.

The delta event

Each token delta is a TaskArtifactUpdateEvent. Its JSON shape (REST SSE; the JSON-RPC binding wraps it under result):

Append and lastChunk semantics

The stream for one narration is a sequence of artifactUpdate events sharing one artifactId:
  1. First delta: append is false (or omitted, which means false), lastChunk is false, one text part is the first token fragment.
  2. Each subsequent delta: append is true, lastChunk is false, one text part is the next token fragment.
  3. Closing event: lastChunk is true. There are two close shapes (see Closing the stream).
  • append: true means append the incoming parts to the existing artifact buffer for that artifactId.
  • append: false means replace the whole artifact with the incoming parts. This is how the completion event collapses many per-delta parts into one canonical text part.
  • lastChunk: true closes one artifact, not the run. A run may emit several lastChunk: true artifacts.

Full event sequence for a completed answer

For an orchestrator that streams "The ", "contract ", "answer." and then completes:
The first delta omits append (false); deltas 2 and 3 set append: true. The closing event has lastChunk: true and append omitted (false = replace), carrying the full consolidated text in one part.

Closing the stream

There are two close shapes, both with lastChunk: true:
  • Completion close (replace). When the run completes (either via the orchestrator’s completion tool or a direct-prose finish), a single artifactUpdate with append: false, lastChunk: true carries the canonical answer text as one text part, with any cited data parts as sibling data parts in the same artifact. The per-delta parts collapse into this single part. If the streamed text already equals the completion text, this is the only close event and the artifact keeps the same artifactId.
  • Intermediate-thinking close. When the model streamed narration but the turn does not complete with that text (a tool call follows, or the terminal is input_required or failed), an artifactUpdate with append: true, lastChunk: true, an empty text part, and artifact.metadata: { "type": "intermediate-thinking" } closes the streamed narration. A fresh artifactId is then generated for the next narration or completion.

Intermediate thinking

An artifactUpdate with artifact.metadata.type: "intermediate-thinking" is the model’s pre-tool narration, not the final answer. It occurs when the model writes some text and then calls a tool, or when the run ends in input_required or failed after narration. A subsequent completion artifact (different artifactId, no type metadata) holds the real answer. Streamed text then a tool call produces two artifacts:
Do not render an intermediate-thinking artifact as the deliverable. It is the model’s reasoning before a tool call. The final answer is the subsequent artifact without the intermediate-thinking metadata.

How the client knows the stream ended

The stream is closed by two independent signals:
  1. Terminal statusUpdate with a terminal task state (TASK_STATE_COMPLETED, TASK_STATE_FAILED, TASK_STATE_CANCELED, TASK_STATE_REJECTED). This is the authoritative end of the run; the SSE transport closes after it. The stream also closes on the interrupted states TASK_STATE_INPUT_REQUIRED and TASK_STATE_AUTH_REQUIRED, which are not terminal (the task can be resumed) but still end the current SSE stream.
  2. lastChunk: true on an artifactUpdate closes one artifact, not the run. A run may emit several artifacts, each closed with its own lastChunk: true.
Keep consuming until the terminal or interrupted statusUpdate arrives; use lastChunk to know when an individual artifact is complete.

Where the answer lives

On the v1.0 (A2A 1.0) surface, a COMPLETED statusUpdate carries no answer message. It carries the final state, but the answer text is in the artifact, specifically the completion artifact whose lastChunk: true event preceded the terminal status.
Do not read the answer from the COMPLETED terminal statusUpdate.status.message. On the v1.0 surface it is absent for completed runs. Read the answer from the preceding lastChunk: true completion artifact.

Citations on streamed answers

When the orchestrator’s answer includes inline citations, the completion artifact’s text part carries metadata.citations: [...], and the cited data parts carry their GID in metadata.dataPartId. Join all text parts of the completion artifact in order before applying citation offsets.

Terminal states with prior narration

If the model streamed text and then ended in input_required or failed, the streamed narration is closed as an intermediate-thinking artifact immediately before the terminal status. The terminal statusUpdate carries the question or failure text as its status.message (for non-completed terminals). Do not treat the intermediate-thinking partial as the answer. If the LLM transport errors mid-narration, the streamed partial is closed as an intermediate-thinking artifact before the terminal FAILED status is emitted.

Reconstruct the text

To reconstruct the streamed text, concatenate the text of every artifactUpdate part for a given artifactId, in arrival order:
  • Treat append as the merge signal: append parts for append: true; replace the whole artifact for append: false.
  • Skip empty text parts on append (the intermediate-thinking close carries {"text": ""}).
  • Deltas are an ordered byte stream. Do not assume a delta ends on a character boundary: a delta may end mid-rune. If you decode deltas independently, buffer incomplete UTF-8 at the byte level, then decode.

Consume the stream: best practices

Do

  • Concatenate the text of every artifactUpdate part for a given artifactId, in arrival order. That concatenation is the streamed text.
  • Treat append as the merge signal. append: true appends to the existing artifact buffer; append: false replaces it (this is how the completion event collapses deltas into one part).
  • Use lastChunk: true to know an artifact is finished, not that the run is finished.
  • Keep consuming until the closing statusUpdate with a terminal state (TASK_STATE_COMPLETED, TASK_STATE_FAILED, TASK_STATE_CANCELED, TASK_STATE_REJECTED) or an interrupted state (TASK_STATE_INPUT_REQUIRED, TASK_STATE_AUTH_REQUIRED). That event ends the SSE stream.
  • Read the final answer from the completion artifact, not from the terminal statusUpdate. On v1.0, a COMPLETED statusUpdate carries no answer message.
  • Honour artifact.metadata.type: "intermediate-thinking". An artifact tagged intermediate-thinking is pre-tool narration, not the final answer.
  • Handle input_required / failed with prior narration. A streamed partial may arrive and be closed as intermediate-thinking immediately before an input_required or failed terminal status.

Don’t

  • Don’t render each delta as the final answer. Deltas are fragments; the canonical answer is the consolidated/replaced artifact. Rendering deltas verbatim is fine for a live “typing” UI, but the stored answer must be the consolidated text.
  • Don’t assume one artifact per run. Intermediate-thinking plus completion can yield two or more artifacts with different artifactIds.
  • Don’t assume lastChunk: true ends the stream. It closes one artifact; the closing statusUpdate closes the run.
  • Don’t read the answer from the COMPLETED terminal statusUpdate.status.message. On v1.0 it is absent for completed runs.
  • Don’t ignore append. Treating the completion replace event as another append duplicates the answer.
  • Don’t split or re-decode deltas on assumed rune boundaries. Concatenate the raw strings first.

Subscribe to an existing task

If you have a task ID (from a non-blocking message:send or after a stream dropped), use tasks:subscribe to stream updates:
Subscribing to an in-progress task replays the events you would have received from the start of the current artifact, then continues with live updates until the terminal statusUpdate.

Resumption with Last-Event-ID

The server writes event IDs but does not yet read the Last-Event-ID request header, so resumption without gaps is not implemented. The section below describes the intended design for when this feature ships.
If an SSE stream drops, the intended resumption mechanism is to send the Last-Event-ID header with the most recent event ID you received:
The server will replay events from that point forward once the feature is implemented. This will work with both message:stream and tasks:subscribe.

Cancel a task

Cancel an in-flight task:
The task transitions to TASK_STATE_CANCELED.

JSON-RPC streaming binding

You can also stream via the JSON-RPC binding by sending a message/stream method to the base A2A endpoint:
The SSE events carry JSON-RPC response envelopes instead of raw A2AStreamResponse objects.

Next steps