Skip to content

Commit d39ec54

Browse files
authored
Merge branch 'main' into fix/a2a-inbound-role-mapping
2 parents f22ca7d + cb4dd42 commit d39ec54

11 files changed

Lines changed: 233 additions & 30 deletions

File tree

src/google/adk/a2a/converters/from_adk_event.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -218,6 +218,23 @@ def convert_event_to_a2a_events(
218218
),
219219
)
220220
)
221+
elif _serialize_value(event.actions) is not None:
222+
a2a_events.append(
223+
TaskStatusUpdateEvent(
224+
task_id=task_id,
225+
context_id=context_id,
226+
status=TaskStatus(
227+
state=TaskState.working,
228+
message=Message(
229+
message_id=str(uuid.uuid4()),
230+
role=Role.agent,
231+
parts=[],
232+
),
233+
timestamp=datetime.now(timezone.utc).isoformat(),
234+
),
235+
final=False,
236+
)
237+
)
221238

222239
a2a_events = _add_event_metadata(event, a2a_events)
223240
return a2a_events
@@ -280,7 +297,10 @@ def _add_event_metadata(
280297
metadata[_get_adk_metadata_key(field_name)] = value
281298

282299
for a2a_event in a2a_events:
283-
if isinstance(a2a_event, TaskStatusUpdateEvent):
300+
if (
301+
isinstance(a2a_event, TaskStatusUpdateEvent)
302+
and a2a_event.status.message
303+
):
284304
a2a_event.status.message.metadata = metadata.copy()
285305
elif isinstance(a2a_event, TaskArtifactUpdateEvent):
286306
a2a_event.artifact.metadata = metadata.copy()

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

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -260,17 +260,15 @@ async def _handle_request(
260260
context.context_id,
261261
self._config.gen_ai_part_converter,
262262
):
263-
a2a_event = await execute_after_event_interceptors(
263+
a2a_events = await execute_after_event_interceptors(
264264
a2a_event,
265265
executor_context,
266266
adk_event,
267267
self._config.execute_interceptors,
268268
)
269-
if a2a_event is None:
270-
continue
271-
272-
task_result_aggregator.process_event(a2a_event)
273-
await event_queue.enqueue_event(a2a_event)
269+
for e in a2a_events:
270+
task_result_aggregator.process_event(e)
271+
await event_queue.enqueue_event(e)
274272

275273
# publish the task result event - this is final
276274
if (

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@
4949
from ..experimental import a2a_experimental
5050
from .config import A2aAgentExecutorConfig
5151
from .executor_context import ExecutorContext
52+
from .interceptors.include_artifacts_in_a2a_event import include_artifacts_in_a2a_event_interceptor
5253
from .utils import execute_after_agent_interceptors
5354
from .utils import execute_after_event_interceptors
5455
from .utils import execute_before_agent_interceptors
@@ -221,15 +222,14 @@ async def _handle_request(
221222
self._config.gen_ai_part_converter,
222223
):
223224
a2a_event.metadata = self._get_invocation_metadata(executor_context)
224-
a2a_event = await execute_after_event_interceptors(
225+
a2a_events = await execute_after_event_interceptors(
225226
a2a_event,
226227
executor_context,
227228
adk_event,
228229
self._config.execute_interceptors,
229230
)
230-
if not a2a_event:
231-
continue
232-
await event_queue.enqueue_event(a2a_event)
231+
for e in a2a_events:
232+
await event_queue.enqueue_event(e)
233233

234234
if error_event:
235235
final_event = error_event

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ class ExecuteInterceptor:
5757
after_event: Optional[
5858
Callable[
5959
[ExecutorContext, A2AEvent, Event],
60-
Awaitable[Union[A2AEvent, None]],
60+
Awaitable[Union[A2AEvent, list[A2AEvent], None]],
6161
]
6262
] = None
6363
"""Hook executed after an ADK event is converted to an A2A event.
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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 .include_artifacts_in_a2a_event import include_artifacts_in_a2a_event_interceptor
16+
17+
__all__ = [
18+
"include_artifacts_in_a2a_event_interceptor",
19+
]
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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 Union
17+
18+
from a2a.server.events import Event as A2AEvent
19+
from a2a.types import Artifact
20+
from a2a.types import TaskArtifactUpdateEvent
21+
from a2a.types import TaskStatusUpdateEvent
22+
from google.adk.a2a.executor.config import ExecuteInterceptor
23+
from google.adk.a2a.executor.config import ExecutorContext
24+
25+
from ....events.event import Event
26+
from ...converters.part_converter import convert_genai_part_to_a2a_part
27+
28+
29+
async def _after_agent(
30+
ctx: ExecutorContext, a2a_event: A2AEvent, adk_event: Event
31+
) -> Union[A2AEvent, list[A2AEvent]]:
32+
"""After agent interceptor that includes artifacts in A2A events."""
33+
if isinstance(a2a_event, (TaskStatusUpdateEvent, TaskArtifactUpdateEvent)):
34+
artifact_service = ctx.runner.artifact_service
35+
if artifact_service and adk_event.actions.artifact_delta:
36+
new_events = []
37+
for filename, version in adk_event.actions.artifact_delta.items():
38+
genai_part = await artifact_service.load_artifact(
39+
app_name=ctx.app_name,
40+
user_id=ctx.user_id,
41+
session_id=ctx.session_id,
42+
filename=filename,
43+
version=version,
44+
)
45+
if genai_part:
46+
a2a_part = convert_genai_part_to_a2a_part(genai_part)
47+
if a2a_part:
48+
a2a_artifact = Artifact(
49+
artifact_id=f"{filename}_{version}",
50+
name=filename,
51+
parts=[a2a_part],
52+
)
53+
new_event = TaskArtifactUpdateEvent(
54+
task_id=a2a_event.task_id,
55+
context_id=a2a_event.context_id,
56+
artifact=a2a_artifact,
57+
metadata=a2a_event.metadata,
58+
append=False,
59+
last_chunk=True,
60+
)
61+
new_events.append(new_event)
62+
63+
adk_event.actions.artifact_delta = {}
64+
65+
if new_events:
66+
return [a2a_event] + new_events
67+
68+
return a2a_event
69+
70+
71+
include_artifacts_in_a2a_event_interceptor = ExecuteInterceptor(
72+
after_event=_after_agent
73+
)

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

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -41,16 +41,24 @@ async def execute_after_event_interceptors(
4141
executor_context: ExecutorContext,
4242
adk_event: Event,
4343
execute_interceptors: Optional[list[ExecuteInterceptor]],
44-
) -> Optional[A2AEvent]:
44+
) -> list[A2AEvent]:
45+
events = [a2a_event]
4546
if execute_interceptors:
4647
for interceptor in execute_interceptors:
4748
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
49+
next_events = []
50+
for e in events:
51+
res = await interceptor.after_event(executor_context, e, adk_event)
52+
if res is None:
53+
continue
54+
if isinstance(res, list):
55+
next_events.extend(res)
56+
else:
57+
next_events.append(res)
58+
events = next_events
59+
if not events:
60+
return []
61+
return events
5462

5563

5664
async def execute_after_agent_interceptors(

src/google/adk/cli/adk_web_server.py

Lines changed: 5 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -706,14 +706,10 @@ async def get_runner_async(self, app_name: str) -> Runner:
706706
if plugins_config and isinstance(plugins_config, dict):
707707
bq_analytics_config = plugins_config.get("bigquery_agent_analytics")
708708

709-
# Determine if the agent was loaded from YAML based on the agent loader info
710-
is_visual_builder = False
711-
detailed_agents = self.agent_loader.list_agents_detailed()
712-
for agent_info in detailed_agents:
713-
if agent_info.get("name") == app_name:
714-
if agent_info.get("language") == "yaml":
715-
is_visual_builder = True
716-
break
709+
# All YAML agents are treated as visual builder agents.
710+
is_visual_builder_agent = os.path.exists(
711+
os.path.join(self.agents_dir, app_name, "root_agent.yaml")
712+
)
717713

718714
if isinstance(agent_or_app, BaseAgent):
719715
plugins = extra_plugins_instances
@@ -746,7 +742,7 @@ async def get_runner_async(self, app_name: str) -> Runner:
746742
agentic_app = agent_or_app
747743

748744
# If the root agent was loaded from YAML, we treat it as being from Visual Builder
749-
if is_visual_builder:
745+
if is_visual_builder_agent:
750746
object.__setattr__(agentic_app, "_is_visual_builder_app", True)
751747

752748
runner = self._create_runner(agentic_app)

tests/unittests/a2a/integration/server.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
"""A2A Server for integration tests."""
1616

17+
from unittest.mock import AsyncMock
1718
from unittest.mock import Mock
1819

1920
from a2a.server.apps.jsonrpc.fastapi_app import A2AFastAPIApplication
@@ -23,9 +24,12 @@
2324
from a2a.types import AgentCard
2425
from a2a.types import AgentSkill
2526
from google.adk.a2a.executor.a2a_agent_executor import A2aAgentExecutor
27+
from google.adk.a2a.executor.config import A2aAgentExecutorConfig
28+
from google.adk.a2a.executor.interceptors.include_artifacts_in_a2a_event import include_artifacts_in_a2a_event_interceptor
2629
from google.adk.agents.base_agent import BaseAgent
2730
from google.adk.runners import Runner
2831
from google.adk.sessions.in_memory_session_service import InMemorySessionService
32+
from google.genai import types
2933

3034

3135
class FakeRunner(Runner):
@@ -43,6 +47,12 @@ def __init__(self, run_async_fn):
4347
)
4448
self.run_async_fn = run_async_fn
4549

50+
mock_artifact_service = Mock()
51+
mock_artifact_service.load_artifact = AsyncMock(
52+
return_value=types.Part(text="artifact content")
53+
)
54+
self.artifact_service = mock_artifact_service
55+
4656
async def run_async(self, **kwargs):
4757
async for event in self.run_async_fn(**kwargs):
4858
yield event
@@ -63,18 +73,21 @@ async def run_async(self, **kwargs):
6373
)
6474

6575

66-
def create_server_app(run_async_fn):
76+
def create_server_app(
77+
run_async_fn=None, config: A2aAgentExecutorConfig | None = None
78+
):
6779
"""Creates an A2A FastAPI application with a mocked runner.
6880
6981
Args:
7082
run_async_fn: A generator function that takes **kwargs and yields Event
7183
objects.
84+
include_artifacts: Whether to include artifacts in A2A events.
7285
7386
Returns:
7487
A FastAPI application instance.
7588
"""
7689
runner = FakeRunner(run_async_fn)
77-
executor = A2aAgentExecutor(runner=runner)
90+
executor = A2aAgentExecutor(runner=runner, config=config)
7891
task_store = InMemoryTaskStore()
7992
handler = DefaultRequestHandler(
8093
agent_executor=executor, task_store=task_store

0 commit comments

Comments
 (0)