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.

Advertisement

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/send call 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.

Advertisement

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 in result.
  • Streaming replaces the single response with a chunked tasks/sendSubscribe stream.
  • Reusing a request id across different tasks is undefined behavior. Don't.
Fix the envelope in your head before you touch task state — every A2A conversation starts here.