A2A streaming and the task lifecycle: SSE progress updates, artifacts, and push notifications
When an agent takes thirty seconds or five minutes to finish, a plain request/response call is the wrong shape: the connection idles through proxies that time out, the user sees no progress, and a retry cannot tell wheth
When an agent takes thirty seconds or five minutes to finish, a plain request/response call is the wrong shape: the connection idles through proxies that time out, the user sees no progress, and a retry cannot tell whether the work already happened. The Agent2Agent (A2A) protocol addresses this by making the unit of work a task with a visible state machine, and by offering three ways to observe it, a blocking call, a streaming call over Server-Sent Events, and push notifications for when the client cannot hold a connection open. The exact method names and fields are versioned in the A2A specification, so pin the release you implement against; the lifecycle and event model described here are the stable core.
The task state machine
Every method call creates or resumes a task whose status moves through a small, explicit set of states:
| State | Meaning | What the client does |
|---|---|---|
submitted |
Accepted, work not started | Wait for updates |
working |
Actively processing | Show progress, keep listening |
input-required |
Agent needs more information (human in the loop) | Prompt the user, then send another message on the same task |
completed |
Terminal; artifacts are ready | Read the result |
failed |
Terminal; an error explains why | Surface the error, optionally retry as a new task |
canceled |
Terminal; the client or system canceled it | Stop |
unknown |
State cannot be determined | Poll or query the task |
Two states make this more than a job queue. input-required turns the task into a conversation: the agent pauses, the client supplies the missing information, and the same task resumes. working is not a single hop; the agent may emit many status messages and partial artifacts while in it.
A task carries an id, a contextId that groups a multi-turn exchange, a status (with a timestamp and a message), and artifacts. Artifacts hold the actual output as ordered parts, text parts, file parts, and structured data parts, which lets an agent stream a document chunk by chunk and attach generated files.
Three invocation styles
A2A is JSON-RPC 2.0 over HTTP. The same logical send has a blocking and a streaming variant:
-
message/sendsubmits a message and returns the task once it reaches a terminal or interaction state. Simple, but no progress for long work. -
message/streamsubmits (or continues) a task and opens atext/event-stream; the server emits status and artifact updates as they happen, ending with a final event. -
tasks/get,tasks/cancelfetch the current task and cancel it. -
tasks/pushNotification/set/getregister and read a push channel for clients that cannot keep an SSE connection open.
Streaming over SSE
message/stream responds with Content-Type: text/event-stream. Each SSE data: frame carries a JSON-RPC message, either a status update or an artifact update:
POST / HTTP/1.1
Content-Type: application/json
Accept: text/event-stream
{
"jsonrpc": "2.0",
"id": "req-1",
"method": "message/stream",
"params": {
"message": {
"role": "user",
"parts": [{ "kind": "text", "text": "Draft a migration plan for our billing service." }],
"messageId": "m-1"
}
}
}
The event stream communicates progress first, then output:
event: status-update
data: {"taskId":"t-9","contextId":"c-2","status":{"state":"working","timestamp":"2026-10-08T10:00:01Z","message":{"role":"agent","parts":[{"kind":"text","text":"Inventorying current endpoints..."}]}}}
event: status-update
data: {"taskId":"t-9","contextId":"c-2","status":{"state":"working","timestamp":"2026-10-08T10:00:12Z","message":{"role":"agent","parts":[{"kind":"text","text":"Drafting the phased rollout..."}]}}}
event: artifact-update
data: {"taskId":"t-9","artifact":{"artifactId":"a-1","name":"migration-plan.md","parts":[{"kind":"text","text":"# Migration plan\\n\\n## Phase 1 ..."}],"lastChunk":true},"append":true}
event: status-update
data: {"taskId":"t-9","contextId":"c-2","status":{"state":"completed","timestamp":"2026-10-08T10:00:41Z"},"final":true}
The client renders the working messages as a progress feed, appends artifact chunks as they arrive, and tears down the stream only when it sees final: true on a terminal state. Because updates are framed as JSON-RPC messages, the same envelope works for transport errors and for the input-required pause; the client does not need a second protocol to handle clarifying questions.
SSE is the right default for streaming because it is unidirectional server-to-client, traverses ordinary HTTP infrastructure, and reconnects with standard semantics. Use it rather than WebSocket when the client only needs to receive progress and sends new input by making another request.
Push notifications for intermittent clients
A serverless function, a mobile app in the background, or a workflow engine cannot hold an SSE socket open for five minutes. Push notifications invert the delivery: the client registers a callback and the server POSTs updates to it.
{
"jsonrpc": "2.0",
"id": "req-2",
"method": "tasks/pushNotification/set",
"params": {
"taskId": "t-9",
"pushNotificationConfig": {
"url": "https://client.example.com/a2a/callback",
"token": "<signed JWT the server presents on callback>",
"authentication": { "schemes": ["Bearer"] }
}
}
}
The server then calls the registered URL with task status update events, typically on terminal states and optionally on progress milestones. The token lets the client authenticate that the callback genuinely comes from the agent, which matters because the callback is an inbound request to the client's own infrastructure. This is the same webhook reliability problem as any async API: document retries, signing, and which states trigger a push, and keep tasks/get as the reconciliation path if a push is missed.
Choosing the delivery mode
| Client situation | Use |
|---|---|
| Fast task, simple integration |
message/send (blocking) |
| Long task, wants live progress, can hold a connection |
message/stream (SSE) |
| Background/mobile/serverless, cannot hold a socket | Push notifications + tasks/get
|
| Recovering after a disconnect |
tasks/get to resync, then resume streaming |
A robust client treats these as composable: it streams when it can, registers a push channel when it cannot, and always knows how to poll tasks/get to reconcile state after a dropped connection. The task id and context id are the correlation keys across all three, which is why resuming does not duplicate work.
Modeling it over HTTP
Document the JSON-RPC endpoint, the SSE response media type, and the task schema explicitly so generated clients and renderers understand the dual response:
paths:
/:
post:
operationId: agentMessageStream
summary: Submit a message and stream task updates over SSE
requestBody:
required: true
content:
application/json:
schema: { $ref: '#/components/schemas/MessageRequest' }
responses:
'200':
description: Server-Sent Events stream of status and artifact updates.
content:
text/event-stream:
schema: { $ref: '#/components/schemas/TaskUpdateEvent' }
'202':
description: Task accepted and queued when the stream is opened lazily.
/tasks/{taskId}:
get:
operationId: getTask
summary: Fetch the current task state and artifacts (reconciliation)
parameters:
- name: taskId
in: path
required: true
schema: { type: string }
responses:
'200':
description: Current task.
content:
application/json:
schema: { $ref: '#/components/schemas/Task' }
Model the task state as the closed enum above, keep artifacts as arrays of typed parts, and document that an input-required status is followed by another message/stream or message/send carrying the same context. The Agent Card served at /.well-known/agent.json advertises which of these capabilities (streaming, push notifications) the agent supports, so clients should discover capabilities there rather than assuming every agent streams.
What this buys agents and integrators
The lifecycle turns "call an agent" from an opaque wait into something observable and resumable: UIs show real progress, workflows persist task ids and reconcile after crashes, human-in-the-loop steps are a first-class state rather than a timeout, and partial artifacts can render before completion. It also maps cleanly onto the patterns already used for long-running REST jobs (202 plus a status resource and webhooks), so an existing HTTP API can be bridged to A2A without inventing a new execution model.
Checklist
- Model agent work as a task with the explicit state machine; treat
input-requiredas a resumable pause on the same context. - Offer
message/sendfor short work andmessage/streamover SSE for long work; end the stream with a terminal,finalevent. - Carry output as artifacts of typed parts, appending chunks in order.
- Provide
tasks/getfor reconciliation after disconnects andtasks/cancelfor explicit cancellation. - Offer push notifications with an authenticated callback for clients that cannot hold a socket; document retries and signing.
- Correlate every mode with task id and context id so resumes never duplicate work.
- Advertise streaming and push capabilities in the Agent Card; pin the A2A spec version you implement.
- Document the SSE media type and task schemas so generated clients handle the streaming response.
Get these right and a five-minute agent run becomes a live, resumable, human-in-the-loop task instead of a spinner and a timeout.
You can model the task lifecycle and SSE responses, generate a typed client, and simulate streaming updates against a mock in one local-first workspace, right in your browser. For wrapping an existing service in this task model, see bridging an existing HTTP API to an A2A agent.
Originally published by Dev.to AI. Aggregated on AIWithGhost for educational purposes β full credit and traffic to the original publisher.