Every message crossing an A2A boundary is wrapped in a JSON-RPC 2.0 envelope with a small set of A2A-specific fields. Get the envelope right and everything else — task lifecycle, streaming, error propagation — falls out cleanly. Get it wrong and you'll spend weeks debugging protocol mismatches.
Here's the envelope, field by field, with the edge cases the spec doesn't always spell out.
Base structure
{
"jsonrpc": "2.0",
"id": "req-8f2e",
"method": "tasks/send",
"params": {
"id": "task-abc-123",
"sessionId": "sess-def-456",
"message": {
"role": "user",
"parts": [{ "type": "text", "text": "Find flights to Bangalore" }]
}
}
}
Standard JSON-RPC 2.0 on the outside. All A2A semantics live inside params.
The task ID
params.id is the task identifier — chosen by the caller. Two constraints:
- Must be unique within the scope of the receiving agent (typically a UUIDv4).
- Reusing an ID on a second
tasks/sendcall is defined as a resume/update — not a new task.
This is why generating a fresh UUID per new user request matters. Reuse only when you truly mean to update an existing task.
The session ID
params.sessionId groups related tasks. All tasks with the same session ID share conversational context on the server side. It's the A2A equivalent of a chat thread.
Optional — but almost always useful.
Message parts
message.parts is an array. Each part has a type field:
text— inline UTF-8 text.file— a file reference by URL or base64 blob.data— arbitrary JSON payload.
Multiple parts in one message is legal — that's how you send text + attachment together.
Task response envelope
{
"jsonrpc": "2.0",
"id": "req-8f2e",
"result": {
"id": "task-abc-123",
"sessionId": "sess-def-456",
"status": { "state": "completed" },
"artifacts": [
{ "name": "flights",
"parts": [{ "type": "data", "data": {"flights": [...] }}]}
]
}
}
The response echoes the request id, includes updated task state, and (for completed tasks) an artifacts array holding results.
Edge cases the spec skims
- Very long tasks may return
state: 'submitted'immediately, then require polling or webhook. - Errors are returned via JSON-RPC
error, not embedded inresult. - Streaming replaces the single response with a chunked
tasks/sendSubscribestream. - Reusing a request
idacross different tasks is undefined behavior. Don't.