Skip to content

Commit 87fcd77

Browse files
google-genai-botcopybara-github
authored andcommitted
feat: Add interceptor framework to A2aAgentExecutor
This change introduces an interceptor mechanism allowing custom logic to be executed before agent runs, after each event, and after the agent run completes. New dependencies are added to support these features. PiperOrigin-RevId: 873952199
1 parent 7557a92 commit 87fcd77

File tree

5 files changed

+348
-21
lines changed

5 files changed

+348
-21
lines changed

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

Lines changed: 51 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,12 @@
5050
from ..converters.request_converter import convert_a2a_request_to_agent_run_request
5151
from ..converters.utils import _get_adk_metadata_key
5252
from ..experimental import a2a_experimental
53+
from .config import ExecuteInterceptor
54+
from .executor_context import ExecutorContext
5355
from .task_result_aggregator import TaskResultAggregator
56+
from .utils import execute_after_agent_interceptors
57+
from .utils import execute_after_event_interceptors
58+
from .utils import execute_before_agent_interceptors
5459

5560
logger = logging.getLogger('google_adk.' + __name__)
5661

@@ -70,6 +75,8 @@ class A2aAgentExecutorConfig(BaseModel):
7075
)
7176
event_converter: AdkEventToA2AEventsConverter = convert_event_to_a2a_events
7277

78+
execute_interceptors: Optional[list[ExecuteInterceptor]] = None
79+
7380

7481
@a2a_experimental
7582
class A2aAgentExecutor(AgentExecutor):
@@ -135,6 +142,10 @@ async def execute(
135142
if not context.message:
136143
raise ValueError('A2A request must have a message')
137144

145+
context = await execute_before_agent_interceptors(
146+
context, self._config.execute_interceptors
147+
)
148+
138149
# for new task, create a task submitted event
139150
if not context.current_task:
140151
await event_queue.enqueue_event(
@@ -202,6 +213,13 @@ async def _handle_request(
202213
run_config=run_request.run_config,
203214
)
204215

216+
self._executor_context = ExecutorContext(
217+
app_name=runner.app_name,
218+
user_id=run_request.user_id,
219+
session_id=run_request.session_id,
220+
runner=runner,
221+
)
222+
205223
# publish the task working event
206224
await event_queue.enqueue_event(
207225
TaskStatusUpdateEvent(
@@ -230,6 +248,15 @@ async def _handle_request(
230248
context.context_id,
231249
self._config.gen_ai_part_converter,
232250
):
251+
a2a_event = await execute_after_event_interceptors(
252+
a2a_event,
253+
self._executor_context,
254+
adk_event,
255+
self._config.execute_interceptors,
256+
)
257+
if a2a_event is None:
258+
continue
259+
233260
task_result_aggregator.process_event(a2a_event)
234261
await event_queue.enqueue_event(a2a_event)
235262

@@ -253,31 +280,34 @@ async def _handle_request(
253280
)
254281
)
255282
# public the final status update event
256-
await event_queue.enqueue_event(
257-
TaskStatusUpdateEvent(
258-
task_id=context.task_id,
259-
status=TaskStatus(
260-
state=TaskState.completed,
261-
timestamp=datetime.now(timezone.utc).isoformat(),
262-
),
263-
context_id=context.context_id,
264-
final=True,
265-
)
283+
final_event = TaskStatusUpdateEvent(
284+
task_id=context.task_id,
285+
status=TaskStatus(
286+
state=TaskState.completed,
287+
timestamp=datetime.now(timezone.utc).isoformat(),
288+
),
289+
context_id=context.context_id,
290+
final=True,
266291
)
267292
else:
268-
await event_queue.enqueue_event(
269-
TaskStatusUpdateEvent(
270-
task_id=context.task_id,
271-
status=TaskStatus(
272-
state=task_result_aggregator.task_state,
273-
timestamp=datetime.now(timezone.utc).isoformat(),
274-
message=task_result_aggregator.task_status_message,
275-
),
276-
context_id=context.context_id,
277-
final=True,
278-
)
293+
final_event = TaskStatusUpdateEvent(
294+
task_id=context.task_id,
295+
status=TaskStatus(
296+
state=task_result_aggregator.task_state,
297+
timestamp=datetime.now(timezone.utc).isoformat(),
298+
message=task_result_aggregator.task_status_message,
299+
),
300+
context_id=context.context_id,
301+
final=True,
279302
)
280303

304+
final_event = await execute_after_agent_interceptors(
305+
self._executor_context,
306+
final_event,
307+
self._config.execute_interceptors,
308+
)
309+
await event_queue.enqueue_event(final_event)
310+
281311
async def _prepare_session(
282312
self,
283313
context: RequestContext,
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
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+
15+
from __future__ import annotations
16+
17+
import dataclasses
18+
from typing import Awaitable
19+
from typing import Callable
20+
from typing import Optional
21+
from typing import Union
22+
23+
from a2a.server.agent_execution.context import RequestContext
24+
from a2a.server.events import Event as A2AEvent
25+
from a2a.types import TaskStatusUpdateEvent
26+
27+
from ...events.event import Event
28+
from ..converters.utils import _get_adk_metadata_key
29+
from .executor_context import ExecutorContext
30+
31+
32+
@dataclasses.dataclass
33+
class ExecuteInterceptor:
34+
"""Interceptor for the A2aAgentExecutor."""
35+
36+
before_agent: Optional[
37+
Callable[[RequestContext], Awaitable[RequestContext]]
38+
] = None
39+
"""Hook executed before the agent starts processing the request.
40+
41+
Allows inspection or modification of the incoming request context.
42+
Must return a valid `RequestContext` to continue execution.
43+
"""
44+
45+
after_event: Optional[
46+
Callable[
47+
[ExecutorContext, A2AEvent, Event],
48+
Awaitable[Union[A2AEvent, None]],
49+
]
50+
] = None
51+
"""Hook executed after an ADK event is converted to an A2A event.
52+
53+
Allows mutating the outgoing event before it is enqueued.
54+
Return `None` to filter out and drop the event entirely,
55+
which also halts any subsequent interceptors in the chain.
56+
"""
57+
58+
after_agent: Optional[
59+
Callable[
60+
[ExecutorContext, TaskStatusUpdateEvent],
61+
Awaitable[TaskStatusUpdateEvent],
62+
]
63+
] = None
64+
"""Hook executed after the agent finishes and the final event is prepared.
65+
66+
Allows inspection or modification of the terminal status event (e.g.,
67+
completed or failed) before it is enqueued. Must return a valid
68+
`TaskStatusUpdateEvent`.
69+
"""
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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+
15+
from __future__ import annotations
16+
17+
from google.adk.runners import Runner
18+
19+
20+
class ExecutorContext:
21+
"""Context for the executor."""
22+
23+
def __init__(
24+
self,
25+
app_name: str,
26+
user_id: str,
27+
session_id: str,
28+
runner: Runner,
29+
):
30+
self._app_name = app_name
31+
self._user_id = user_id
32+
self._session_id = session_id
33+
self._runner = runner
34+
35+
@property
36+
def app_name(self) -> str:
37+
return self._app_name
38+
39+
@property
40+
def user_id(self) -> str:
41+
return self._user_id
42+
43+
@property
44+
def session_id(self) -> str:
45+
return self._session_id
46+
47+
@property
48+
def runner(self) -> Runner:
49+
return self._runner
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
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+
from __future__ import annotations
15+
16+
from typing import Optional
17+
18+
from a2a.server.agent_execution.context import RequestContext
19+
from a2a.server.events import Event as A2AEvent
20+
from a2a.types import TaskStatusUpdateEvent
21+
22+
from ...events.event import Event
23+
from ..converters.utils import _get_adk_metadata_key
24+
from .config import ExecuteInterceptor
25+
from .executor_context import ExecutorContext
26+
27+
28+
async def execute_before_agent_interceptors(
29+
context: RequestContext,
30+
execute_interceptors: Optional[list[ExecuteInterceptor]],
31+
) -> RequestContext:
32+
if execute_interceptors:
33+
for interceptor in execute_interceptors:
34+
if interceptor.before_agent:
35+
context = await interceptor.before_agent(context)
36+
return context
37+
38+
39+
async def execute_after_event_interceptors(
40+
a2a_event: A2AEvent,
41+
executor_context: ExecutorContext,
42+
adk_event: Event,
43+
execute_interceptors: Optional[list[ExecuteInterceptor]],
44+
) -> Optional[A2AEvent]:
45+
if execute_interceptors:
46+
for interceptor in execute_interceptors:
47+
if interceptor.after_event:
48+
a2a_event = await interceptor.after_event(
49+
executor_context, a2a_event, adk_event
50+
)
51+
if a2a_event is None:
52+
return None
53+
return a2a_event
54+
55+
56+
async def execute_after_agent_interceptors(
57+
executor_context: ExecutorContext,
58+
final_event: TaskStatusUpdateEvent,
59+
execute_interceptors: Optional[list[ExecuteInterceptor]],
60+
) -> TaskStatusUpdateEvent:
61+
if execute_interceptors:
62+
for interceptor in reversed(execute_interceptors):
63+
if interceptor.after_agent:
64+
final_event = await interceptor.after_agent(
65+
executor_context, final_event
66+
)
67+
return final_event

0 commit comments

Comments
 (0)