A2A Protocol Core Architecture: Agent Cards, Tasks, and Message Flow (Part 2 of 8)

A2A Protocol Core Architecture: Agent Cards, Tasks, and Message Flow (Part 2 of 8)

In Part 1, we covered what the A2A protocol is and why the enterprise ecosystem is rallying around it. Now it is time to go under the hood. To build anything real with A2A, you need a solid understanding of its core building blocks: how agents advertise themselves, how messages are structured, how tasks move through their lifecycle, and how streaming works for long-running operations.

This part is specification-level detail, but kept practical. Every concept here maps directly to code you will write in Parts 3 and 4.

The Four Core Building Blocks

The A2A protocol is built around four primary concepts that work together to enable agent-to-agent communication:

flowchart LR
    AC[Agent Card\nCapability Declaration] --> CA[Client Agent\nTask Initiator]
    CA -->|JSON-RPC over HTTP| RA[Remote Agent\nA2A Server]
    RA -->|SSE Stream / Response| CA
    RA --> TL[Task Lifecycle\nsubmitted → working → completed]
  1. Agent Card – the discovery and capability document
  2. Client Agent – the agent that delegates work
  3. Remote Agent (A2A Server) – the agent that executes work
  4. Task Lifecycle – the state machine governing every operation

Let us dig into each one in depth.

Agent Cards: The Discovery Layer

Before any agent can send a task to another agent, it needs to know three things: what the remote agent can do, how to reach it, and how to authenticate. All of this lives in the Agent Card.

An Agent Card is a JSON document served at a well-known URL on the remote agent’s server:

GET https://your-agent.example.com/.well-known/agent.json

Here is a realistic Agent Card for an inventory management agent:

{
  "name": "Inventory Management Agent",
  "description": "Manages stock levels, processes inventory queries, and triggers reorder workflows",
  "version": "1.2.0",
  "url": "https://inventory-agent.example.com/a2a",
  "documentationUrl": "https://inventory-agent.example.com/docs",
  "provider": {
    "organization": "Acme Corp",
    "url": "https://acmecorp.example.com"
  },
  "capabilities": {
    "streaming": true,
    "pushNotifications": true,
    "stateTransitionHistory": true
  },
  "defaultInputModes": ["text", "application/json"],
  "defaultOutputModes": ["text", "application/json"],
  "skills": [
    {
      "id": "check-stock",
      "name": "Check Stock Level",
      "description": "Returns current stock quantity for one or more product SKUs",
      "tags": ["inventory", "stock", "query"],
      "examples": [
        "What is the current stock for SKU-12345?",
        "Check inventory levels for products in category electronics"
      ],
      "inputModes": ["text", "application/json"],
      "outputModes": ["application/json"]
    },
    {
      "id": "trigger-reorder",
      "name": "Trigger Reorder",
      "description": "Initiates a reorder workflow when stock falls below threshold",
      "tags": ["inventory", "reorder", "procurement"],
      "inputModes": ["application/json"],
      "outputModes": ["application/json"]
    }
  ],
  "securitySchemes": {
    "bearer": {
      "type": "http",
      "scheme": "bearer",
      "bearerFormat": "JWT"
    }
  },
  "security": [{ "bearer": [] }]
}

A few things worth noting here. The capabilities block tells the client agent whether this server supports streaming via SSE, push notifications for asynchronous updates, and state transition history for audit purposes. The skills array is how the client agent knows which specific tasks to route here. The securitySchemes follows OpenAPI conventions so any enterprise identity system can integrate cleanly.

Agent Cards can also be signed in A2A v0.3+, which lets client agents cryptographically verify the card has not been tampered with. This is critical for enterprise environments where you cannot trust every network hop.

The Message Structure

A2A uses JSON-RPC 2.0 as its message format. If you have worked with any JSON-RPC API before, this will look familiar. Every request has a method, a params block, and an id for correlation.

There are four primary methods in the A2A spec:

MethodPurpose
tasks/sendSubmit a new task, wait for completion (synchronous)
tasks/sendSubscribeSubmit a task and subscribe to SSE updates (streaming)
tasks/getPoll for the current state of an existing task
tasks/cancelCancel an in-progress task

Here is what a tasks/send request looks like when the client agent asks the inventory agent to check stock:

{
  "jsonrpc": "2.0",
  "method": "tasks/send",
  "id": "req-001",
  "params": {
    "id": "task-7f3a9b2c-1234-4abc-8def-0fedcba98765",
    "message": {
      "role": "user",
      "parts": [
        {
          "type": "text",
          "text": "Check current stock levels for SKU-12345 and SKU-67890"
        }
      ]
    },
    "metadata": {
      "correlationId": "order-workflow-42",
      "requestedBy": "procurement-agent-v2"
    }
  }
}

The id inside params is the Task ID. This is a UUID the client generates and owns. It is used to track this specific unit of work through its entire lifecycle, including polling, streaming updates, and eventual completion or failure.

The message structure mirrors what you know from LLM chat APIs: a role (user, agent) and parts. Parts can be text, file references, or structured JSON data. This flexibility is what makes A2A modality agnostic.

Task Lifecycle: The State Machine

Every task in A2A moves through a defined set of states. Understanding this state machine is essential before you write a single line of implementation code.

stateDiagram-v2
    [*] --> submitted: tasks/send or tasks/sendSubscribe
    submitted --> working: Remote agent starts processing
    working --> input_required: Agent needs more information
    input_required --> working: Client provides additional input
    working --> completed: Task finishes successfully
    working --> failed: Unrecoverable error
    working --> canceled: tasks/cancel received
    submitted --> canceled: tasks/cancel received
    completed --> [*]
    failed --> [*]
    canceled --> [*]

The input_required state is especially important for enterprise workflows. It is how A2A handles human-in-the-loop scenarios. If the remote agent needs approval from a human before proceeding, or needs clarifying information it cannot resolve on its own, it transitions to input_required and waits. The client agent then delivers the additional input and the task resumes.

Here is what the response object looks like when a task completes:

{
  "jsonrpc": "2.0",
  "id": "req-001",
  "result": {
    "id": "task-7f3a9b2c-1234-4abc-8def-0fedcba98765",
    "status": {
      "state": "completed",
      "timestamp": "2026-03-07T08:45:22Z"
    },
    "artifacts": [
      {
        "name": "stock-levels",
        "description": "Current inventory levels for requested SKUs",
        "parts": [
          {
            "type": "application/json",
            "data": {
              "SKU-12345": { "quantity": 240, "location": "Warehouse-A", "reorderThreshold": 50 },
              "SKU-67890": { "quantity": 12, "location": "Warehouse-B", "reorderThreshold": 30, "belowThreshold": true }
            }
          }
        ]
      }
    ],
    "metadata": {
      "correlationId": "order-workflow-42",
      "processingTimeMs": 340
    }
  }
}

The artifacts array is how the remote agent returns its results. An artifact has a name, description, and parts. Parts follow the same structure as message parts, so results can be text, structured JSON, file references, or any combination.

Streaming with Server-Sent Events

For long-running tasks, waiting for a single HTTP response is not practical. A2A uses Server-Sent Events (SSE) for streaming, which gives the remote agent a channel to push state updates and partial results back to the client as they become available.

You trigger streaming by using tasks/sendSubscribe instead of tasks/send. The server then keeps the HTTP connection open and sends events in this format:

data: {"type":"taskStatusUpdate","taskId":"task-7f3a9b2c-...","status":{"state":"submitted","timestamp":"2026-03-07T08:45:00Z"}}

data: {"type":"taskStatusUpdate","taskId":"task-7f3a9b2c-...","status":{"state":"working","timestamp":"2026-03-07T08:45:01Z","message":{"role":"agent","parts":[{"type":"text","text":"Querying warehouse database..."}]}}}

data: {"type":"taskStatusUpdate","taskId":"task-7f3a9b2c-...","status":{"state":"working","timestamp":"2026-03-07T08:45:02Z","message":{"role":"agent","parts":[{"type":"text","text":"Processing SKU-67890 threshold analysis..."}]}}}

data: {"type":"taskArtifactUpdate","taskId":"task-7f3a9b2c-...","artifact":{"name":"stock-levels","index":0,"lastChunk":true,"parts":[{"type":"application/json","data":{...}}]}}

data: {"type":"taskStatusUpdate","taskId":"task-7f3a9b2c-...","status":{"state":"completed","timestamp":"2026-03-07T08:45:22Z"}}

Each SSE event has a type field. taskStatusUpdate carries state transitions and intermediate progress messages. taskArtifactUpdate carries partial or complete result artifacts. The lastChunk flag on artifact updates signals that the artifact is fully delivered.

Here is the full streaming sequence visualized:

sequenceDiagram
    participant CA as Client Agent
    participant RA as Remote Agent (A2A Server)

    CA->>RA: POST /a2a (tasks/sendSubscribe)
    Note over RA: HTTP connection stays open

    RA-->>CA: SSE: taskStatusUpdate (submitted)
    RA-->>CA: SSE: taskStatusUpdate (working, "Querying database...")
    RA-->>CA: SSE: taskStatusUpdate (working, "Analyzing thresholds...")
    RA-->>CA: SSE: taskArtifactUpdate (partial result, lastChunk=false)
    RA-->>CA: SSE: taskArtifactUpdate (final result, lastChunk=true)
    RA-->>CA: SSE: taskStatusUpdate (completed)

    Note over CA,RA: HTTP connection closes

Push Notifications for Disconnected Clients

SSE works well when the client can maintain a persistent connection. But in enterprise environments, clients are often behind firewalls, run as serverless functions, or simply cannot hold a connection open for hours-long tasks.

A2A solves this with push notifications. If the Agent Card declares "pushNotifications": true, the client can register a webhook URL when submitting the task:

{
  "jsonrpc": "2.0",
  "method": "tasks/sendSubscribe",
  "id": "req-002",
  "params": {
    "id": "task-abc123",
    "message": { "role": "user", "parts": [{ "type": "text", "text": "Run quarterly compliance report" }] },
    "pushNotification": {
      "url": "https://my-orchestrator.example.com/webhooks/a2a-callback",
      "token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
      "authentication": {
        "schemes": ["bearer"]
      }
    }
  }
}

The remote agent posts status updates and artifacts to that URL as the task progresses. The client disconnects immediately after submitting and picks up results when its webhook fires. This pattern is essential for tasks that take minutes, hours, or even days.

Multi-Turn Conversations and input_required

The input_required state enables something powerful: multi-turn interactions within a single task. Rather than completing or failing, the remote agent can pause and ask for more information.

When a task enters input_required, the status update includes a message from the agent explaining what it needs:

{
  "type": "taskStatusUpdate",
  "taskId": "task-abc123",
  "status": {
    "state": "input_required",
    "timestamp": "2026-03-07T09:10:00Z",
    "message": {
      "role": "agent",
      "parts": [
        {
          "type": "text",
          "text": "SKU-67890 is below reorder threshold. Reorder quantity options: 100 units ($4,200), 250 units ($9,800), 500 units ($18,000). Please confirm quantity to proceed."
        }
      ]
    }
  }
}

The client agent responds by sending a new message on the same task ID:

{
  "jsonrpc": "2.0",
  "method": "tasks/send",
  "id": "req-003",
  "params": {
    "id": "task-abc123",
    "message": {
      "role": "user",
      "parts": [{ "type": "text", "text": "Proceed with 250 units." }]
    }
  }
}

The task resumes from working state. The entire conversation history for that task is preserved, which is what the stateTransitionHistory capability flag in the Agent Card advertises.

Error Handling

A2A errors follow JSON-RPC 2.0 conventions. Every error has a numeric code, a message, and an optional data block for additional context. The spec defines a set of standard error codes:

CodeMeaning
-32700Parse error (malformed JSON)
-32600Invalid request
-32601Method not found
-32602Invalid params
-32603Internal error
-32001Task not found
-32002Task not cancelable (already in terminal state)
-32003Push notification not supported
-32004Unsupported operation
-32005Incompatible content types

A failed task response looks like this:

{
  "jsonrpc": "2.0",
  "id": "req-001",
  "error": {
    "code": -32603,
    "message": "Internal error",
    "data": {
      "taskId": "task-7f3a9b2c-...",
      "reason": "Warehouse database connection timeout after 30s",
      "retryable": true
    }
  }
}

The retryable field in the data block is a convention worth adopting in your own implementations. It lets the client agent decide whether to retry immediately, back off and retry, or escalate to a human operator.

The Complete Architecture Picture

Putting it all together, here is what a complete A2A interaction looks like from end to end in a multi-agent enterprise system:

flowchart TD
    OA[Orchestrator Agent] -->|1 - GET agent.json| AC1[Inventory Agent Card]
    OA -->|1 - GET agent.json| AC2[Procurement Agent Card]

    OA -->|2 - tasks/sendSubscribe| IA[Inventory Agent]
    IA -->|3 - SSE: working| OA
    IA -->|4 - SSE: input_required| OA
    OA -->|5 - tasks/send confirm qty| IA
    IA -->|6 - SSE: completed + artifact| OA

    OA -->|7 - tasks/sendSubscribe| PA[Procurement Agent]
    PA -->|8 - Webhook callback| OA
    PA -->|9 - Webhook completed + PO artifact| OA

    subgraph Discovery
        AC1
        AC2
    end

    subgraph Execution
        IA
        PA
    end

Transport Security

One thing the spec is explicit about: production A2A deployments must use HTTPS. There is no optional mode here. TLS 1.3 is recommended with strong cipher suites. The client agent must validate the server’s TLS certificate against trusted CAs during the handshake.

Authentication credentials are always passed at the HTTP layer using the schemes declared in the Agent Card’s securitySchemes block. The A2A message body itself never carries credentials. This separation of concerns is intentional and it aligns with how enterprise API gateways and service meshes already handle auth.

What to Remember Before Part 3

Before we start writing code in Part 3, here are the key concepts to have solid in your head:

  • Agent Cards are served at /.well-known/agent.json and describe capabilities, skills, endpoint, and authentication requirements
  • All messages use JSON-RPC 2.0 over HTTP POST
  • Tasks have a unique client-generated UUID and move through a defined state machine
  • Use tasks/send for synchronous tasks, tasks/sendSubscribe for streaming
  • SSE events carry either taskStatusUpdate or taskArtifactUpdate payloads
  • Push notifications let disconnected clients receive results via webhook
  • The input_required state enables human-in-the-loop and multi-turn interactions on a single task
  • Errors follow JSON-RPC 2.0 with A2A-specific codes starting at -32001

In Part 3, we implement a fully functional A2A agent server in Node.js. We will build the Agent Card endpoint, the task handler, SSE streaming, and a basic in-memory task store to track state. By the end of that post you will have a running A2A server you can test against.

References

Written by:

578 Posts

View All Posts
Follow Me :