Skip to content
How to Build Two Python Agents with Google’s A2A Protocol - Step by Step Tutorial

Tutorial: Building Two Python Agents with Google’s A2A Protocol Step by Step

Updated on

Learn how to build two simple Python agents using Google’s A2A protocol. This step-by-step tutorial covers the setup, code examples, and agent communication. A2A is an open standard for agent-to-agent communication, enabling interoperability across different frameworks and vendors.

Google’s Agent-to-Agent (A2A) protocol is an open standard that enables different AI agents to communicate and work together, regardless of which framework or vendor they come from (GitHub - google/A2A (opens in a new tab)). In simpler terms, A2A gives your agents a common language for interaction. This tutorial will walk you through creating two simple Python agents – one acting as an A2A server and one as an A2A client – that communicate with each other using the A2A protocol. We’ll cover the setup, explain how A2A works at a basic level, and provide annotated code examples for both agents. No prior experience with A2A is assumed, and we will avoid advanced concepts like complex toolchains or multi-agent orchestration to keep things beginner-friendly.

What is the A2A Protocol?

At its core, A2A is a protocol (built on HTTP) that standardizes how agents exchange messages, requests, and data. Here are a few key concepts in A2A:

  • Agent Card: A public metadata file (usually hosted at /.well-known/agent.json) describing an agent’s capabilities, skills, endpoint URL, and authentication requirements. Other agents or clients fetch this card to discover what the agent can do (GitHub - google/A2A (opens in a new tab)). It’s essentially the agent’s “business card” in the A2A network.
  • A2A Server: An agent that exposes an HTTP API endpoint implementing A2A methods (for example, endpoints to send tasks). It receives requests and executes tasks on behalf of other agents (GitHub - google/A2A (opens in a new tab)).
  • A2A Client: An application or agent that sends requests to an A2A server’s URL to initiate tasks or conversations (GitHub - google/A2A (opens in a new tab)). In an agent-to-agent scenario, one agent’s client component will call another agent’s server component via A2A.
  • Task: The fundamental unit of work in A2A. A client starts a task by sending a message (using a tasks/send request) to an agent. Each task has a unique ID and goes through a lifecycle of states (e.g. submitted, working, input-required, completed, etc.) (GitHub - google/A2A (opens in a new tab)).
  • Message: A single turn in the communication between the client (role "user") and the agent (role "agent"). The client’s request is a message (from the “user”), and the agent’s answer is a message (from the “agent”) (GitHub - google/A2A (opens in a new tab)).
  • Part: A piece of content within a message. A message is composed of one or more parts. Parts can be text, files, or structured data. For example, a text part (TextPart) carries plain text, whereas a file part might carry an image or other binary data (GitHub - google/A2A (opens in a new tab)). In this tutorial, we’ll stick to simple text parts for clarity.

How A2A Communication Works (Basic Flow): When one agent wants to communicate with another via A2A, the interaction typically follows a standardized flow:

  1. Discovery: The client agent first discovers the other agent by fetching its Agent Card (usually from the server’s /.well-known/agent.json URL) (GitHub - google/A2A (opens in a new tab)). This card tells the client what the agent is called, what it can do, and where to send requests.
  2. Initiation: The client then sends a request to the server agent’s A2A endpoint to start a task – usually via a POST to the tasks/send endpoint. This initial request includes a unique task ID and the first user message (e.g. a question or command) (GitHub - google/A2A (opens in a new tab)).
  3. Processing: The server agent receives the task request and processes it. If the agent supports streaming responses, it may stream intermediate updates back (using Server-Sent Events) as it works, although in our simple example we won’t use streaming. Otherwise, the agent will process the task and send back a final response when done (GitHub - google/A2A (opens in a new tab)). The response will contain the task’s outcome – typically one or more messages from the agent (and optionally artifacts like files if that’s part of the task). For basic text interactions, the final response will just include the agent’s answer message.

To summarize, A2A defines a clear request-response cycle for agents: a client finds an agent’s capabilities (Agent Card), sends it a task (with an initial user message), and receives the agent’s response (as messages and artifacts), all in a consistent JSON format. Now, let’s set up our environment and build a minimal example to see this in action.

Installation and Setup

Before writing any code, make sure you have the required tools installed:

  • Python 3.12 or higher – the A2A samples recommend Python 3.12+ (even Python 3.13) for compatibility (A2A/samples/python/hosts/cli at main · google/A2A · GitHub (opens in a new tab)).
  • UV (Python package manager) – this is an optional but recommended tool by the A2A project for managing dependencies and running the samples (A2A/samples/python/hosts/cli at main · google/A2A · GitHub (opens in a new tab)). UV is a modern package manager; however, if you prefer, you can also use pip/venv to install dependencies manually. In this tutorial, we will keep things simple and use standard Python tools.
  • Google A2A repository – obtain the official A2A code by cloning the GitHub repository:
    git clone https://github.com/google/A2A.git
    This will download the project to your machine. Our tutorial code will reference parts of this repository (particularly the sample A2A client utilities).
  • Dependencies – if you use UV, you can let it handle dependencies automatically. For example, from the repository’s samples/python directory, you can run uv sync (to install deps) or run commands like uv run ... as shown in the official README. If you prefer using pip, ensure you install any libraries needed for our example code:
    pip install flask requests
    The Flask library will be used to create a simple HTTP server for our agent, and requests will be used for the client to call the agent. (The official A2A sample code uses FastAPI/UVICorn and an internal client library, but for a beginner-friendly approach we use Flask and requests for clarity.)

Creating a Simple A2A Server Agent

First, we’ll build the server side: an agent that listens for incoming tasks and responds. Our agent will be extremely simple – it will accept a text prompt and just echo it back with a friendly message. Despite its simplicity, it will follow the A2A protocol structure so that any A2A client can communicate with it.

Agent Capabilities and Agent Card: For our agent to be discoverable, it needs to provide an Agent Card. In a real deployment, we would host a JSON file at /.well-known/agent.json on the server. For our Flask app, we can serve this data via an endpoint. The Agent Card typically includes fields like the agent’s name, description, the URL of its A2A endpoint, supported features (capabilities), etc. (GitHub - google/A2A (opens in a new tab)). We’ll construct a minimal Agent Card with just a few key fields.

tasks/send Endpoint: A2A expects the agent to implement certain API endpoints. The most important is tasks/send, which the client will call (HTTP POST) to send a new task (with a user message) to the agent (GitHub - google/A2A (opens in a new tab)). Our Flask app will define a route for /tasks/send to handle these requests. It will parse the incoming JSON (which contains the task ID and user’s message), process it (in our case, just generate an echo response), and return a JSON response following the A2A format.

Let’s write the code for our simple A2A server agent using Flask:

from flask import Flask, request, jsonify
 
app = Flask(__name__)
 
# Define the Agent Card data (metadata about this agent)
AGENT_CARD = {
    "name": "EchoAgent",
    "description": "A simple agent that echoes back user messages.",
    "url": "http://localhost:5000",  # The base URL where this agent is hosted
    "version": "1.0",
    "capabilities": {
        "streaming": False,           # This agent doesn't support streaming responses
        "pushNotifications": False    # No push notifications in this simple example
    }
    # (In a full Agent Card, there could be more fields like authentication info, etc.)
}
 
# Serve the Agent Card at the well-known URL.
@app.get("/.well-known/agent.json")
def get_agent_card():
    """Endpoint to provide this agent's metadata (Agent Card)."""
    return jsonify(AGENT_CARD)
 
# Handle incoming task requests at the A2A endpoint.
@app.post("/tasks/send")
def handle_task():
    """Endpoint for A2A clients to send a new task (with an initial user message)."""
    task_request = request.get_json()  # parse incoming JSON request
    # Extract the task ID and the user's message text from the request.
    task_id = task_request.get("id")
    user_message = ""
    try:
        # According to A2A spec, the user message is in task_request["message"]["parts"][0]["text"]
        user_message = task_request["message"]["parts"][0]["text"]
    except Exception as e:
        return jsonify({"error": "Invalid request format"}), 400
 
    # For this simple agent, the "processing" is just echoing the message back.
    agent_reply_text = f"Hello! You said: '{user_message}'"
 
    # Formulate the response in A2A Task format.
    # We'll return a Task object with the final state = 'completed' and the agent's message.
    response_task = {
        "id": task_id,
        "status": {"state": "completed"},
        "messages": [
            task_request.get("message", {}),             # include the original user message in history
            {
                "role": "agent",                        # the agent's reply
                "parts": [{"text": agent_reply_text}]   # agent's message content as a TextPart
            }
        ]
        # We could also include an "artifacts" field if the agent returned files or other data.
    }
    return jsonify(response_task)
 
# Run the Flask app (A2A server) if this script is executed directly.
if __name__ == "__main__":
    app.run(host="0.0.0.0", port=5000)

Understanding the Server Code: Let’s break down what’s happening in the code above:

  • We created a Flask app and defined a global AGENT_CARD dictionary. This contains basic info about our agent. Notably, it has a name, description, a URL (pointing to itself on localhost and port 5000), and a capabilities section. We’ve set streaming: False because our agent will only do simple synchronous replies (if an agent supported streaming, it would handle tasks/sendSubscribe and stream partial results, which we’re not covering here).
  • We set up a route /.well-known/agent.json that returns the agent card JSON. This mimics the standard location where an agent’s card is expected to be found for discovery (GitHub - google/A2A (opens in a new tab)).
  • We define a POST route for /tasks/send. When an A2A client sends a task, Flask will call handle_task(). Inside this function:
    • We parse the JSON body of the request into task_request. According to the A2A schema, this will include an "id" (task ID) and a "message" object representing the user’s initial message.
    • We extract the text of the user’s message. In A2A, a message can have multiple parts (e.g., text, files). Here we assume the first part is text and grab task_request["message"]["parts"][0]["text"]. In a more robust implementation, we would verify the part type and handle errors (our code does a simple try/except and returns a 400 error if the format isn’t as expected).
    • We then generate the agent’s response. Our logic is trivial: we take the user text and prepends "Hello! You said: ...". In a real agent, this is where the core AI logic would go (calling an LLM, performing calculations, etc.).
    • Next, we build the response Task object. We must echo back the id (so the client knows which task this is), set the status.state to "completed" (since we finished the task), and provide a list of messages. We include the original user message (for context/history) and then add our agent’s reply as another message. Each message has a role ("user" or "agent") and a list of parts. We use a single TextPart containing our response text for the agent’s message.
    • Finally, we return this as JSON. Flask will serialize the Python dict into JSON for us.

With this server running, our agent is effectively “live” on http://localhost:5000. It can serve its Agent Card and handle task requests.

Creating a Simple A2A Client Agent

Now that we have an agent server, we need a client that can talk to it. This client could be another agent or just a user-facing application. In A2A terms, it’s an A2A client because it will consume the A2A API of our server agent (GitHub - google/A2A (opens in a new tab)). For our tutorial, we’ll write a small Python script that plays the role of a second agent (or user) sending a question to our EchoAgent and receiving the answer.

Steps for the Client:

  1. Discover the agent: The client should fetch the Agent Card of the server agent to learn about its endpoint and capabilities. We know our agent card is at http://localhost:5000/.well-known/agent.json. We can use the Python requests library to GET this URL.
  2. Send a task: Next, the client will send a POST request to the agent’s tasks/send endpoint with a JSON payload representing the task. This payload should include:
    • a unique "id" for the task (any unique string or number; for example, "task1"),
    • a "message" object with role: "user" and a parts list containing the user’s query as a TextPart.
  3. Receive the response: The agent will respond with a JSON representing the Task (including the agent’s reply message). The client needs to read this and extract the agent’s reply.

Let’s write the client code to perform these steps:

import requests
import uuid
 
# 1. Discover the agent by fetching its Agent Card
AGENT_BASE_URL = "http://localhost:5000"
agent_card_url = f"{AGENT_BASE_URL}/.well-known/agent.json"
response = requests.get(agent_card_url)
if response.status_code != 200:
    raise RuntimeError(f"Failed to get agent card: {response.status_code}")
agent_card = response.json()
print("Discovered Agent:", agent_card["name"], "-", agent_card.get("description", ""))
 
# 2. Prepare a task request for the agent
task_id = str(uuid.uuid4())  # generate a random unique task ID
user_text = "What is the meaning of life?"
task_payload = {
    "id": task_id,
    "message": {
        "role": "user",
        "parts": [
            {"text": user_text}
        ]
    }
}
print(f"Sending task {task_id} to agent with message: '{user_text}'")
 
# 3. Send the task to the agent's tasks/send endpoint
tasks_send_url = f"{AGENT_BASE_URL}/tasks/send"
result = requests.post(tasks_send_url, json=task_payload)
if result.status_code != 200:
    raise RuntimeError(f"Task request failed: {result.status_code}, {result.text}")
task_response = result.json()
 
# 4. Process the agent's response
# The response should contain the task ID, status, and the messages (including the agent's reply).
if task_response.get("status", {}).get("state") == "completed":
    # The last message in the list should be the agent's answer (since our agent included history in messages).
    messages = task_response.get("messages", [])
    if messages:
        agent_message = messages[-1]  # last message (from agent)
        agent_reply_text = ""
        # Extract text from the agent's message parts
        for part in agent_message.get("parts", []):
            if "text" in part:
                agent_reply_text += part["text"]
        print("Agent's reply:", agent_reply_text)
    else:
        print("No messages in response!")
else:
    print("Task did not complete. Status:", task_response.get("status"))

Understanding the Client Code: Here’s what the client does:

  • It uses requests.get to retrieve the agent card from http://localhost:5000/.well-known/agent.json. On success, we parse the JSON and print out the agent’s name and description to verify we found the right agent.
  • We generate a unique task ID using uuid.uuid4(). This is just to ensure we don’t reuse an ID (in real scenarios, the client must ensure each task it sends has a unique ID).
  • We create the task payload as a Python dictionary. This payload follows the A2A schema: it has an "id" (our generated ID) and a "message" for the user’s query. The message has role: "user" and one part containing the text "What is the meaning of life?" (you can change this text to anything – the EchoAgent will just echo it back).
  • We send this payload to the agent’s tasks/send endpoint using requests.post. Note that we use the json=... parameter, which instructs the requests library to send the dictionary as JSON.
  • We check for a 200 OK response. If the status is not 200, we raise an error. (In a more forgiving script, you might handle non-200 responses more gracefully.)
  • If the call succeeded, we parse the JSON response into task_response. This should be the Task object returned by our agent. We then examine it:
    • We check if status.state is "completed" – in our design, the agent will mark it completed after one reply. If, for example, the agent needed more input ("input-required") or failed, we would handle those cases. For simplicity, we assume completion.
    • We then retrieve the list of messages from the response. We expect the last message in this list to be the agent’s reply (since our server code appended the agent’s message after the user’s). We extract the text from the agent’s message parts. In this case, it should be the echo response we constructed.
    • Finally, we print out the agent’s reply.

At this point, our client will have printed the response from the agent. In the next section, we’ll run both the server and client to see the whole exchange.

Running and Testing the Agents

To test our two agents, follow these steps:

  1. Start the server agent: Run the Flask server script (let’s call it echo_agent.py). For example, in your terminal:

    python echo_agent.py

    This should start the Flask development server on http://localhost:5000. You should see a message like “Running on http://0.0.0.0:5000/” (opens in a new tab). The agent is now listening for requests. (Make sure nothing else is already using port 5000, or change the port in the code if needed.)

  2. Run the client script: Open another terminal and run the client script (e.g. client_agent.py):

    python client_agent.py

    The client should print out something like:

    Discovered Agent: EchoAgent – A simple agent that echoes back user messages.
    Sending task 3f8ae8ac-... to agent with message: 'What is the meaning of life?'
    Agent's reply: Hello! You said: 'What is the meaning of life?'

    This indicates the client successfully discovered the agent, sent a question, and received the answer. The agent’s reply is the echoed message we expected.

  3. Verify the interaction: You can also check the server’s console output. Flask will log the requests it handled. You should see a GET request for /.well-known/agent.json and a POST to /tasks/send, along with a 200 status for each. This confirms the protocol flow:

    • Discovery (client GET the agent card) → Initiation (client POST the task) → Processing/Completion (server responds with answer).

Congratulations! You’ve just implemented a basic agent-to-agent communication using the A2A protocol. One agent acted as a service (EchoAgent) and the other as a client that queried it. While this example is simplistic (the “AI” just parrots back the input), it demonstrates the plumbing needed for any A2A interaction.

Using the A2A Client Library (Optional)

In our client code, we manually crafted HTTP requests using requests. The official A2A GitHub repository provides helper classes to simplify this process. For instance, there is an A2AClient class that can handle fetching the agent card and sending tasks for you, as well as an A2ACardResolver to discover the agent’s card. Here’s how we could use those (for those interested in a more idiomatic approach):

import asyncio
from samples.python.common.client import A2AClient, A2ACardResolver
from samples.python.common.types import TaskSendParams, Message, TextPart
 
async def query_agent(agent_url, user_text):
    # Fetch agent card automatically
    card_resolver = A2ACardResolver(agent_url)
    agent_card = card_resolver.get_agent_card()
    print("Discovered Agent:", agent_card.name)
    # Create A2A client with the agent's card
    client = A2AClient(agent_card=agent_card)
    # Prepare Task parameters (using A2A type classes)
    payload = TaskSendParams(
        id=str(uuid.uuid4()),
        message=Message(role="user", parts=[TextPart(text=user_text)])
    )
    # Send the task and wait for completion
    result_task = await client.send_task(payload)  # send_task is an async method
    # Extract agent's reply from result_task
    if result_task.status.state.value == "completed":
        # The A2A Task object can be inspected for messages and artifacts
        for msg in result_task.messages:
            if msg.role == "agent":
                # Print text parts of agent's message
                print("Agent's reply:", " ".join(part.text for part in msg.parts if hasattr(part, "text")))

In the above snippet, A2ACardResolver and A2AClient come from the A2A sample library (located in the samples/python/common directory of the repo). The TaskSendParams, Message, and TextPart are data classes (likely based on Pydantic models) corresponding to parts of the A2A JSON schema. Using these, we don’t have to manually construct dictionaries; we create Python objects and the library will handle the JSON serialization and HTTP calls. The client’s send_task method is asynchronous (hence we use await), and it returns a Task object. Our example shows how you might fetch the agent’s reply from that object.

Note: The above code is for illustration and requires the A2A repository code to be accessible in your Python path. If you have cloned the repo and installed its requirements (via UV or pip), you could integrate this into your client. The official CLI tool in the A2A repo essentially does these steps – it reads the Agent Card and enters a loop to send tasks and print responses (A2A/samples/python/hosts/cli at main · google/A2A · GitHub (opens in a new tab)).

📚

Conclusion

In this tutorial, we covered the basics of Google’s A2A protocol and demonstrated a simple two-agent communication. We set up a minimal A2A server (EchoAgent) and a client that interacts with it. Along the way, we learned about Agent Cards (for discovery) and how tasks and messages are structured in A2A.

While our example was a trivial echo agent, you now have the foundation to build more complex agents. For instance, you could replace the echo logic with a call to a language model or an API to make the agent actually solve a problem or answer questions. The beauty of A2A is that as long as your agent adheres to the protocol (serves an agent card and implements the task endpoints), any other A2A-compatible agent or tool can integrate with it.

Next Steps: You might explore the official A2A repository samples for more advanced patterns. For example, Google’s samples include an agent that generates images from text and returns them as image artifacts (A2A/samples/python/agents/crewai/README.md at main · google/A2A · GitHub (opens in a new tab)), and another that handles multi-turn form-filling dialogues. You can also try the provided command-line interface (CLI) or web demo to chat with your agent. To use the CLI with our EchoAgent, you could run: uv run hosts/cli --agent http://localhost:5000 (if you have UV and the repo setup) to initiate a REPL-like chat with the agent (A2A/samples/python/hosts/cli at main · google/A2A · GitHub (opens in a new tab)).

By standardizing agent communication, A2A opens the door for a rich ecosystem of interoperable AI agents. We’ve only scratched the surface here with a basic example. Happy experimenting with A2A – and may your agents collaborate effectively!

Sources:

  1. Google A2A GitHub – Agent2Agent Protocol README (Conceptual Overview) (GitHub - google/A2A (opens in a new tab)) (GitHub - google/A2A (opens in a new tab)) (GitHub - google/A2A (opens in a new tab)) (GitHub - google/A2A (opens in a new tab))
  2. Google A2A GitHub – Samples and CLI Usage (A2A/samples/python/hosts/cli at main · google/A2A · GitHub (opens in a new tab)) (A2A/samples/python/hosts/cli at main · google/A2A · GitHub (opens in a new tab))
  3. Google A2A GitHub – CrewAI Sample README (agent capabilities via A2A) (A2A/samples/python/agents/crewai/README.md at main · google/A2A · GitHub (opens in a new tab)) (for further exploration)
📚