Discover the two ways agents report task progress. We dissect streaming via Server-Sent Events on message/sendSubscribe, then push notifications with pushNotificationConfig, complete with retry strategies and polling fallback.

In episode 6 you built real agents with a2a-sdk for Python and @a2a-js/sdk for TypeScript. Both SDKs already carry streaming and push notification features, but we've only touched the surface. This time we'll tear both down to the wire level: how task events flow in real time over SSE, how the server notifies the client through webhooks, and how the system keeps working when a connection or webhook goes wrong.
This episode's roadmap: first we compare the two communication modes — blocking and streaming. Then we look at the SSE flow on the wire, streaming practice in both SDKs, push notification configuration, and finally the retry strategies and polling fallback.
Since episode 3 we've known message/send as the easiest way to send a task. This method is blocking: the client sends a request, the server processes it, then returns the final task in one HTTP response. Convenient for quick jobs, but problematic for long-running tasks — imagining an LLM thinking for two minutes with no word at all feels like staring at a loading screen that never finishes.
The alternative is streaming through the message/sendSubscribe method. In this mode the server opens an SSE connection and streams an event each time there's progress: statuses change, progress messages arrive, partial artifacts take shape, until the task completes. The comparison:
message/send): one request, one final response. Simple, suitable for short tasks.message/sendSubscribe): one open connection, many events. Real-time, suitable for long tasks.tasks/get) as a middle ground without a persistent connection.Info
An agent that exposes streaming isn't obligated to be consumed via streaming by every client. The A2A specification lets clients choose the mode per task — even switching modes mid-flight for the same task. This means one server can serve both simple clients and clients that need real-time progress.
SSE (Server-Sent Events) is a simple HTTP mechanism: the server writes data: blocks to a connection that stays open, separated by blank lines, and the client reads them as a stream. Each block in A2A contains a JSON-RPC notification or a final response. Here's an example of the stream a client receives while a task is being processed:
data: {"jsonrpc":"2.0","method":"notifications/stream","params":{"kind":"status-update","taskId":"t-102","contextId":"c-7","status":{"state":"working","message":{"role":"agent","parts":[{"kind":"text","text":"Mengumpulkan data dari 3 sumber..."}]}},"final":false}}
data: {"jsonrpc":"2.0","method":"notifications/stream","params":{"kind":"artifact-update","taskId":"t-102","contextId":"c-7","artifact":{"name":"parsial","parts":[{"kind":"text","text":"Sumber 1: 12 referensi"}]},"append":true,"lastChunk":false}}
data: {"jsonrpc":"2.0","id":1,"result":{"kind":"task","id":"t-102","contextId":"c-7","status":{"state":"completed"},"artifacts":[{"name":"laporan","parts":[{"kind":"text","text":"Hasil akhir: 42 poin"}]}]}}Note the three event types the client recognizes:
status-update with final: false — intermediate statuses like working, plus an optional progress message.artifact-update — artifact fragments appended incrementally, marked with lastChunk.task with the same id — the final task that closes the stream.Conceptually this is what the chain of update_status and add_artifact calls you wrote in episode 6 produces. To see it directly without the SDK, you can join the stream with curl -N:
curl -N -X POST http://localhost:9999/ \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"message/sendSubscribe","params":{"message":{"role":"user","parts":[{"kind":"text","text":"Analisis tren"}]}}}'With -N, curl doesn't wait for the stream to end — every data: block is printed the moment it arrives. Handy for quick debugging before touching the SDK.
The SDKs hide all the SSE formatting above. In Python, A2AClient provides send_message_subscribe, which returns an async iterator; in TypeScript, sendMessageStream returns an async generator:
import asyncio
from a2a.client import A2AClient
from a2a.types import Task, TaskStatusUpdateEvent
async def main() -> None:
async with A2AClient(url="http://127.0.0.1:9999") as client:
async for event in client.send_message_subscribe(
message={"role": "user", "parts": [{"kind": "text", "text": "Analisis tren"}]}
):
if isinstance(event, TaskStatusUpdateEvent):
print(f"[status] {event.status.state}")
if event.status.message:
print(event.status.message.parts[0].text)
elif isinstance(event, Task):
print(f"[task] {event.id} - {event.status.state}")
asyncio.run(main())The consumption pattern in both is identical: iterate the events, differentiate the type, display. The language difference becomes irrelevant — that's the power of using the official SDKs we emphasized in episode 6.
Streaming requires an open connection, which is hard to satisfy when the client is an orchestrator behind a NAT, load balancer, or a scheduler that only "calls briefly". For those scenarios A2A provides push notifications: the client opens no connection at all, instead telling the server where status reports should be POSTed.
The configuration is sent through the tasks/pushNotificationConfig method. The url parameter is the client's webhook, and token is optional for authentication:
{
"jsonrpc": "2.0",
"id": 2,
"method": "tasks/pushNotificationConfig",
"params": {
"taskId": "t-102",
"pushNotificationConfig": {
"id": "webhook-lead-1",
"url": "https://orchestrator.example.com/hooks/a2a/t-102",
"token": "rahasia-hmac"
}
}
}The flow after that reverses direction. The server executes the task, then every time the status changes it POSTs to the url above with exactly the same payload as the SSE events (status-update, artifact-update, or the final task). The client simply receives the POST at that endpoint, validates the token, and updates its task status. This suits architectures where agents genuinely need active two-way communication, such as a multi-agent orchestrator monitoring dozens of remote agents at once.
Warning
Push notifications make the agent server issue outbound requests to the client's webhook. Make sure that webhook is HTTPS, the token is validated, and the payload carries a unique task identity. Without validation, your webhook endpoint could be abused as a tool for throwing around arbitrary payloads.
Both streaming and push can fail on the network. A stream can drop mid-way; a webhook can go down or respond slowly. A2A doesn't give up at that point — it provides three complementary layers of resilience:
tasks/get. The simplest and always available. The client asks for the task status periodically:curl -s -X POST http://localhost:9999/ \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":3,"method":"tasks/get","params":{"taskId":"t-102"}}'Together, the three give at-least-once delivery: there's no guarantee an event arrives exactly once, but it's guaranteed that no status is permanently lost as long as at least one path succeeds. This kind of design is a must for agents reporting to a production orchestrator.
Episode 7 completes the ways agents report progress. Streaming through message/sendSubscribe provides a real-time event flow over SSE — with the three event types status-update, artifact-update, and task — while push notifications through tasks/pushNotificationConfig reverse the communication direction through webhooks. Beneath both are retry with backoff, stream reconnection, and the tasks/get polling fallback that guarantee no status is lost.
Here's the core takeaway:
message/sendSubscribe opens an SSE connection and streams task events in real time until the final task.status-update (final false), artifact-update (incremental append), and the closing task.tasks/pushNotificationConfig with a url webhook and optional token, with the server actively POSTing.Streaming and push carry rich content — messages, artifacts, partial results — yet so far we've only used plain text. In episode 8 we'll dissect Content & Structured Output: the Part types for text, file, and structured JSON-schema-based data, and how to send structured results between agents without free parsing. See you there!