The A2A protocol — Google’s open-standard for Agent-to-Agent communication (now part of the Linux Foundation) — has officially reached its 1.0 milestone. This is important step for the whole AI ecosystem, signaling production readiness and stability.
As we discuss this upgrade, it is crucial to distinguish between the A2A Specification and the A2A SDK. The 1.0 milestone applies to the core specification — the underlying Protocol Buffers (.proto files) that dictate the strict structural schemas for how agents format, route, and exchange messages. Sitting on top of this specification are the official A2A SDKs (available in many different languages), which abstract away the raw protobufs and make development, routing, and message construction significantly easier.
Press enter or click to view image in full size
However, as with any major software milestone, 1.0 introduces a number of structural, breaking changes. In an isolated environment, upgrading is straightforward. But in real-world multi-agent meshes — where different agents are often maintained by different teams on entirely different release cycles — a breaking change can instantly fracture communication across your architecture.
If one team upgrades their A2A Client to communicate using the 1.0 spec, what happens when it tries to delegate a task to a legacy 0.3 Server?
Fortunately, Google engineers behind A2A anticipated this and built a backward compatibility layer directly into the SDK (available in both Go and Python). This post is a hands-on, empirical guide to that compatibility mechanism. Instead of just reading the spec, we are going to build a testing matrix and evaluate the communication between every permutation of 0.3 and 1.0 clients and servers.
A2A Foundations
Before we dive into the compatibility matrix, let’s quickly level-set on the architecture. Standardizing communication across disparate AI frameworks (like Google ADK, LangChain, etc.) is the core promise of A2A. The concept is elegant: by wrapping an AI Agent in a standardized component — the A2A Executor — we can expose that agent as an A2A Server.
This server features standardized endpoints, the most critical being the Agent Card (a JSON document describing the agent’s capabilities, supported interfaces, and authorization methods). Communication relies on a standard client-server architecture, where an A2A Client (which, in a mesh, is typically just another AI Agent) sends standardized requests to delegate tasks.
Press enter or click to view image in full size
Code for all tests is available here: https://github.com/lolejniczak-shared/a2a-backward-compatibility
The Hello World Agent
To demonstrate that an AI Agent can be virtually anything under the hood, we’ll start with a dead-simple Python class. This class features an invoke method that simply returns a standard greeting:
class HelloWorldAgent:
"""Hello World Agent."""
async def invoke(self) -> str:
return 'Hello from A2A Agent'
Next, the Executor steps in. It takes our underlying AI Agent and translates that custom invoke method into the standard execution pattern expected by the A2A specification:
from typing_extensions import override
from a2a.server.agent_execution import AgentExecutor, RequestContext
from a2a.server.events import EventQueue
from a2a.helpers import new_text_message
from agent import HelloWorldAgent
class HelloWorldAgentExecutor(AgentExecutor):
def __init__(self):
self.agent = HelloWorldAgent()
@override
async def execute(
self,
context: RequestContext,
event_queue: EventQueue,
) -> None:
result = await self.agent.invoke()
await event_queue.enqueue_event(new_text_message(result))
@override
async def cancel(
self, context: RequestContext, event_queue: EventQueue
) -> None:
raise Exception('cancel not supported')
Notice how the execute method is simply calling our invoke method behind the scenes and queuing the result. We will use this exact executor across all the testing scenarios in this post.
Navigating the 1.0 Breaking Changes
The jump to version 1.0 brings necessary maturity, but it also introduces structural changes that break compatibility with older clients (https://a2a-protocol.org/latest/whats-new-v1/ https://github.com/a2aproject/a2a-python/blob/v1.0.0/docs/migrations/v1_0/README.md). A perfect example of this is the Agent Card.
In version 0.3, the Agent Card was relatively flat. In 1.0, the schema has been significantly reorganized to better support multiple transports and interfaces.
Here are the key changes you will encounter in the 1.0 Agent Card Object:
Press enter or click to view image in full size
Because critical routing information like the url and protocolVersion have fundamentally shifted locations, a 0.3 A2A Client will immediately fail to parse a 1.0 Agent Card.
To prevent ecosystem fragmentation, the Google A2A maintainers introduced a deliberate backward compatibility strategy Backward Compatibility Strategy (#1401). This feature allows a 1.0 Server to handle both 1.0 and 0.3 responses when it detects a 0.3 Client.
But how exactly do we configure this compatibility, and does it actually work? Let’s write some code to find out.
Enabling Backward Compatibility
The most important lesson here is that backward compatibility is not enabled by default. If you upgrade your server to 1.0 without changing your configuration, older clients will be locked out.
To bridge the gap, you must explicitly enable compatibility on the A2A Server at two distinct levels:
- The Agent Card Definition First, your Agent Card must advertise that it supports both the 1.0 and 0.3 protocols. Inside the
supported_interfacesarray, you need to provide distinct entries for each version. (If you support multiple transports, like REST and JSON-RPC, you will need two entries per transport).
agent_card = AgentCard(
name='Hello World Agent',
description='Just a hello world agent',
supported_interfaces = [
AgentInterface(
url = "http://localhost:9999/",
protocol_binding = "JSONRPC",
protocol_version = "1.0"
),
AgentInterface(
url = "http://localhost:9999/",
protocol_binding = "JSONRPC",
protocol_version = "0.3"
)
],
default_input_modes=['text/plain'], ##must be valid media type
default_output_modes=['text/plain'],
capabilities=AgentCapabilities(streaming=True),
skills=[skill],
version = "0.0.1" ## this is version of agent, not of the protocol. Protocol version is availble through supported interfaces
)
2. Route Configuration (enable_v0_3_compat) Advertising support is only half the battle; the server must also know how to process legacy payloads. When initializing your FastAPI server, you must pass enable_v0_3_compat=True to your route creators.
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.routes import (
create_agent_card_routes,
create_jsonrpc_routes,
create_rest_routes,
)
from fastapi import FastAPI
import uvicorn
.........
.........
.........
# 1. Setup Handler
request_handler = DefaultRequestHandler(
agent_card=agent_card,
agent_executor=HelloWorldAgentExecutor(),
task_store=InMemoryTaskStore(),
)
# 2. Create Routes with Compatibility Flags Enabled
jsonrpc_routes = create_jsonrpc_routes(
request_handler=request_handler,
rpc_url='/a2a/jsonrpc',
enable_v0_3_compat=True, # Explicitly enable 0.3 compatibility
)
agent_card_routes = create_agent_card_routes(
agent_card=agent_card,
)
# 3. Initialize FastAPI and mount routes
app = FastAPI()
app.routes.extend(agent_card_routes)
app.routes.extend(jsonrpc_routes)
if __name__ == "__main__":
print("A2A server v1.0 with compatibility starting on port 9999...")
uvicorn.run("main:app", host="0.0.0.0", port=9999)
Migration Note: If you are upgrading from 0.3, you will notice the A2AFastAPIApplication or A2AStarletteApplicationwrapper is no longer present in the 1.0 SDK. Instead, you directly extend a standard FastAPI app with A2A routes.
After enabling backward compatibility, Agent Card will include attributes that can be understood by both 0.3 and 1.0 clients (left: Agent Card generated without compatibility enabled, right: Agent Card generated after enabling compatibility):
The Execution Matrix: Testing Compatibility
We have our clients and our servers coded. Now, let’s run through the definitive testing matrix to see exactly how these versions interact — and where they break. All test scenarios are pictured on the diagrams below:
Press enter or click to view image in full size
Press enter or click to view image in full size
Lets focus first on server implementations.
The Server Implementations
To run our matrix, we need three distinct server files:
-
A2A server v0.3
-
A2A server v1.0 with disabled compatibility (default)
-
A2A server v1.0 with enabled compatibility
Server A: Legacy A2A Server v0.3
from executor import (
HelloWorldAgentExecutor, # type: ignore[import-untyped]
)
from a2a.server.apps import A2AStarletteApplication
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.tasks import InMemoryTaskStore
from a2a.types import AgentCapabilities, AgentCard, AgentSkill
skill = AgentSkill(
id='hello_world',
name='Returns hello world',
description='just returns hello world',
tags=['hello world'],
examples=['hi', 'hello world'],
)
agent_card = AgentCard(
name='Hello World Agent',
description='Just a hello world agent',
url='http://localhost:9999/',
version='1.0.0',
defaultInputModes=['text'],
defaultOutputModes=['text'],
capabilities=AgentCapabilities(streaming=True),
skills=[skill],
)
request_handler = DefaultRequestHandler(
agent_executor=HelloWorldAgentExecutor(),
task_store=InMemoryTaskStore(),
)
server = A2AFastAPIApplication(
agent_card=agent_card, http_handler=request_handler
)
import uvicorn
print("A2A server v 0.3")
uvicorn.run(server.build(), host='0.0.0.0', port=9999)
Server B: A2A Server v1.0 (Compatibility Disabled)
from executor import (
HelloWorldAgentExecutor,
)
from a2a.server.request_handlers import DefaultRequestHandler
from a2a.server.tasks import InMemoryTaskStore
from a2a.types import AgentCapabilities, AgentCard, AgentSkill, AgentInterface
from a2a.server.routes import (
create_agent_card_routes,
create_jsonrpc_routes,
create_rest_routes,
)
from fastapi import FastAPI
import uvicorn
host = "0.0.0.0"
port = 9999
# 1. Define Skills and Agent Card
skill = AgentSkill(
id='hello_world',
name='Returns hello world',
description='just returns hello world',
tags=['hello world'],
examples=['hi', 'hello world'],
)
agent_card = AgentCard(
name='Hello World Agent',
description='Just a hello world agent',
supported_interfaces=[
AgentInterface(
url=f'http://{host}:{port}/a2a/jsonrpc',
protocol_binding="JSONRPC",
protocol_version="1.0"
)
],
default_input_modes=['text/plain'],
default_output_modes=['text/plain'],
capabilities=AgentCapabilities(streaming=True),
skills=[skill],
version="0.0.1"
)
# 2. Setup Handlers
request_handler = DefaultRequestHandler(
agent_card=agent_card,
agent_executor=HelloWorldAgentExecutor(),
task_store=InMemoryTaskStore(),
)
# 3. Create Routes
jsonrpc_routes = create_jsonrpc_routes(
request_handler=request_handler,
rpc_url='/a2a/jsonrpc',
enable_v0_3_compat=False,
)
agent_card_routes = create_agent_card_routes(
agent_card=agent_card,
)
# 4. Initialize FastAPI and mount routes
app = FastAPI()
app.routes.extend(agent_card_routes)
app.routes.extend(jsonrpc_routes)
# 5. Execution Logic
if __name__ == "__main__":
print(f"A2A server v 1.0 starting on port {port}...")
uvicorn.run("s10ncnew:app", host=host, port=port, reload=True)
Server C: A2A Server v1.0 (Compatibility Enabled)
(Note: This is identical to Server B, with the exception of the legacy interface added to the Agent Card and the compatibility flag set to True).
# ... [Imports and Skill definition identical to Server B] ...
agent_card = AgentCard(
name='Hello World Agent',
description='Just a hello world agent',
supported_interfaces=[
AgentInterface(
url=f'http://{host}:{port}/a2a/jsonrpc',
protocol_binding="JSONRPC",
protocol_version="1.0"
),
AgentInterface(
url=f'http://{host}:{port}/a2a/jsonrpc',
protocol_binding="JSONRPC",
protocol_version="0.3" # Legacy version supported
)
],
# ... [Rest of Agent Card identical] ...
)
# ... [Handler definition identical] ...
jsonrpc_routes = create_jsonrpc_routes(
agent_card=agent_card,
request_handler=request_handler,
rpc_url='/a2a/jsonrpc',
enable_v0_3_compat=True, # Compatibility enabled!
)
# ... [App initialization identical] ...
Notice how the 1.0 scripts handle the new route creation compared to the legacy 0.3 wrapper.
Press enter or click to view image in full size
The Client Implementations
Now we need our two clients to fire requests at the servers we just built:
-
A2A Client v0.3
-
A2A Client v1.0
Client A: Legacy A2A Client v0.3
import httpx
from uuid import uuid4
import asyncio
from typing import Any
from a2a.client.middleware import ClientCallInterceptor
from a2a.client.client_factory import ClientFactory, ClientConfig
from google import auth as google_auth
from google.auth.transport import requests as google_requests
import uuid
import requests
from google.oauth2 import id_token
from a2a.client import A2ACardResolver
import json
AGENT_URL = 'http://localhost:9999'
USER_QUERY = 'What will be the role of agentic protocols in agentic economy?'
async def main():
async with httpx.AsyncClient(
headers={
##"Authorization": f"Bearer {BEARER_TOKEN}",
"Content-Type": "application/json",
}
) as httpx_client:
resolver = A2ACardResolver(httpx_client, AGENT_URL)
card = await resolver.get_agent_card()
print('\n✓ Agent Card Found:')
print(f' Name: {card.name}')
config = ClientConfig(httpx_client=httpx_client)
client = await ClientFactory.connect(
agent=card,
client_config=config,
relative_card_path='/.well-known/agent-card.json'
)
message_payload = {
'role': 'user',
'parts': [{'kind': 'text', 'text': USER_QUERY}],
'messageId': uuid4().hex,
'contextId': '959304c1-e814-4486-b9a7-efe885a6b066'
}
# Send raw dictionary
resp = client.send_message(message_payload)
final_response = None
async for response_chunk in resp:
if isinstance(response_chunk, tuple):
final_response = response_chunk[0]
else:
final_response = response_chunk
if final_response:
print("--- Final Response ---")
print(final_response.model_dump(mode='json', exclude_none=True))
if __name__ == '__main__':
asyncio.run(main())
Client B: A2A Client v1.0
import httpx
from uuid import uuid4
import asyncio
from typing import Any
from google.protobuf.struct_pb2 import Struct, Value
from contextlib import aclosing
from a2a.types import SendMessageRequest, Message, Part, Role
from a2a.client import A2ACardResolver
from a2a.client.client_factory import ClientFactory, ClientConfig
from contextlib import aclosing
import json
AGENT_URL = 'http://localhost:9999'
USER_QUERY = 'What will be the role of agentic protocols in agentic economy?'
async def main():
async with httpx.AsyncClient(
headers={
##"Authorization": f"Bearer {BEARER_TOKEN}",
"Content-Type": "application/json",
}
) as httpx_client:
resolver = A2ACardResolver(httpx_client, AGENT_URL)
card = await resolver.get_agent_card()
print('\n✓ Agent Card Found:')
print(f' Name: {card.name}')
config = ClientConfig(httpx_client=httpx_client)
client = await ClientFactory(config).create_from_url(
url=AGENT_URL,
)
msg = Message(
role=Role.ROLE_USER, ##instead of user
message_id=f'stream-{uuid4()}',
parts=[
Part(text=USER_QUERY), ##'kind': 'text', there is no kind field in 1.0
]
)
async for event in client.send_message(request=SendMessageRequest(message=msg)):
print(f"Agent says: {event}")
if __name__ == '__main__':
asyncio.run(main())
When you compere both side by side you will notice that we had to change our 1.0 A2A Client. This is another lesson: If you upgrade your A2A Client to 1.0, you must adapt to a fundamentally redesigned message payload.
The structural changes here are significant, primarily affecting the Message object and how individual Part contents are defined.
1. The Role Enum Shift In version 0.3, message roles were simple lowercase strings. In 1.0, this is a breaking change; roles have been formalized into SCREAMING_SNAKE_CASE enums with a ROLE_ prefix.
-
v0.3:
"user","agent" -
v1.0:
Role.ROLE_USER,Role.ROLE_AGENT
Press enter or click to view image in full size
2. The Unified Part Architecture This is the most impactful change for client developers. Version 1.0 completely deprecates the old structure of separate part types (TextPart, FilePart, DataPart) and removes the kind discriminator field entirely.
Instead, 1.0 introduces a single, unified Part message. The content type is now determined implicitly by which member field is present (e.g., text, raw, url, or data).
Press enter or click to view image in full size
Now — same but applied to our scripts: LEFT — 0.3 A2A Client, RIGHT: 1.0 A2A CLIENT
Press enter or click to view image in full size
We are ready now to test all scenarios:
I run my 0.3 code from environment with the following dependencies:
a2a-sdk[all]==v0.3.25
uvicorn
grpcio
For 1.0 code that list is as follows:
a2a-sdk==v1.0.0
uvicorn
typing_extensions
sse-starlette
fastapi
jsonschema
grpcio
Scenario 1: Client 0.3 ↔ Server 0.3 (The Control Group)
Press enter or click to view image in full size
As expected, running a legacy client against a legacy server operates flawlessly. Monitoring the server logs (RIGHT), we observe the standard handshake: a GET request to retrieve the Agent Card, followed by a POST to execute the payload. The client terminal (LEFT) successfully yields:
'Hello from A2A Agent'
Scenario 2: Client 0.3 ↔ Server 1.0 (No Compatibility)
This is the fracture point. If a server is upgraded to 1.0 without intentionally configuring the compatibility flags, legacy clients will immediately crash. When we point our 0.3 Client at this standard 1.0 server, the client successfully fetches the Agent Card but entirely fails to parse the new schema:
Press enter or click to view image in full size
Because the url field was relocated into the supportedInterfaces array in 1.0, the legacy 0.3 Pydantic model throws a hard validation error before the client even attempts to construct a message:
a2a.client.errors.A2AClientJSONError:
JSON Error: Failed to validate agent card structure from
http://localhost:9999/.well-known/agent-card.json:
[{"type":
"missing","loc":["url"],
"msg":"Field required",
"input":
{
"name": "Hello World Agent",
"description": "Just a hello world agent",
"supportedInterfaces": [
{
"url": "http://0.0.0.0:9999/a2a/jsonrpc",
"protocolBinding": "JSONRPC",
"protocolVersion": "1.0"
}
],
"version": "0.0.1",
"capabilities": {
"streaming": true
},
"defaultInputModes": [
"text/plain"
],
"defaultOutputModes": [
"text/plain"
],
"skills": [
{
"id": "hello_world",
"name": "Returns hello world",
"description": "just returns hello world",
"tags": [
"hello world"
],
"examples": [
"hi",
"hello world"
]
}
]
}
"url":"https://errors.pydantic.dev/2.12/v/missing"}]
Scenario 3: Client 0.3 ↔ Server 1.0 (Compatibility Enabled)
Press enter or click to view image in full size
This scenario demonstrates the value of the enable_v0_3_compat flag. With compatibility enabled on the 1.0 server and the legacy interface advertised in the Agent Card, the 0.3 Client requests the card and receives a structure it perfectly understands. It sends its legacy dictionary payload, the server dynamically maps it to the new unified Part architecture behind the scenes, and the client successfully receives the expected response. Complete backward compatibility is achieved.
Scenario 4: Client 1.0 ↔ Server 0.3
What happens if an edge team aggressively upgrades their client to 1.0, but the A2A server is still on 0.3? Surprisingly, it routes perfectly. The 1.0 A2A Client sends its modern, strongly-typed message, and successfully communicates with the legacy 0.3 server. There is no need to enable any special compatibility flags on the client side — the 1.0 Client SDK handles the downward translation seamlessly.
Press enter or click to view image in full size
Scenarios 5 Client 1.0 ↔ Server 1.0 ( Without Compatibility)
No surprise. Naturally, a 1.0 Client talking to a native 1.0 Server works perfectly out of the box.
Press enter or click to view image in full size
Scenarios 6 Client 1.0 ↔ Server 1.0 ( With Compatibility)
Can compatibility configuration break things?
Press enter or click to view image in full size
Seems like all works fine. The server correctly routes the 1.0 Client to the 1.0 endpoints without degradation or issue.
A2A Backward Compatibility Test Summary:
To provide a clear overview of our findings, the table below summarizes the results of all six client-server permutations tested in our execution matrix. As the results demonstrate, the ecosystem is highly resilient. The only point of failure occurs when a legacy 0.3 Client attempts to communicate with a 1.0 Server that has not explicitly opted into 0.3 support — highlighting exactly why enabling the enable_v0_3_compat flag and modifying agent cart is essential for a migration.
Press enter or click to view image in full size
Enterprise Considerations: Caching in Gemini
If you are managing agents within a wider corporate ecosystem, there is one final architectural quirk you must account for.
How about Gemini Enterprise? If you are new to Gemini Enterprise or would like to learn more on how to register A2A agents in Gemini Enterprise check my previous blog post entitled: Running and Debugging A2A Agents in Gemini Enterprise:
https://medium.com/google-cloud/running-and-debugging-a2a-agents-in-gemini-enterprise-118a158e7ff1
TLDR is that when you register an A2A Agent in Gemini Enterprise, the platform ingests a copy of your Agent Card. You can think of this as a local, platform-side cache.
Press enter or click to view image in full size
If you upgrade your A2A Server to 1.0 and enable backward compatibility, you have fundamentally changed the structure of your Agent Card (by advertising two different protocol versions in your supported_interfaces). Therefore, you must update that cached Agent Card in Gemini Enterprise to reflect the new compatibility routing. (Note: Keep an eye on upcoming Google Cloud Next announcements, as agent governance and dynamic routing capabilities are evolving rapidly).
Conclusion
The upgrade to A2A 1.0 brings a much cleaner, more robust architecture, but it demands careful rollout strategies. By utilizing the built-in enable_v0_3_compat flags, you can safely modernize your infrastructure while keeping the lights on for partners on A2A 0.3 in your multi-agent mesh.
Code:
Code to reproduce my experiments is available in available in my git repo: https://github.com/lolejniczak-shared/a2a-backward-compatibility
This article is authored by Lukasz Olejniczak — Customer Engineer at Google Cloud. The views expressed are those of the authors and don’t necessarily reflect those of Google.
Please clap for this article if you enjoyed reading it. For more about google cloud, data science, data engineering, and AI/ML follow me on LinkedIn.

















