Prerequisites
- An existing agent (see Create an agent)
- An access token or client credentials
- Understanding of the task lifecycle and A2A protocol
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
Stream a new message
Usemessage: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 sendsid and data lines. The event and retry fields are declared for forward compatibility but are not sent:
Event types
Each SSE event carries anA2AStreamResponse with exactly one field:
Token-by-token streaming
As the model generates its answer, the answer text streams as a sequence ofartifactUpdate 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-streamingmessage: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 aTaskArtifactUpdateEvent. 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 ofartifactUpdate events sharing one artifactId:
- First delta:
appendisfalse(or omitted, which meansfalse),lastChunkisfalse, onetextpart is the first token fragment. - Each subsequent delta:
appendistrue,lastChunkisfalse, onetextpart is the next token fragment. - Closing event:
lastChunkistrue. There are two close shapes (see Closing the stream).
append: truemeans append the incoming parts to the existing artifact buffer for thatartifactId.append: falsemeans 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: truecloses one artifact, not the run. A run may emit severallastChunk: trueartifacts.
Full event sequence for a completed answer
For an orchestrator that streams"The ", "contract ", "answer." and then completes:
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 withlastChunk: true:
- Completion close (replace). When the run completes (either via the orchestrator’s completion tool or a direct-prose finish), a single
artifactUpdatewithappend: false,lastChunk: truecarries 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 sameartifactId. - 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_requiredorfailed), anartifactUpdatewithappend: true,lastChunk: true, an emptytextpart, andartifact.metadata: { "type": "intermediate-thinking" }closes the streamed narration. A freshartifactIdis then generated for the next narration or completion.
Intermediate thinking
AnartifactUpdate 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:
How the client knows the stream ended
The stream is closed by two independent signals:- Terminal
statusUpdatewith 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 statesTASK_STATE_INPUT_REQUIREDandTASK_STATE_AUTH_REQUIRED, which are not terminal (the task can be resumed) but still end the current SSE stream. lastChunk: trueon anartifactUpdatecloses one artifact, not the run. A run may emit several artifacts, each closed with its ownlastChunk: true.
statusUpdate arrives; use lastChunk to know when an individual artifact is complete.
Where the answer lives
On the v1.0 (A2A 1.0) surface, aCOMPLETED 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.
Citations on streamed answers
When the orchestrator’s answer includes inline citations, the completion artifact’s text part carriesmetadata.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 ininput_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 thetext of every artifactUpdate part for a given artifactId, in arrival order:
- Treat
appendas the merge signal: append parts forappend: true; replace the whole artifact forappend: false. - Skip empty
textparts 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
textof everyartifactUpdatepart for a givenartifactId, in arrival order. That concatenation is the streamed text. - Treat
appendas the merge signal.append: trueappends to the existing artifact buffer;append: falsereplaces it (this is how the completion event collapses deltas into one part). - Use
lastChunk: trueto know an artifact is finished, not that the run is finished. - Keep consuming until the closing
statusUpdatewith 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, aCOMPLETEDstatusUpdatecarries no answer message. - Honour
artifact.metadata.type: "intermediate-thinking". An artifact taggedintermediate-thinkingis pre-tool narration, not the final answer. - Handle
input_required/failedwith prior narration. A streamed partial may arrive and be closed asintermediate-thinkingimmediately before aninput_requiredorfailedterminal 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: trueends the stream. It closes one artifact; the closingstatusUpdatecloses the run. - Don’t read the answer from the
COMPLETEDterminalstatusUpdate.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-blockingmessage:send or after a stream dropped), use tasks:subscribe to stream updates:
statusUpdate.
Resumption with Last-Event-ID
If an SSE stream drops, the intended resumption mechanism is to send theLast-Event-ID header with the most recent event ID you received:
message:stream and tasks:subscribe.
Cancel a task
Cancel an in-flight task:TASK_STATE_CANCELED.
JSON-RPC streaming binding
You can also stream via the JSON-RPC binding by sending amessage/stream method to the base A2A endpoint:
A2AStreamResponse objects.
Next steps
- Read about the task lifecycle for state transitions
- Learn how to render inline citations on streamed answers
- Learn how to send a message without streaming
- Understand the A2A protocol bindings