Skip to content

Commit 6f0dcb3

Browse files
google-genai-botcopybara-github
authored andcommitted
feat: Add a new extension for the new version of ADK-A2A integration
This change introduces a new interceptor that adds the 'https://google.github.io/adk-docs/a2a/a2a-extension/' extension to the request headers in the A2A client from the RemoteAgent side. To send this extension along with requests, the RemoteAgent has to be instantiated with the `use_legacy` flag set to False. The AgentExecutor will default to the new implementation when this extension is requested by the client, but this behavior can be disabled via the `use_legacy` flag. The 'force_new' flag on the agent_executor side can be used to bypass the presence of the extension, and always activate the new version of the agent_executor. PiperOrigin-RevId: 883021792
1 parent 780093f commit 6f0dcb3

File tree

7 files changed

+115
-14
lines changed

7 files changed

+115
-14
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
"""Interceptor that injects the new agent version extension."""
15+
16+
from __future__ import annotations
17+
18+
from typing import Union
19+
20+
from a2a.client.middleware import ClientCallContext
21+
from a2a.extensions.common import HTTP_EXTENSION_HEADER
22+
from a2a.types import Message as A2AMessage
23+
from google.adk.a2a.agent.config import ParametersConfig
24+
from google.adk.a2a.agent.config import RequestInterceptor
25+
from google.adk.agents.invocation_context import InvocationContext
26+
from google.adk.events.event import Event
27+
28+
_NEW_A2A_ADK_INTEGRATION_EXTENSION = (
29+
'https://google.github.io/adk-docs/a2a/a2a-extension/'
30+
)
31+
32+
33+
async def _before_request(
34+
_: InvocationContext,
35+
a2a_request: A2AMessage,
36+
params: ParametersConfig,
37+
) -> tuple[Union[A2AMessage, Event], ParametersConfig]:
38+
"""Adds A2A_new_agent_version to client_call_context."""
39+
if params.client_call_context is None:
40+
params.client_call_context = ClientCallContext()
41+
42+
http_kwargs = params.client_call_context.state.get('http_kwargs', {})
43+
headers = http_kwargs.get('headers', {})
44+
a2a_extensions = headers.get(HTTP_EXTENSION_HEADER, '').split(',')
45+
a2a_extensions = [ext for ext in a2a_extensions if ext]
46+
if _NEW_A2A_ADK_INTEGRATION_EXTENSION not in a2a_extensions:
47+
a2a_extensions.append(_NEW_A2A_ADK_INTEGRATION_EXTENSION)
48+
headers[HTTP_EXTENSION_HEADER] = ','.join(a2a_extensions)
49+
http_kwargs['headers'] = headers
50+
params.client_call_context.state['http_kwargs'] = http_kwargs
51+
return a2a_request, params
52+
53+
54+
_new_integration_extension_interceptor = RequestInterceptor(
55+
before_request=_before_request
56+
)

src/google/adk/a2a/executor/a2a_agent_executor.py

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939
from typing_extensions import override
4040

4141
from ...utils.context_utils import Aclosing
42+
from ..agent.interceptors.new_integration_extension import _NEW_A2A_ADK_INTEGRATION_EXTENSION
4243
from ..converters.request_converter import AgentRunRequest
4344
from ..converters.utils import _get_adk_metadata_key
4445
from ..experimental import a2a_experimental
@@ -62,23 +63,25 @@ class A2aAgentExecutor(AgentExecutor):
6263
Args:
6364
runner: The runner to use for the agent.
6465
config: The config to use for the executor.
65-
use_legacy: Whether to use the legacy executor implementation.
66+
use_legacy: If true, force the legacy implementation.
67+
force_new_version: If true, force the new implementation regardless of the
68+
extension.
6669
"""
6770

6871
def __init__(
6972
self,
7073
*,
7174
runner: Runner | Callable[..., Runner | Awaitable[Runner]],
7275
config: Optional[A2aAgentExecutorConfig] = None,
73-
use_legacy: bool = True,
76+
use_legacy: bool = False,
77+
force_new_version: bool = False,
7478
):
7579
super().__init__()
76-
if not use_legacy:
77-
self._executor_impl = ExecutorImpl(runner=runner, config=config)
78-
else:
79-
self._executor_impl = None
80-
self._runner = runner
81-
self._config = config or A2aAgentExecutorConfig()
80+
self._runner = runner
81+
self._config = config or A2aAgentExecutorConfig()
82+
self._use_legacy = use_legacy
83+
self._force_new_version = force_new_version
84+
self._executor_impl = None
8285

8386
async def _resolve_runner(self) -> Runner:
8487
"""Resolve the runner, handling cases where it's a callable that returns a Runner."""
@@ -129,7 +132,16 @@ async def execute(
129132
* Converts the ADK output events into A2A task updates
130133
* Publishes the updates back to A2A server via event queue
131134
"""
132-
if self._executor_impl:
135+
should_use_new_impl = not self._use_legacy and (
136+
self._force_new_version or self._check_new_version_extension(context)
137+
)
138+
139+
if should_use_new_impl:
140+
if self._executor_impl is None:
141+
self._executor_impl = ExecutorImpl(
142+
runner=self._runner,
143+
config=self._config,
144+
)
133145
await self._executor_impl.execute(context, event_queue)
134146
return
135147

@@ -338,3 +350,10 @@ async def _prepare_session(
338350
run_request.session_id = session.id
339351

340352
return session
353+
354+
def _check_new_version_extension(self, context: RequestContext):
355+
"""Check if the extension for the new version is requested and activate it."""
356+
if _NEW_A2A_ADK_INTEGRATION_EXTENSION in context.requested_extensions:
357+
context.add_activated_extension(_NEW_A2A_ADK_INTEGRATION_EXTENSION)
358+
return True
359+
return False

src/google/adk/a2a/executor/a2a_agent_executor_impl.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@
3939

4040
from ...runners import Runner
4141
from ...utils.context_utils import Aclosing
42+
from ..agent.interceptors.new_integration_extension import _NEW_A2A_ADK_INTEGRATION_EXTENSION
4243
from ..converters.from_adk_event import create_error_status_event
4344
from ..converters.long_running_functions import handle_user_input
4445
from ..converters.long_running_functions import LongRunningFunctions
@@ -306,5 +307,5 @@ def _get_invocation_metadata(
306307
_get_adk_metadata_key('session_id'): executor_context.session_id,
307308
# TODO: Remove this metadata once the new agent executor
308309
# is fully adopted.
309-
_get_adk_metadata_key('agent_executor_v2'): True,
310+
_NEW_A2A_ADK_INTEGRATION_EXTENSION: {'adk_agent_executor_v2': True},
310311
}

src/google/adk/agents/remote_a2a_agent.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,8 @@
5454
AGENT_CARD_WELL_KNOWN_PATH = "/.well-known/agent.json"
5555

5656
from ..a2a.agent.config import A2aRemoteAgentConfig
57+
from ..a2a.agent.interceptors.new_integration_extension import _NEW_A2A_ADK_INTEGRATION_EXTENSION
58+
from ..a2a.agent.interceptors.new_integration_extension import _new_integration_extension_interceptor
5759
from ..a2a.agent.utils import execute_after_request_interceptors
5860
from ..a2a.agent.utils import execute_before_request_interceptors
5961
from ..a2a.converters.event_converter import convert_a2a_message_to_event
@@ -135,6 +137,7 @@ def __init__(
135137
] = None,
136138
full_history_when_stateless: bool = False,
137139
config: Optional[A2aRemoteAgentConfig] = None,
140+
use_legacy: bool = True,
138141
**kwargs: Any,
139142
) -> None:
140143
"""Initialize RemoteA2aAgent.
@@ -156,6 +159,8 @@ def __init__(
156159
request. If False, the default behavior of sending only events since the
157160
last reply from the agent will be used.
158161
config: Optional configuration object.
162+
use_legacy: If false, send request to the server including the extension
163+
indicating that the server should use the new implementation.
159164
**kwargs: Additional arguments passed to BaseAgent
160165
161166
Raises:
@@ -185,6 +190,13 @@ def __init__(
185190
self._full_history_when_stateless = full_history_when_stateless
186191
self._config = config or A2aRemoteAgentConfig()
187192

193+
if not use_legacy:
194+
if self._config.request_interceptors is None:
195+
self._config.request_interceptors = []
196+
self._config.request_interceptors.append(
197+
_new_integration_extension_interceptor
198+
)
199+
188200
# Validate and store agent card reference
189201
if isinstance(agent_card, AgentCard):
190202
self._agent_card = agent_card
@@ -669,9 +681,7 @@ async def _run_async_impl(
669681
else:
670682
metadata = a2a_response.metadata
671683

672-
if metadata and metadata.get(
673-
_get_adk_metadata_key("agent_executor_v2")
674-
):
684+
if metadata and metadata.get(_NEW_A2A_ADK_INTEGRATION_EXTENSION):
675685
event = await self._handle_a2a_response_v2(a2a_response, ctx)
676686
else:
677687
event = await self._handle_a2a_response(a2a_response, ctx)

tests/unittests/a2a/executor/test_a2a_agent_executor.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ def setup_method(self):
6666
self.mock_context.current_task = None
6767
self.mock_context.task_id = "test-task-id"
6868
self.mock_context.context_id = "test-context-id"
69+
self.mock_context.requested_extensions = []
6970

7071
self.mock_event_queue = Mock(spec=EventQueue)
7172

tests/unittests/a2a/executor/test_a2a_agent_executor_impl.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from google.adk.a2a.converters.request_converter import AgentRunRequest
3030
from google.adk.a2a.converters.utils import _get_adk_metadata_key
3131
from google.adk.a2a.executor.a2a_agent_executor_impl import _A2aAgentExecutor as A2aAgentExecutor
32+
from google.adk.a2a.executor.a2a_agent_executor_impl import _NEW_A2A_ADK_INTEGRATION_EXTENSION
3233
from google.adk.a2a.executor.a2a_agent_executor_impl import A2aAgentExecutorConfig
3334
from google.adk.a2a.executor.config import ExecuteInterceptor
3435
from google.adk.events.event import Event
@@ -77,7 +78,7 @@ def setup_method(self):
7778
_get_adk_metadata_key("app_name"): "test-app",
7879
_get_adk_metadata_key("user_id"): "test-user",
7980
_get_adk_metadata_key("session_id"): "test-session",
80-
_get_adk_metadata_key("agent_executor_v2"): True,
81+
_NEW_A2A_ADK_INTEGRATION_EXTENSION: {"adk_agent_executor_v2": True},
8182
}
8283

8384
async def _create_async_generator(self, items):

0 commit comments

Comments
 (0)