The a2a-sdk has achieved a major milestone in stability and reliability with the update to full A2A Protocol v1.0 compatibility. This guide provides a detailed overview of the breaking changes in version v1.0 and instructions for migrating your codebase.
Beyond protocol support, v1.0 enhances the developer experience by introducing unified helper utilities for easier object creation and adopting Starlette route factory functions for more flexible server configuration.
This documentation details the technical upgrades and architectural modifications introduced in A2A Python SDK v1.0. For developers using the database persistence layer, please refer to the Database Migration Guide for specific update instructions.
- Update Dependencies
- Types
- Server: DefaultRequestHandler
- Server: Application Setup
- Supporting v0.3 Clients
- Client: Creating a Client
- Client: Send Message
- Client: Push Notifications Config
- Helper Utilities
- Summary of Key Changes
- Get Started
(UV users) To upgrade to the latest version of the a2a-sdk, update the dependencies section in your pyproject.toml file.
| File | Before (v0.3) |
After (v1.0) |
|---|---|---|
pyproject.toml |
dependencies = ["a2a-sdk>=0.3.0"] | dependencies = ["a2a-sdk>=1.0.0"] |
Installation
After updating your configuration file, sync your environment:
- Using UV:
uv sync- Using pip:
pip install --upgrade a2a-sdkTypes have migrated from Pydantic models to Protobuf-based classes.
All the enum values are now standardized from snake_case to SCREAMING_SNAKE_CASE format.
This affects every enum in the SDK: TaskState, Role.
| Enum | v0.3 | v1.0 |
|---|---|---|
TaskState |
(no equivalent — protobuf default) | TaskState.TASK_STATE_UNSPECIFIED |
TaskState |
TaskState.submitted |
TaskState.TASK_STATE_SUBMITTED |
TaskState |
TaskState.working |
TaskState.TASK_STATE_WORKING |
TaskState |
TaskState.completed |
TaskState.TASK_STATE_COMPLETED |
TaskState |
TaskState.failed |
TaskState.TASK_STATE_FAILED |
TaskState |
TaskState.canceled |
TaskState.TASK_STATE_CANCELED |
TaskState |
TaskState.input_required |
TaskState.TASK_STATE_INPUT_REQUIRED |
TaskState |
TaskState.auth_required |
TaskState.TASK_STATE_AUTH_REQUIRED |
TaskState |
TaskState.rejected |
TaskState.TASK_STATE_REJECTED |
Role |
(no equivalent — protobuf default) | Role.ROLE_UNSPECIFIED |
Role |
Role.user |
Role.ROLE_USER |
Role |
Role.agent |
Role.ROLE_AGENT |
Example:
a2a-mcp-without-framework/server/agent_executor.pyin PR #509
Constructing messages is simplified in v1.0. The old API required wrapping content in an intermediate type (TextPart, FilePart, DataPart) before placing it inside a Part. In v1.0, Part is a single unified message — set the content type directly on it and the wrapper types are gone entirely.
Key differences:
Part(TextPart(text=...))→Part(text=...)(flat union field)Role.user→Role.ROLE_USER,Role.agent→Role.ROLE_AGENT
Before (v0.3):
from a2a.types import Message, Part, Role, TextPart
from uuid import uuid4
message = Message(
role=Role.user,
parts=[Part(TextPart(text="Hello"))],
message_id=uuid4().hex,
task_id=uuid4().hex,
)After (v1.0):
Using A2A helper utilities
from a2a.helpers import new_text_message
from a2a.types import Role
# Use the helper function to create `Hello` message
message = new_text_message(text="Hello", role=Role.ROLE_USER)Without helper utils, you can still construct directly
from a2a.types import Message, Part, Role
from uuid import uuid4
message = Message(
role=Role.ROLE_USER,
parts=[Part(text="Hello")],
message_id=uuid4().hex,
task_id=uuid4().hex,
)Example:
helloworld/test_client.pyin PR #474
The new AgentCard can supports multiple transport bindings using AgentInterface class.
Key differences:
urlis gone; usesupported_interfaceswith one or moreAgentInterfaceentriesAgentCapabilities.input_modesandAgentCapabilities.output_modesare removed; useAgentCard.default_input_modes/AgentCard.default_output_modesfor card-level defaults, orAgentSkill.input_modes/AgentSkill.output_modesfor per-skill overridessupports_authenticated_extended_cardis no longer a top-levelAgentCardfield; it has moved intoAgentCapabilitiesand is renamed toextended_agent_cardAgentInterface.protocol_bindingaccepted values:'JSONRPC','HTTP+JSON','GRPC'examplesfield was removed; set it perAgentSkillinstead
Before (v0.3):
from a2a.types import AgentCard, AgentCapabilities, AgentSkill
agent_card = AgentCard(
name='My Agent',
description='...',
url='http://localhost:9999/',
version='1.0.0',
default_input_modes=['text/plain'],
default_output_modes=['text/plain'],
supports_authenticated_extended_card=True,
capabilities=AgentCapabilities(
input_modes=['text/plain'],
output_modes=['text/plain'],
streaming=True,
),
skills=[skill],
examples=['example'],
)After (v1.0):
from a2a.types import AgentCard, AgentCapabilities, AgentInterface, AgentSkill,
agent_card = AgentCard(
name='My Agent',
description='...',
supported_interfaces=[
# JSON-RPC
AgentInterface(
protocol_binding='JSONRPC',
url='http://localhost:41241/a2a/jsonrpc/',
),
# GRPC
AgentInterface(
protocol_binding='GRPC',
url='http://localhost:50051/a2a/grpc/',
)
],
version='1.0.0',
default_input_modes=['text/plain'],
default_output_modes=['text/plain'],
capabilities=AgentCapabilities(
streaming=True,
extended_agent_card=True,
),
skills=[skill],
)Example:
a2a-mcp-without-framework/server/__main__.pyin PR #509
DefaultRequestHandler now requires agent_card as a constructor argument (it was previously passed to the application wrapper).
Before (v0.3):
request_handler = DefaultRequestHandler(
agent_executor=MyAgentExecutor(),
task_store=InMemoryTaskStore(),
)After (v1.0):
request_handler = DefaultRequestHandler(
agent_executor=MyAgentExecutor(),
task_store=InMemoryTaskStore(),
agent_card=agent_card,
)Example:
a2a-mcp-without-framework/server/__main__.pyin PR #509
The wrapper classes (A2AStarletteApplication, A2AFastApiApplication and A2ARESTFastApiApplication) are now removed. The Server setup now uses Starlette route factory functions directly, giving you full control over the routing.
Before (v0.3):
from a2a.server.apps import A2AStarletteApplication
import uvicorn
server = A2AStarletteApplication(
agent_card=agent_card,
http_handler=request_handler,
)
uvicorn.run(server.build(), host=host, port=port)After (v1.0):
from a2a.server.routes import create_agent_card_routes, create_jsonrpc_routes
from starlette.applications import Starlette
import uvicorn
routes = []
routes.extend(create_agent_card_routes(agent_card))
routes.extend(create_jsonrpc_routes(request_handler, rpc_url='/'))
app = Starlette(routes=routes)
uvicorn.run(app, host=host, port=port)If you need REST transport in addition to JSON-RPC:
from a2a.server.routes import create_agent_card_routes, create_jsonrpc_routes, create_rest_routes
from starlette.applications import Starlette
import uvicorn
routes = []
routes.extend(create_agent_card_routes(agent_card))
routes.extend(create_jsonrpc_routes(request_handler, rpc_url='/'))
routes.extend(create_rest_routes(request_handler))
app = Starlette(routes=routes)
uvicorn.run(app, host=host, port=port)Example:
a2a-mcp-without-framework/server/__main__.pyin PR #509
If you cannot update all clients at once, you can run a v1.0 server that simultaneously accepts v0.3 connections. Two changes are needed.
1. Add the v0.3 AgentInterface to supported_interfaces in your AgentCard:
supported_interfaces=[
AgentInterface(protocol_binding='JSONRPC', protocol_version='0.3', url='http://localhost:9999/'),
]2. Enable the compat flag on the relevant route factory:
create_jsonrpc_routes(request_handler, rpc_url='/', enable_v0_3_compat=True)
create_rest_routes(request_handler, enable_v0_3_compat=True)For a full working example see
samples/hello_world_agent.py. For known limitations see issue #742.
New create_client() ClientFactory function that creates a client for the agent.
Note: The legacy
A2AClientclass has been removed.
Before (v0.3):
from a2a.client import ClientFactory
# From URL
factory = ClientFactory()
client = factory.create_client('http://localhost:9999/')
# From an already-resolved AgentCard
factory = ClientFactory()
client = factory.create_client(agent_card)After (v1.0):
from a2a.client import create_client
# From URL — resolves the agent card automatically
client = await create_client('http://localhost:9999/')
# From an already-resolved AgentCard
client = await create_client(agent_card)Example:
a2a-mcp-without-framework/client/agent.pyin PR #509 (create_client()usage)
The BaseClient.send_message() return type is standardised from AsyncIterator[ClientEvent | Message] to AsyncIterator[StreamResponse].
Each StreamResponse yields exactly one of: task, message, status_update, or artifact_update. Use HasField() to check which field is set.
Before (v0.3):
async for event, message in client.send_message(request):
if isinstance(event, Task):
...
if isinstance(event, UpdateEvent):
...
if message:
...After (v1.0):
async for chunk in client.send_message(request):
if chunk.HasField('artifact_update'):
...
elif chunk.HasField('status_update'):
...
elif chunk.HasField('task'):
...
elif chunk.HasField('message'):
...ClientConfig.push_notification_config is now singular (a single TaskPushNotificationConfig or None), not a list.
Before (v0.3):
config = ClientConfig(
push_notification_configs=[my_push_config],
)After (v1.0):
config = ClientConfig(
push_notification_config=my_push_config,
)A new a2a.helpers module consolidates helper functions into a single import. Most were previously available under a2a.utils.*; a few are new in v1.0.
from a2a.helpers import (
display_agent_card, # print a human-readable summary of an AgentCard to stdout
get_artifact_text, # join all text parts of an Artifact into a single string (delimiter='\n')
get_message_text, # join all text parts of a Message into a single string (delimiter='\n')
get_stream_response_text, # extract text from a StreamResponse proto message
get_text_parts, # return a list of raw text strings from a sequence of Parts (skips non-text parts)
new_artifact, # create an Artifact from a list of Parts, name, optional description and artifact_id
new_message, # create a Message from a list of Parts with role (default ROLE_AGENT), optional task_id/context_id
new_task, # create a Task with explicit task_id, context_id, and state
new_task_from_user_message, # create a TASK_STATE_SUBMITTED Task from a user Message; raises if role != ROLE_USER or parts are empty
new_text_artifact, # create an Artifact with a single text Part, name, optional description and artifact_id
new_text_artifact_update_event, # create a TaskArtifactUpdateEvent with a text artifact
new_text_message, # create a Message with a single text Part; role defaults to ROLE_AGENT
new_text_status_update_event, # create a TaskStatusUpdateEvent with a text message
)- Standardisation to
SCREAMING_SNAKE_CASE— All enum values have been renamed fromkebab-casestrings toSCREAMING_SNAKE_CASEfor compliance with the ProtoJSON specification. AgentCard— Significantly restructured to support multiple transport interfaces.AgentInterface— The top-levelurlfield is replaced bysupported_interfaces, a list ofAgentInterfaceobjects. Each entry describes a single transport endpoint carryingprotocol_binding,protocol_version, andurl.- Input and output modes —
AgentCapabilities.input_modesandAgentCapabilities.output_modesare removed and now live directly onAgentCardasdefault_input_modesanddefault_output_modes. Individual skills can override these with their owninput_modesandoutput_modes.
- Application setup — The wrapper classes (
A2AStarletteApplication,A2AFastApiApplicationandA2ARESTFastApiApplication) are now removed. Server setup now uses route factory functionscreate_jsonrpc_routes(),create_rest_routes(),create_agent_card_routes()composed directly into a Starlette or FastAPI app. - Helper utilities — A new
a2a.helpersmodule consolidates all helper functions under a single import, replacing the scattereda2a.utils.*modules and adding new helpers for constructing and reading v1.0 proto types.
The fastest way to see v1.0 in action is to run the samples:
| File | Role | Description |
|---|---|---|
samples/hello_world_agent.py |
Server | A2A agent exposing JSON-RPC, REST, and gRPC — with v0.3 compat enabled |
samples/cli.py |
Client | Interactive terminal client; supports all three transports |
# In one terminal — start the agent:
uv run python samples/hello_world_agent.py
# In another — connect with the CLI:
uv run python samples/cli.pyThen type a message like hello and press Enter. See samples/README.md for full details.
For more examples see the a2a-samples repository.