Agent2Agent - A Technical Deep Dive into the Protocol’s Core Logic

May 19, 2025
Jakob Pörschmann
AILLMAgentTechnology

Developer-focused deep dive into the protocol’s architecture, key message types, and practical implementation patterns.

Are you still working yourself, or does your AI Agent already make your living? Just a couple days ago the Y-Combinator backed Startup Firecrawl published a range of job ads for content generation, coding and customer support roles. Nothing special, except that only AI Agents are allowed to apply to these roles. Firecrawl is ready to pay a monthly salary of 5000 USD to the best performing agent. Yes, this is happening and it’s 2025.

Agent focussed job ads by the y-combinator startup firecrawl The popular Y-Combinator Startup Firecrawl is looking to hire your expert agent.

This raises some questions of course. Actually it raises many! One thing is clear though. If AI agents might steal knowledge worker jobs any time soon they must agree on a standardized way of communication. An agent economy cannot mean that every company builds their own agent to solve given challenges. Smart organizations will rather specialize on offering SOTA specialized agentic capabilities. The exact workflows, models and data that an agent uses will often remain behind closed doors. Enter the Agent2Agent (A2A) protocol. Google proposed A2A as an open protocol enabling connections and interoperability between independent Agentic systems. In other words, A2A proposes a communication standard connecting agentic systems independent from the technology they’re built on. Agents are treated as discoverable “black boxes”, which means that the agent author can offer the Agent’s capabilities to others without exposing their background logic. The Agent logic remains opaque.

Remember Remote Procedure Calls (RPC)? Boiled down to the basics, A2A defines an RPC layer for the agentic age.

A2A Technical Foundations

Please note that this article was written mid-may 2025. A2A is rapidly evolving, thus all concepts might still be subject to change.

On the highest level the Agent2Agent Protocol is designed around the Client-Server model. That means, a client agent (usually the agent embedded in the end user application) receives access to the interface of a remote agent. The remote agent exposes a list of functionalities that it is able to offer. If needed the client agent can call the remote agent’s functionalities remotely, without knowing exactly how these functionalities are implemented. All the remote agent shares is the required input and output format to be expected.

Agent2Agent Client-Server-Model The A2A Client-Server-Model allows exposing a remote agent while keeping the processing confidential.

A2A is designed around a set of objects, RPC methods and a memory layer. Core objects are the A2A currency of information exchange. Every instruction and result that an Agent receives or sends can be communicated using these objects.

Core Objects that A2A Servers define:

  • AgentCard
  • Task
  • Artifact
  • Part
  • Message
  • PushNotification

Let’s take a deeper look at the implementation of each of these.

AgentCard

A2A is the information exchange language between agents across organizations. In a world where agents communicate with each other independently the AgentCard is an Agent’s business Card link to the docs. It defines the skills an agent can offer and the endpoint that clients need to call to access them. The AgentCard also includes required authentication methods as well as further communication possibilities such as whether streaming and async processing is supported by this particular agent. To make agents discoverable organizations need to agree on where to host their agent cards. For that the A2A authors propose the universal [base-url]/.well-known/agent.json.

interface AgentCard {
  name: string;
  description?: string | null;
  url: string;
  provider?: AgentProvider | null;
  version: string;
  documentationUrl?: string | null;
  capabilities: AgentCapabilities;
  authentication?: AgentAuthentication | null;
  defaultInputModes?: string[];
  defaultOutputModes?: string[];
  skills: AgentSkill[];
}

Tasks

Tasks are the core object containing work instructions passed from the client to the remote agent link to the docs. They are identified via an unique ID, which is usually generated by the client.

interface Task {
  id: string; // unique identifier for the task
  sessionId: string; // client-generated id for the session holding the task.
  status: TaskStatus; // current status of the task
  artifacts?: Artifact[]; // collection of artifacts created by the agent.
  history?: Message[] | null;
  metadata?: Record<string, any>; // extension metadata
}

Artifacts and Parts

Artifacts contain the content with which the model responded to a given task link to the docs. Aside from a name, a description and metadata an artifact contains Parts link to the docs. Parts are the wrapper for content of any type. Parts are defined as TextPart (containing Text), FileParts (containing file bytes or a reference URI) or DataParts (containing data records of any type).

interface Artifact {
  name?: string;
  description?: string;
  parts: Part[];
  metadata?: Record<string,any>;
}

interface TextPart {
  type: "text";
  text: string;
}

interface FilePart {
  type: "file";
  file: {
    name?: string;
    mimeType?: string;
    // oneof {
    bytes?: string; //base64 encoded content
    uri?: string;
    //}
  }; 
}

interface DataPart {
  type: "data";
  data: Record<string, any>;
}

type Part = (TextPart | FilePart | DataPart) & { metadata: Record<string, any> }

Messages

Messages track the conversation between end user and client agent link to the docs. Messages can be registered in the role of the agent of the user. Message content is again defined by a list of Parts.

interface Message {
  role: "user" | "agent";
  parts: Part[];
  metadata?: Record<string,any>;
}

PushNotification

Push Notifications are A2As way of implementing async processing. A2A projects a future in which remote agents take over tasks that can take hours, days or longer. In these scenarios, we do not want to maintain a HTTP connection to wait for the respective response. Instead we want to assign the agent a task, close the connection and wait for the agent to notify us once the processing has been completed. A2A Push Notifications make that possible. To enable Push Notifications the client sends a PushNotificationConfig together with the task to be assigned to the remote agent. The remote agent processes the request and once the Task processing has been completed makes a POST request to an URL that the client provided in the PushNotificationConfig. The PushNotificationConfig contains a token which identifies the task. The remote server returns this token together with the PushNotification so that the client knows which task has been completed.

interface PushNotificationConfig {
 url: string;
 token?: string;
 authentication?: AuthenticationInfo | null;
}

The AgentCard allows agents to indicate whether PushNotification after async processing is supported by a given agent.

A2A RPC Methods

Methods are the actions that clients can rely on to interact with the server and objects. They mostly revolve around sending tasks to the remote agent and instructing it how to respond.

Main A2A RPC Methods:

  • tasks/send (submit a new task sync or async)
  • tasks/sendSubscribe (submit a task via streaming connection)
  • tasks/get (get task status)
  • tasks/cancel (cancel task)
  • tasks/pushNotification/set (set push notification config for task)
  • tasks/pushNotification/get (get all push notification for task)
  • tasks/resubscribe (reopen closed http connection in synchronous or streaming case)

Let’s take a look at the most common RPC workflows given these methods. Source is the A2A documentation.

Synchronous Task Processing with A2A

In the synchronous processing case the client agent passes a task UID, Session ID and message to the remote agent. As in any standard HTTP request the client waits for the fully processed response from the remote agent.

Client Agent Request:

{
  "jsonrpc": "2.0",
  "id": "req-001",
  "method": "tasks/send",
  "params": {
    "id": "task-abc-123",
    "sessionId": "session-xyz-789",
    "message": {
      "role": "user",
      "parts": [
        {
          "type": "text",
          "text": "What is the capital of France?"
        }
      ]
    }
  }
}

Given the received message the remote agent identifies the right handler (tasksSendHandler) and checks for an async processing config (which doesn’t exist in this case). According to that the remote agent passes the request through the agent logic. Once the request is processed the remote agent packages the response in an artifact and returns the response to the client as follows. The result produced by the agent is wrapped in an artifact. The client agent can easily unwrap this one send it to the end user as an agent message.

Remote Agent Response:

{
  "jsonrpc": "2.0",
  "id": "req-001",
  "result": {
    "id": "task-abc-123",
    "sessionId": "session-xyz-789",
    "status": {
      "state": "completed",
      "message": {
        "role": "agent",
        "parts": [
          {
            "type": "text",
            "text": "The capital of France is Paris."
          }
        ]
      },
      "timestamp": "2024-03-15T10:00:05Z"
    },
    "artifacts": [
      {
        "name": "Answer",
        "index": 0,
        "parts": [
          {
            "type": "text",
            "text": "The capital of France is Paris."
          }
        ]
      }
    ]
  }
}

Asynchronous task processing using Push Notifications with A2A

Asynchronous processing is done via push notifications that the remote agent sends proactively. To make that possible the client needs to provide a PushNotificationConfig. Ideally, this is done together the initial task submission. If a PushNotificationConfig is provided the remote agent returns an initial acknowledgement, confirming that the Task has been submitted successfully and that a Notification on completion will follow.

A task request with PushNotificationConfig could look as follows:

{
  "jsonrpc": "2.0",
  "id": "req-005",
  "method": "tasks/send",
  "params": {
    "id": "task-reportgen-aaa",
    "message": {
      "role": "user",
      "parts": [
        {
          "type": "text",
          "text": "Generate the Q1 sales report. This usually takes a while. Notify me when it's ready."
        }
      ]
    },
    "pushNotification": {
      "url": "https://client.example.com/webhook/a2a-notifications",
      "token": "secure-client-token-for-task-aaa",
      "authentication": {
        "schemes": ["Bearer"]
        // Assuming server knows how to get a Bearer token for this webhook audience,
        // or this implies the webhook is public/uses the 'token' for auth.
        // 'credentials' could provide more specifics if needed by the server.
      }
    }
  }
}


{
  "jsonrpc": "2.0",
  "id": "req-005",
  "result": {
    "id": "task-reportgen-aaa",
    "status": { "state": "submitted", "timestamp": "2024-03-15T11:00:00Z" }
    // ... other fields ...
  }
}

Once the Remote Agent completed the task processing it could return a message that looks as follows. The Push Notification payload needs to include the taskID for the client to know which exact message this notification is about. The client needs to implement a task tracking system, to automate the tasks/get calls once the PushNotification has been received.

Sample Push Notification payload:

{
  "eventType": "taskUpdate",
  "taskId": "task-reportgen-aaa",
  "status": { "state": "completed", "timestamp": "2024-03-15T18:30:00Z" },
  "summary": "Q1 sales report generated successfully."
  // Server MAY include more details or a link to fetch the full task.
}

Bringing things together - The A2A Architecture

After discussing Objects and methods, let’s take a global look at the A2A Server logic. Given an incoming request our A2A server first checks for the type of request. The request is instantly routed to the respective A2A handler method.

This is necessary as a tasks/send request will require an entirely different processing logic compared to the tasks/sendSubscribe request. The tasks/Send handler needs to check for PushNotificationConfig and send a response accordingly. The tasks/sendSubscribe handler on the other hand needs to open a SSE stream and stream the responses in chunks as they are generated.

The Memory layer keeps track of queued messages, their status as well as methods of communicating the processed results.

Memory Layer responsibilities:

  • Task Queue
  • PushNotification Endpoint tracking

Agent2Agent Internal Architecture Overview A2A Server are designed around the TaskStore/TaskManager and the method handler.

Connecting your agent via the handler methods

Here is an extract from one of the official A2A sample implementations direct Github Link. The AgentTaskManager is part of the ADK based Remote Agent implementation. As you can see the Agent implements the handler (on_send_task) and TaskManager (Memory Layer). The example makes use of an InMemoryTaskManager. It defines the handler method as on_send_task, which calls the workflow to actually invoke the ADK defined Agent. As you can read from the example below the _invoke() method is the heart of the A2A implementation of the given example agent. The _invoke() method manages unpacking the task object via _get_user_query(), sends the user query to the agent via agent.invoke() and updates the task store with the new TaskStatus, Message and Artifact.

class AgentTaskManager(InMemoryTaskManager):
    def __init__(self, agent: AgentWithTaskManager):
        super().__init__()
        self.agent = agent

[...]

    async def on_send_task(self, request: SendTaskRequest) -> SendTaskResponse:
        error = self._validate_request(request)
        if error:
            return error
        await self.upsert_task(request.params)
        return await self._invoke(request)

[...]

    async def _invoke(self, request: SendTaskRequest) -> SendTaskResponse:
        task_send_params: TaskSendParams = request.params
        query = self._get_user_query(task_send_params)
        try:
            result = self.agent.invoke(query, task_send_params.sessionId)
        except Exception as e:
            logger.error(f'Error invoking agent: {e}')
            raise ValueError(f'Error invoking agent: {e}')
        parts = [{'type': 'text', 'text': result}]
        task_state = (
            TaskState.INPUT_REQUIRED
            if 'MISSING_INFO:' in result
            else TaskState.COMPLETED
        )
        task = await self._update_store(
            task_send_params.id,
            TaskStatus(
                state=task_state, message=Message(role='agent', parts=parts)
            ),
            [Artifact(parts=parts)],
        )
        return SendTaskResponse(id=request.id, result=task)

    def _get_user_query(self, task_send_params: TaskSendParams) -> str:
        part = task_send_params.message.parts[0]
        if not isinstance(part, TextPart):
            raise ValueError('Only text parts are supported')
        return part.text

That’s it, congratulations. If you read until here you do have a good grasp of how the Agent2Agent protocol works under the hood. The example code above in combination with the pre-defined RPC methods are heart pieces of A2A.

A major question that might ponder around your mind: “How the hell does this differ to MCP?!”. Stay tuned for my next article in a couple days on a detailed comparison on A2A and MCP functionalities and patterns.