Google's Agent Development Kit (ADK, google/adk-python) currently pins a2a-sdk>=0.3.4,<0.4.0 and uses Pydantic-based types for all A2A protocol objects. The a2a-sdk v1.0 release migrates every protocol type from Pydantic models to Protobuf-generated classes, which breaks construction patterns (Part(root=TextPart(...)) becomes Part(text=...)), enum naming (TaskState.working becomes TASK_STATE_WORKING), field access (.root.text becomes .text), and serialization calls (.model_dump() is gone on protobuf objects). The AgentCard.url field is removed in favor of supported_interfaces.
ADK's A2A integration is marked experimental and spans approximately 20 source files under src/google/adk/a2a/ plus src/google/adk/agents/remote_a2a_agent.py, with roughly 12,000 lines of test coverage across 15+ test files. The migration affects every file in this surface area. The v1.0 SDK ships a a2a.compat.v0_3 compatibility layer, but it does not cover ADK's internal patterns (direct .root access, isinstance checks on discriminated-union members, model_dump/model_validate calls on A2A types). A phased approach -- bump dependency with compat shims first, migrate tests to define the target contract, then update implementation -- minimizes risk.
| Attribute | Value |
|---|---|
| ADK version | 2.2.0 (google-adk) |
| a2a-sdk dependency | >=0.3.4,<0.4.0 (main); >=0.3.0,<0.4.0 (dev) |
| Status | Experimental (decorator-gated, suppressible via env var) |
| Source files (a2a/) | ~20 Python modules |
| Test files | 15+ files, ~12,000 lines |
| Class / Module | Role | File |
|---|---|---|
A2aAgentExecutor |
Server-side: runs ADK agent against A2A request | executor/a2a_agent_executor.py |
_A2aAgentExecutor |
New (v2) executor implementation | executor/a2a_agent_executor_impl.py |
A2aAgentExecutorConfig |
Configuration for executor (converters, interceptors) | executor/config.py |
ExecutorContext |
Runtime context (app_name, user_id, session_id, runner) | executor/executor_context.py |
TaskResultAggregator |
Aggregates streaming task status into final state | executor/task_result_aggregator.py |
RemoteA2aAgent |
Client-side: calls remote A2A agent from ADK | agents/remote_a2a_agent.py |
A2aRemoteAgentConfig |
Configuration for remote agent (converters, interceptors) | agent/config.py |
AgentCardBuilder |
Builds AgentCard from ADK agent metadata | utils/agent_card_builder.py |
to_a2a() |
One-liner to wrap ADK agent as A2A Starlette app | utils/agent_to_a2a.py |
convert_a2a_part_to_genai_part |
A2A Part -> GenAI Part | converters/part_converter.py |
convert_genai_part_to_a2a_part |
GenAI Part -> A2A Part | converters/part_converter.py |
convert_event_to_a2a_events |
ADK Event -> list of A2A events (legacy) | converters/event_converter.py |
convert_event_to_a2a_events |
ADK Event -> list of A2A events (new impl) | converters/from_adk_event.py |
convert_a2a_task_to_event |
A2A Task -> ADK Event | converters/to_adk_event.py |
convert_a2a_message_to_event |
A2A Message -> ADK Event | converters/to_adk_event.py |
convert_a2a_status_update_to_event |
A2A TaskStatusUpdateEvent -> ADK Event | converters/to_adk_event.py |
convert_a2a_artifact_update_to_event |
A2A TaskArtifactUpdateEvent -> ADK Event | converters/to_adk_event.py |
LongRunningFunctions |
Tracks long-running function calls across events | converters/long_running_functions.py |
| Log utilities | Structured logging for A2A requests/responses | logs/log_utils.py |
| Test File | Lines | Focus |
|---|---|---|
test_part_converter.py |
1,057 | Part round-trip conversion |
test_event_converter.py |
992 | Legacy event conversion |
test_event_round_trip.py |
208 | Event round-trip consistency |
test_from_adk.py |
108 | ADK Event -> A2A events (new impl) |
test_to_adk.py |
420 | A2A -> ADK Event (new impl) |
test_request_converter.py |
411 | A2A request -> AgentRunRequest |
test_utils.py |
204 | Metadata key utilities |
test_a2a_agent_executor.py |
1,074 | Legacy executor |
test_a2a_agent_executor_impl.py |
811 | New executor |
test_task_result_aggregator.py |
315 | Task result aggregation |
test_client_server.py |
638 | Integration: HTTP round-trip |
test_log_utils.py |
381 | Logging utilities |
test_agent_card_builder.py |
1,174 | AgentCard construction |
test_agent_to_a2a.py |
1,053 | to_a2a() function |
test_remote_a2a_agent.py |
2,900 | RemoteA2aAgent |
| Total | ~12,000 |
a2a-sdk>=1.0,<2inpyproject.toml- All A2A protocol types use Protobuf-generated classes
- v1.0 wire protocol (JSON-RPC with protobuf serialization)
- Backward compatibility maintained for ADK's public API signatures (constructor parameters, return types)
to_a2a()produces valid v1.0 agent cards and handles v1.0 messages- All existing A2A tests pass
The discriminated union Part changes from Pydantic to a flat protobuf oneof:
| v0.3 Pattern | v1.0 Pattern | Occurrences (src) | Occurrences (test) |
|---|---|---|---|
Part(root=TextPart(text=...)) |
Part(text=TextPart(text=...)) |
3 | 25 |
Part(root=FilePart(...)) |
Part(file=FilePart(...)) |
2 | ~5 |
Part(root=DataPart(...)) |
Part(data=DataPart(...)) |
6 | ~10 |
part.root (access inner type) |
Direct field access (part.text, part.file, part.data) |
28 (src), 54 (test) | -- |
isinstance(part.root, TextPart) |
part.HasField("text") or part.text is not None |
5 (src) | ~15 |
isinstance(part.root, DataPart) |
part.HasField("data") or part.data is not None |
3 (src) | ~5 |
isinstance(part.root, FilePart) |
part.HasField("file") or part.file is not None |
1 (src) | ~5 |
isinstance(part.file, FileWithUri) |
part.file.HasField("uri") or check field presence |
1 (src) | ~3 |
isinstance(part.file, FileWithBytes) |
part.file.HasField("bytes_") or check field presence |
1 (src) | ~3 |
part.model_dump_json(...) on A2A types |
Protobuf serialization (MessageToJson or SDK helper) |
~8 (src) | ~10 |
DataPart.model_validate_json(...) |
Protobuf deserialization (Parse or SDK helper) |
1 (src) | ~2 |
part.model_dump(...) on A2A Message/Task |
Protobuf serialization | ~10 (src) | ~5 |
All enums change from snake_case Pydantic string enums to SCREAMING_SNAKE_CASE protobuf enums:
| v0.3 Value | v1.0 Value | Occurrences (src) |
|---|---|---|
TaskState.submitted |
TaskState.TASK_STATE_SUBMITTED |
3 |
TaskState.working |
TaskState.TASK_STATE_WORKING |
8 |
TaskState.completed |
TaskState.TASK_STATE_COMPLETED |
2 |
TaskState.failed |
TaskState.TASK_STATE_FAILED |
5 |
TaskState.input_required |
TaskState.TASK_STATE_INPUT_REQUIRED |
7 |
TaskState.auth_required |
TaskState.TASK_STATE_AUTH_REQUIRED |
5 |
Role.agent |
Role.ROLE_AGENT |
10 |
Role.user |
Role.ROLE_USER |
1 |
| Total enum sites | ~41 src, ~106 test |
| v0.3 | v1.0 | Impact |
|---|---|---|
A2AStarletteApplication |
Route factory functions (or updated class API) | agent_to_a2a.py line 181 |
A2AStarletteApplication.add_routes_to_app() |
New route registration pattern | agent_to_a2a.py line 187 |
DefaultRequestHandler constructor |
May have new/changed parameters | agent_to_a2a.py line 157 |
InMemoryTaskStore |
May be renamed or restructured | agent_to_a2a.py line 148 |
InMemoryPushNotificationConfigStore |
May be renamed or restructured | agent_to_a2a.py line 155 |
| v0.3 | v1.0 | Impact |
|---|---|---|
A2AClient.send_message() return type |
May change from AsyncGenerator[ClientEvent] |
remote_a2a_agent.py line 669 |
ClientEvent (tuple of Task, update) |
Likely restructured | remote_a2a_agent.py lines 446-510 |
A2ACardResolver |
May have API changes | remote_a2a_agent.py line 253 |
A2AClientFactory / ClientConfig |
May have parameter changes | remote_a2a_agent.py line 237 |
ClientCallContext |
May change from Pydantic to protobuf | agent/config.py, agent/utils.py |
| v0.3 | v1.0 | Impact |
|---|---|---|
AgentCard.url |
Removed; use supported_interfaces |
agent_card_builder.py line 82, remote_a2a_agent.py lines 296-308 |
AgentCard(**json_data) construction |
Protobuf ParseDict or SDK factory |
agent_to_a2a.py line 69, remote_a2a_agent.py line 275 |
AgentCapabilities() default construction |
Protobuf construction | agent_card_builder.py line 65 |
AgentSkill construction |
Protobuf construction | agent_card_builder.py (many sites) |
SecurityScheme type hint |
May change to protobuf type | agent_card_builder.py line 58 |
- Read the a2a-sdk v1.0 migration guide and changelog at a2aproject/a2a-python
- Inventory all v0.3 type usage in ADK -- see the interfaces inventory and breaking changes checklist in this gist
- Check a2a.compat.v0_3 -- determine which imports can be temporarily aliased through the compat layer
- Set up test infrastructure -- ensure all ~12K lines of A2A tests run cleanly on current
mainas the baseline
Goal: Existing tests pass with new SDK version by routing imports through a2a.compat.v0_3.
- Bump
a2a-sdkto>=1.0,<2inpyproject.toml(both main and dev extras) - Add a thin
_compat.pyshim insrc/google/adk/a2a/that re-exports v0.3-compatible types:try: from a2a.compat.v0_3 import Part, TextPart, DataPart, ... except ImportError: from a2a.types import Part, TextPart, DataPart, ...
- Update all imports in
src/google/adk/a2a/to use the shim - Run full test suite -- fix any import errors or immediate breakage
- Success criteria: All existing tests pass with
a2a-sdk>=1.0
Goal: Tests define the v1.0 contract that implementation must satisfy.
Work through test files one at a time, updating:
Part(root=TextPart(...))->Part(text=TextPart(...)).root.text->.text.text(or appropriate field access)TaskState.working->TaskState.TASK_STATE_WORKINGRole.agent->Role.ROLE_AGENT.model_dump()-> protobuf serializationisinstancechecks -> field presence checks
Order (by dependency, easiest first):
test_utils.py(no A2A type changes needed)test_part_converter.py(foundational -- defines part contract)test_log_utils.pytest_event_converter.pytest_event_round_trip.pytest_from_adk.pytest_to_adk.pytest_request_converter.pytest_task_result_aggregator.pytest_a2a_agent_executor.pytest_a2a_agent_executor_impl.pytest_agent_card_builder.pytest_agent_to_a2a.pytest_client_server.py(integration -- last)test_remote_a2a_agent.py
Run tests continuously: after updating each file, run the full suite to catch regressions.
Work through source files in dependency order:
File: src/google/adk/a2a/converters/part_converter.py
This is the most critical file -- every other converter depends on it.
Changes:
convert_a2a_part_to_genai_part(): Remove.rootaccess; usepart.text,part.file,part.datawith field-presence checks instead ofisinstanceconvert_genai_part_to_a2a_part(): ReplacePart(root=...)withPart(text=...),Part(file=...),Part(data=...)- Replace
model_dump_json()/model_validate_json()on A2A types with protobuf serialization - Replace
model_dump()calls onDataPartwith protobuf dict conversion
Files:
converters/event_converter.py(legacy)converters/from_adk_event.py(new impl)converters/to_adk_event.py
Changes:
- All
TaskState.xxx->TaskState.TASK_STATE_XXX - All
Role.xxx->Role.ROLE_XXX - All
.root.metadata->.metadata(if protobuf flattens the union) - All
Part(root=TextPart(...))->Part(text=TextPart(...)) - All
isinstance(a2a_part.root, DataPart)-> field-presence check
File: converters/long_running_functions.py
Changes:
isinstance(a2a_part.root, DataPart)-> field-presence check (3 sites).root.metadataaccess -> direct metadata accessTaskState.input_required/TaskState.auth_required-> SCREAMING_SNAKE_CASERole.agent->Role.ROLE_AGENTPart(root=TextPart(...))->Part(text=TextPart(...))
File: converters/request_converter.py
Changes:
- No direct A2A type construction (uses part_converter)
RequestContextimport may need updating
File: utils/agent_card_builder.py
Changes:
AgentCard(url=..., ...)-> Usesupported_interfacesinstead ofurlAgentCapabilities()-> Protobuf constructionAgentSkill(...)-> Protobuf constructionAgentProvider,SecuritySchemetype hints -> Protobuf types
Files:
executor/a2a_agent_executor.pyexecutor/a2a_agent_executor_impl.pyexecutor/task_result_aggregator.py
Changes:
- All
TaskState.xxxenum values (30+ sites across files) - All
Role.xxxenum values TextPart(text=...)construction for error messagesMessage(...)constructionTaskStatusUpdateEvent(...)constructionTaskArtifactUpdateEvent(...)constructionTask(...)construction (in_A2aAgentExecutor)
File: agents/remote_a2a_agent.py
Changes:
AgentCard.url->supported_interfacesTaskState.submitted,TaskState.working-> SCREAMING_SNAKE_CASERole.user->Role.ROLE_USER.model_dump()on A2A Message/Task -> protobuf serializationA2AMessage(...)construction- Client API changes (if any in v1.0)
File: logs/log_utils.py
Changes:
- All
.root.text,.root.data,.root.metadata-> direct field access isinstancechecks -> field-presence checks ortype().__name__fallbacks.model_dump_json()on A2A Part -> protobuf serialization
File: utils/agent_to_a2a.py
Changes:
A2AStarletteApplication-> new server setup patternAgentCard(**json_data)-> protobuf constructionDefaultRequestHandler-> updated constructorInMemoryTaskStore,InMemoryPushNotificationConfigStore-> updated names/APIs
- Run ADK's full A2A test suite -- all ~12K lines must pass
- Run the integration test (
test_client_server.py) -- validates real HTTP round-trip with v1.0 wire protocol - End-to-end manual test:
to_a2a(agent)-> uvicorn ->RemoteA2aAgent-> verify response - ITK compatibility (bonus): Run the A2A Interoperability Test Kit against an ADK-served agent to validate v1.0 wire-level compliance
- Remove compat layer imports (if used in Phase 1)
- Remove
_compat.pyshim - Update inline documentation and docstrings
- Update sample code in
contributing/samples/a2a_* - Consider removing "experimental" warnings if the API is now stable
| File (relative to repo root) | Changes Needed | Risk | Dependencies |
|---|---|---|---|
src/google/adk/a2a/converters/utils.py |
None (no A2A types) | NONE | -- |
src/google/adk/a2a/converters/part_converter.py |
Part construction (9 sites), isinstance (5), .root access (1 fn), model_dump/validate (6) | CRITICAL | None |
src/google/adk/a2a/converters/event_converter.py |
TaskState enums (8), Role enums (4), .root access (12), Part construction (1) | HIGH | part_converter |
src/google/adk/a2a/converters/from_adk_event.py |
TaskState (2), Role (1), Part construction (1), model_dump (2), .root checks implicit | HIGH | part_converter |
src/google/adk/a2a/converters/to_adk_event.py |
TaskState (1), .root access (13 via part_converter calls), model_dump (4 on EventActions -- safe, these are ADK types) | MEDIUM | part_converter |
src/google/adk/a2a/converters/long_running_functions.py |
TaskState (5), Role (2), isinstance (3), .root access (6), Part construction (2) | HIGH | part_converter |
src/google/adk/a2a/converters/request_converter.py |
Minimal (uses part_converter) | LOW | part_converter |
src/google/adk/a2a/executor/config.py |
Type aliases reference A2A types (no construction) | LOW | converters |
src/google/adk/a2a/executor/executor_context.py |
None (no A2A types) | NONE | -- |
src/google/adk/a2a/executor/task_result_aggregator.py |
TaskState enums (10) | MEDIUM | -- |
src/google/adk/a2a/executor/a2a_agent_executor.py |
TaskState (6), Role (1), TextPart construction (1), Message construction (2) | HIGH | converters, config |
src/google/adk/a2a/executor/a2a_agent_executor_impl.py |
TaskState (5), Role (1), TextPart (1), Message (2), Task (1) | HIGH | converters, config |
src/google/adk/a2a/executor/utils.py |
None (delegates to interceptors) | NONE | config |
src/google/adk/a2a/executor/interceptors/include_artifacts_in_a2a_event.py |
Artifact, TaskArtifactUpdateEvent construction | LOW | part_converter |
src/google/adk/a2a/agent/config.py |
Type aliases (no construction) | LOW | converters |
src/google/adk/a2a/agent/utils.py |
None (delegates to interceptors) | NONE | config |
src/google/adk/a2a/agent/interceptors/new_integration_extension.py |
ClientCallContext, HTTP_EXTENSION_HEADER | LOW | -- |
src/google/adk/a2a/utils/agent_card_builder.py |
AgentCard.url removal, AgentCapabilities/AgentSkill/AgentProvider construction | HIGH | -- |
src/google/adk/a2a/utils/agent_to_a2a.py |
A2AStarletteApplication removal, AgentCard construction | HIGH | agent_card_builder |
src/google/adk/a2a/logs/log_utils.py |
.root access (5), isinstance checks (5), model_dump_json (2) | MEDIUM | -- |
src/google/adk/agents/remote_a2a_agent.py |
AgentCard.url (3), TaskState (4), Role (1), model_dump (5), Message construction (1), client API | HIGH | converters, config |
src/google/adk/a2a/experimental.py |
None | NONE | -- |
The ~12,000 lines of existing tests are the main guardrail. After migrating tests (Phase 2), they define the v1.0 contract. Every implementation change in Phase 3 must keep these tests green.
tests/unittests/a2a/integration/test_client_server.py (638 lines) tests a real HTTP round-trip:
- Spins up a Starlette server with
to_a2a() - Sends messages via A2A client
- Validates responses end-to-end
This is the most valuable single test for catching wire-protocol issues.
The A2A Interoperability Test Kit (a2aproject/a2a-python ITK) tests SDK-to-SDK interop at the wire level. It does NOT test ADK's wrapper layer, but running it against an ADK-served agent validates that ADK produces valid v1.0 responses.
- Property-based tests for Part round-trip conversion (v1.0 types -> GenAI -> v1.0 types)
- Snapshot tests for serialized AgentCard output (to catch regressions in card structure)
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| Protobuf types lack Pydantic helpers (model_dump, model_validate) | CERTAIN | HIGH | Write thin wrappers or use protobuf's MessageToDict/ParseDict |
Part.root removal breaks 80+ access sites |
CERTAIN | HIGH | Systematic find-replace with field-presence checks |
A2AStarletteApplication removal breaks to_a2a() |
LIKELY | MEDIUM | Follow v1.0 SDK server setup docs; may need significant rewrite of agent_to_a2a.py |
AgentCard.url removal breaks card construction + validation |
CERTAIN | MEDIUM | Switch to supported_interfaces |
| Enum value changes break 40+ comparisons | CERTAIN | MEDIUM | Systematic find-replace; create enum mapping if compat needed |
Client API changes break RemoteA2aAgent |
LIKELY | MEDIUM | Review v1.0 client API; may need restructuring of response handling |
| Compat layer doesn't cover all patterns | LIKELY | LOW | Identified upfront; compat layer is Phase 1 bridge only |
| Test mocking of A2A types breaks with protobuf | LIKELY | MEDIUM | Protobuf objects may not support Mock specs; update test patterns |
| Protobuf objects not hashable/comparable the same way | POSSIBLE | LOW | Update assertions to compare field-by-field |
- All existing A2A tests pass with
a2a-sdk>=1.0types (no compat imports) - Integration test (
test_client_server.py) passes with v1.0 wire protocol - No breaking changes to ADK's public API signatures:
to_a2a()function signature unchangedRemoteA2aAgentconstructor unchangedA2aAgentExecutorconstructor unchangedA2aAgentExecutorConfigfields unchangedAgentCardBuilderconstructor unchanged
to_a2a()produces valid v1.0 agent cards- Sample apps in
contributing/samples/a2a_*updated and working - ITK compatibility validated (stretch goal)
- ADK: google/adk-python
- A2A SDK: a2aproject/a2a-python
- A2A Protocol Spec: a2aproject/a2a-spec