From a7e5b8b6b4d24b7eaa8fbbade66cdb5112a92572 Mon Sep 17 00:00:00 2001 From: guillaume blaquiere Date: Mon, 17 Nov 2025 20:44:49 +0100 Subject: [PATCH 1/7] fix: agent engine memory service with A2A endpoint activated. --- src/google/adk/cli/fast_api.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/google/adk/cli/fast_api.py b/src/google/adk/cli/fast_api.py index eec6bb646b..95ddd570f1 100644 --- a/src/google/adk/cli/fast_api.py +++ b/src/google/adk/cli/fast_api.py @@ -14,6 +14,7 @@ from __future__ import annotations +import copy import json import logging import os @@ -350,7 +351,13 @@ def create_a2a_runner_loader(captured_app_name: str): """Factory function to create A2A runner with proper closure.""" async def _get_a2a_runner_async() -> Runner: - return await adk_web_server.get_runner_async(captured_app_name) + original_runner = await adk_web_server.get_runner_async(captured_app_name) + runner = copy.copy(original_runner) # Create a shallow copy + runner.memory_service = InMemoryMemoryService() + runner.session_service = InMemorySessionService() + runner.artifact_service = InMemoryArtifactService() + runner.credential_service = InMemoryCredentialService() + return runner return _get_a2a_runner_async From 252f676d16d73030e87062dab8e2ba448afd19b4 Mon Sep 17 00:00:00 2001 From: guillaume blaquiere Date: Mon, 17 Nov 2025 20:49:18 +0100 Subject: [PATCH 2/7] Update src/google/adk/cli/fast_api.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/google/adk/cli/fast_api.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/google/adk/cli/fast_api.py b/src/google/adk/cli/fast_api.py index 95ddd570f1..20bec8cd03 100644 --- a/src/google/adk/cli/fast_api.py +++ b/src/google/adk/cli/fast_api.py @@ -352,11 +352,13 @@ def create_a2a_runner_loader(captured_app_name: str): async def _get_a2a_runner_async() -> Runner: original_runner = await adk_web_server.get_runner_async(captured_app_name) - runner = copy.copy(original_runner) # Create a shallow copy - runner.memory_service = InMemoryMemoryService() - runner.session_service = InMemorySessionService() - runner.artifact_service = InMemoryArtifactService() - runner.credential_service = InMemoryCredentialService() + runner = Runner( + app=original_runner.app, + session_service=InMemorySessionService(), + artifact_service=InMemoryArtifactService(), + memory_service=InMemoryMemoryService(), + credential_service=InMemoryCredentialService(), + ) return runner return _get_a2a_runner_async From 963c8453b81ea28156fec6bee716a48ee96bcf52 Mon Sep 17 00:00:00 2001 From: guillaume blaquiere Date: Tue, 18 Nov 2025 22:52:46 +0100 Subject: [PATCH 3/7] chore: reformating --- src/google/adk/cli/fast_api.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/google/adk/cli/fast_api.py b/src/google/adk/cli/fast_api.py index 95ddd570f1..baf0e3c680 100644 --- a/src/google/adk/cli/fast_api.py +++ b/src/google/adk/cli/fast_api.py @@ -351,7 +351,9 @@ def create_a2a_runner_loader(captured_app_name: str): """Factory function to create A2A runner with proper closure.""" async def _get_a2a_runner_async() -> Runner: - original_runner = await adk_web_server.get_runner_async(captured_app_name) + original_runner = await adk_web_server.get_runner_async( + captured_app_name + ) runner = copy.copy(original_runner) # Create a shallow copy runner.memory_service = InMemoryMemoryService() runner.session_service = InMemorySessionService() From c595c3c77adb368c8404ea6935e46efd9ec15f49 Mon Sep 17 00:00:00 2001 From: guillaume blaquiere Date: Sun, 23 Nov 2025 09:54:44 +0100 Subject: [PATCH 4/7] test: add tests --- src/google/adk/cli/fast_api.py | 9 ++- tests/unittests/cli/test_fast_api.py | 100 +++++++++++++++++++++++++++ 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/src/google/adk/cli/fast_api.py b/src/google/adk/cli/fast_api.py index 84ce8df3da..e32a88c3f6 100644 --- a/src/google/adk/cli/fast_api.py +++ b/src/google/adk/cli/fast_api.py @@ -354,12 +354,19 @@ async def _get_a2a_runner_async() -> Runner: original_runner = await adk_web_server.get_runner_async( captured_app_name ) + kwargs = {} + if original_runner.app: + kwargs["app"] = original_runner.app + else: + kwargs["app_name"] = original_runner.app_name + kwargs["agent"] = original_runner.agent + runner = Runner( - app=original_runner.app, session_service=InMemorySessionService(), artifact_service=InMemoryArtifactService(), memory_service=InMemoryMemoryService(), credential_service=InMemoryCredentialService(), + **kwargs, ) return runner diff --git a/tests/unittests/cli/test_fast_api.py b/tests/unittests/cli/test_fast_api.py index 2d7b9472ba..54c497430b 100755 --- a/tests/unittests/cli/test_fast_api.py +++ b/tests/unittests/cli/test_fast_api.py @@ -30,6 +30,8 @@ from google.adk.agents.base_agent import BaseAgent from google.adk.agents.run_config import RunConfig from google.adk.apps.app import App +from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService +from google.adk.auth.credential_service.in_memory_credential_service import InMemoryCredentialService from google.adk.cli.fast_api import get_fast_api_app from google.adk.evaluation.eval_case import EvalCase from google.adk.evaluation.eval_case import Invocation @@ -38,6 +40,7 @@ from google.adk.evaluation.in_memory_eval_sets_manager import InMemoryEvalSetsManager from google.adk.events.event import Event from google.adk.events.event_actions import EventActions +from google.adk.memory.in_memory_memory_service import InMemoryMemoryService from google.adk.runners import Runner from google.adk.sessions.in_memory_session_service import InMemorySessionService from google.adk.sessions.session import Session @@ -963,6 +966,103 @@ def test_a2a_agent_discovery(test_app_with_a2a): logger.info("A2A agent discovery test passed") +@pytest.mark.skipif( + sys.version_info < (3, 10), reason="A2A requires Python 3.10+" +) +def test_a2a_runner_factory_creates_isolated_runner(temp_agents_dir_with_a2a): + """Verify the A2A runner factory creates a copy of the runner with in-memory services.""" + # 1. Setup Mocks for the original runner and its services + original_runner = Runner( + agent=MagicMock(), + app_name="test_app", + session_service=MagicMock(), + ) + original_runner.memory_service = MagicMock() + original_runner.artifact_service = MagicMock() + original_runner.credential_service = MagicMock() + + # Mock the AdkWebServer to control the runner it returns + mock_web_server_instance = MagicMock() + mock_web_server_instance.get_runner_async = AsyncMock( + return_value=original_runner + ) + # The factory captures the app_name, so we need to mock list_agents + mock_web_server_instance.list_agents.return_value = ["test_a2a_agent"] + + # 2. Patch dependencies in the fast_api module + with ( + patch("google.adk.cli.fast_api.AdkWebServer") as mock_web_server, + patch("a2a.server.apps.A2AStarletteApplication") as mock_a2a_app, + patch("a2a.server.tasks.InMemoryTaskStore") as mock_task_store, + patch( + "google.adk.a2a.executor.a2a_agent_executor.A2aAgentExecutor" + ) as mock_executor, + patch( + "a2a.server.request_handlers.DefaultRequestHandler" + ) as mock_handler, + patch("a2a.types.AgentCard") as mock_agent_card, + patch("a2a.utils.constants.AGENT_CARD_WELL_KNOWN_PATH", "/agent.json"), + ): + mock_web_server.return_value = mock_web_server_instance + mock_task_store.return_value = MagicMock() + mock_executor.return_value = MagicMock() + mock_handler.return_value = MagicMock() + mock_agent_card.return_value = MagicMock() + + # Change to temp directory + original_cwd = os.getcwd() + os.chdir(temp_agents_dir_with_a2a) + try: + # 3. Call get_fast_api_app to trigger the factory creation + get_fast_api_app( + agents_dir=".", + web=False, + session_service_uri="", + artifact_service_uri="", + memory_service_uri="", + allow_origins=[], + a2a=True, # Enable A2A to create the factory + host="127.0.0.1", + port=8000, + ) + finally: + os.chdir(original_cwd) + + # 4. Capture the factory from the mocked A2aAgentExecutor + assert mock_executor.call_args is not None, "A2aAgentExecutor not called" + kwargs = mock_executor.call_args.kwargs + assert "runner" in kwargs + runner_factory = kwargs["runner"] + + # 5. Execute the factory to get the new runner + # Since runner_factory is an async function, we need to run it. + # We run it in a separate thread to avoid event loop conflicts if an event loop is already running. + from concurrent.futures import ThreadPoolExecutor + with ThreadPoolExecutor(max_workers=1) as executor: + a2a_runner = executor.submit(asyncio.run, runner_factory()).result() + + # 6. Assert that the new runner is a separate, modified copy + assert a2a_runner is not original_runner, "Runner should be a copy" + + # Assert that services have been replaced with InMemory versions + assert isinstance(a2a_runner.memory_service, InMemoryMemoryService) + assert isinstance(a2a_runner.session_service, InMemorySessionService) + assert isinstance(a2a_runner.artifact_service, InMemoryArtifactService) + assert isinstance(a2a_runner.credential_service, InMemoryCredentialService) + + # Assert that the original runner's services are unchanged + assert not isinstance(original_runner.memory_service, InMemoryMemoryService) + assert not isinstance( + original_runner.session_service, InMemorySessionService + ) + assert not isinstance( + original_runner.artifact_service, InMemoryArtifactService + ) + assert not isinstance( + original_runner.credential_service, InMemoryCredentialService + ) + + @pytest.mark.skipif( sys.version_info < (3, 10), reason="A2A requires Python 3.10+" ) From 3f913a1e7ee8efa73ff8dc64b4ea29815a3a6e19 Mon Sep 17 00:00:00 2001 From: guillaume blaquiere Date: Wed, 10 Dec 2025 23:07:59 +0100 Subject: [PATCH 5/7] Fix: only set the in-memory service service when VertexAI session service is used. Left unchanged for other session service --- contributing/samples/gepa/experiment.py | 1 - contributing/samples/gepa/run_experiment.py | 1 - src/google/adk/cli/fast_api.py | 41 +++++++++++++-------- tests/unittests/cli/test_fast_api.py | 6 ++- 4 files changed, 30 insertions(+), 19 deletions(-) diff --git a/contributing/samples/gepa/experiment.py b/contributing/samples/gepa/experiment.py index 2f5d03a772..f68b349d9c 100644 --- a/contributing/samples/gepa/experiment.py +++ b/contributing/samples/gepa/experiment.py @@ -43,7 +43,6 @@ from tau_bench.types import EnvRunResult from tau_bench.types import RunConfig import tau_bench_agent as tau_bench_agent_lib - import utils diff --git a/contributing/samples/gepa/run_experiment.py b/contributing/samples/gepa/run_experiment.py index cfd850b3a3..1bc4ee58c8 100644 --- a/contributing/samples/gepa/run_experiment.py +++ b/contributing/samples/gepa/run_experiment.py @@ -25,7 +25,6 @@ from absl import flags import experiment from google.genai import types - import utils _OUTPUT_DIR = flags.DEFINE_string( diff --git a/src/google/adk/cli/fast_api.py b/src/google/adk/cli/fast_api.py index 1c993dc83a..a7915d23f5 100644 --- a/src/google/adk/cli/fast_api.py +++ b/src/google/adk/cli/fast_api.py @@ -36,10 +36,14 @@ from starlette.types import Lifespan from watchdog.observers import Observer +from ..artifacts.in_memory_artifact_service import InMemoryArtifactService from ..auth.credential_service.in_memory_credential_service import InMemoryCredentialService from ..evaluation.local_eval_set_results_manager import LocalEvalSetResultsManager from ..evaluation.local_eval_sets_manager import LocalEvalSetsManager +from ..memory.in_memory_memory_service import InMemoryMemoryService from ..runners import Runner +from ..sessions.in_memory_session_service import InMemorySessionService +from ..sessions.vertex_ai_session_service import VertexAiSessionService from .adk_web_server import AdkWebServer from .service_registry import load_services_module from .utils import envs @@ -343,21 +347,28 @@ async def _get_a2a_runner_async() -> Runner: original_runner = await adk_web_server.get_runner_async( captured_app_name ) - kwargs = {} - if original_runner.app: - kwargs["app"] = original_runner.app - else: - kwargs["app_name"] = original_runner.app_name - kwargs["agent"] = original_runner.agent - - runner = Runner( - session_service=InMemorySessionService(), - artifact_service=InMemoryArtifactService(), - memory_service=InMemoryMemoryService(), - credential_service=InMemoryCredentialService(), - **kwargs, - ) - return runner + # Check if the session service is Agent Engine session Service + if isinstance( + original_runner.session_service, VertexAiSessionService + ): + # VertexAiSessionService is not compliant with A2A (impossible to create session on the fly with contextID) + # So, change it to InMemorySessionService. Put the other service in memory because persistence do not make sense + kwargs = {} + if original_runner.app: + kwargs["app"] = original_runner.app + else: + kwargs["app_name"] = original_runner.app_name + kwargs["agent"] = original_runner.agent + + runner = Runner( + session_service=InMemorySessionService(), + artifact_service=InMemoryArtifactService(), + memory_service=InMemoryMemoryService(), + credential_service=InMemoryCredentialService(), + **kwargs, + ) + return runner + return original_runner return _get_a2a_runner_async diff --git a/tests/unittests/cli/test_fast_api.py b/tests/unittests/cli/test_fast_api.py index 41d7f414c5..85405428eb 100755 --- a/tests/unittests/cli/test_fast_api.py +++ b/tests/unittests/cli/test_fast_api.py @@ -30,9 +30,9 @@ from google.adk.agents.base_agent import BaseAgent from google.adk.agents.run_config import RunConfig from google.adk.apps.app import App +from google.adk.artifacts.base_artifact_service import ArtifactVersion from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService from google.adk.auth.credential_service.in_memory_credential_service import InMemoryCredentialService -from google.adk.artifacts.base_artifact_service import ArtifactVersion from google.adk.cli.fast_api import get_fast_api_app from google.adk.errors.input_validation_error import InputValidationError from google.adk.evaluation.eval_case import EvalCase @@ -47,6 +47,7 @@ from google.adk.sessions.in_memory_session_service import InMemorySessionService from google.adk.sessions.session import Session from google.adk.sessions.state import State +from google.adk.sessions.vertex_ai_session_service import VertexAiSessionService from google.genai import types from pydantic import BaseModel import pytest @@ -1165,7 +1166,7 @@ def test_a2a_runner_factory_creates_isolated_runner(temp_agents_dir_with_a2a): original_runner = Runner( agent=MagicMock(), app_name="test_app", - session_service=MagicMock(), + session_service=VertexAiSessionService(), ) original_runner.memory_service = MagicMock() original_runner.artifact_service = MagicMock() @@ -1228,6 +1229,7 @@ def test_a2a_runner_factory_creates_isolated_runner(temp_agents_dir_with_a2a): # Since runner_factory is an async function, we need to run it. # We run it in a separate thread to avoid event loop conflicts if an event loop is already running. from concurrent.futures import ThreadPoolExecutor + with ThreadPoolExecutor(max_workers=1) as executor: a2a_runner = executor.submit(asyncio.run, runner_factory()).result() From 4bb2d460d16e9aeee9a27770abf7b0e27dc43108 Mon Sep 17 00:00:00 2001 From: guillaume blaquiere Date: Sat, 24 Jan 2026 23:00:43 +0100 Subject: [PATCH 6/7] chore: fix unit test --- pyproject.toml | 1 + .../unittests/agents/test_remote_a2a_agent.py | 19 +++++++++++++------ 2 files changed, 14 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index b17c29ff21..a40b17cc68 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -206,6 +206,7 @@ known_third_party = ["google.adk"] [tool.pytest.ini_options] testpaths = ["tests"] +pythonpath = "src" asyncio_default_fixture_loop_scope = "function" asyncio_mode = "auto" diff --git a/tests/unittests/agents/test_remote_a2a_agent.py b/tests/unittests/agents/test_remote_a2a_agent.py index d395a5516f..5b9795b58f 100644 --- a/tests/unittests/agents/test_remote_a2a_agent.py +++ b/tests/unittests/agents/test_remote_a2a_agent.py @@ -17,6 +17,7 @@ import tempfile from unittest.mock import AsyncMock from unittest.mock import create_autospec +from unittest.mock import MagicMock from unittest.mock import Mock from unittest.mock import patch @@ -683,7 +684,10 @@ def test_construct_message_parts_from_session_stops_on_agent_reply(self): agent1 = Mock() agent1.content = content2 agent1.author = self.agent.name - agent1.custom_metadata = None + agent1.custom_metadata = { + A2A_METADATA_PREFIX + "response": True, + } + part3 = Mock() part3.text = "User 2" @@ -785,7 +789,10 @@ def test_construct_message_parts_from_session_stateful_partial_history(self): agent1 = Mock() agent1.content = content2 agent1.author = self.agent.name - agent1.custom_metadata = {A2A_METADATA_PREFIX + "context_id": "ctx-1"} + agent1.custom_metadata = { + A2A_METADATA_PREFIX + "response": True, + A2A_METADATA_PREFIX + "context_id": "ctx-1", + } part3 = Mock() part3.text = "User 2" @@ -990,7 +997,7 @@ async def test_handle_a2a_response_with_task_submitted_and_no_update(self): mock_a2a_task, self.agent.name, self.mock_context, - self.mock_a2a_part_converter, + self.agent._a2a_part_converter, ) # Check the parts are updated as Thought assert result.content.parts[0].thought is True @@ -1758,7 +1765,7 @@ async def test_run_async_impl_successful_request(self): ) # Tuple with parts and context_id # Mock A2A client - mock_a2a_client = create_autospec(spec=A2AClient, instance=True) + mock_a2a_client = MagicMock(spec=A2AClient) mock_response = Mock() mock_send_message = AsyncMock() mock_send_message.__aiter__.return_value = [mock_response] @@ -1897,7 +1904,7 @@ async def test_run_async_impl_with_meta_provider(self): ) # Tuple with parts and context_id # Mock A2A client - mock_a2a_client = create_autospec(spec=A2AClient, instance=True) + mock_a2a_client = MagicMock(spec=A2AClient) mock_response = Mock() mock_send_message = AsyncMock() mock_send_message.__aiter__.return_value = [mock_response] @@ -2034,7 +2041,7 @@ async def test_run_async_impl_successful_request(self): ) # Tuple with parts and context_id # Mock A2A client - mock_a2a_client = create_autospec(spec=A2AClient, instance=True) + mock_a2a_client = MagicMock(spec=A2AClient) mock_response = Mock() mock_send_message = AsyncMock() mock_send_message.__aiter__.return_value = [mock_response] From 9193be4d845969fbb022395fe350e953f94c0bae Mon Sep 17 00:00:00 2001 From: guillaume blaquiere Date: Mon, 26 Jan 2026 21:42:41 +0100 Subject: [PATCH 7/7] Update src/google/adk/cli/fast_api.py Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> --- src/google/adk/cli/fast_api.py | 19 +++++-------------- 1 file changed, 5 insertions(+), 14 deletions(-) diff --git a/src/google/adk/cli/fast_api.py b/src/google/adk/cli/fast_api.py index 887b3255aa..78da37f44d 100644 --- a/src/google/adk/cli/fast_api.py +++ b/src/google/adk/cli/fast_api.py @@ -503,20 +503,11 @@ async def _get_a2a_runner_async() -> Runner: ): # VertexAiSessionService is not compliant with A2A (impossible to create session on the fly with contextID) # So, change it to InMemorySessionService. Put the other service in memory because persistence do not make sense - kwargs = {} - if original_runner.app: - kwargs["app"] = original_runner.app - else: - kwargs["app_name"] = original_runner.app_name - kwargs["agent"] = original_runner.agent - - runner = Runner( - session_service=InMemorySessionService(), - artifact_service=InMemoryArtifactService(), - memory_service=InMemoryMemoryService(), - credential_service=InMemoryCredentialService(), - **kwargs, - ) + runner = copy.copy(original_runner) + runner.session_service = InMemorySessionService() + runner.artifact_service = InMemoryArtifactService() + runner.memory_service = InMemoryMemoryService() + runner.credential_service = InMemoryCredentialService() return runner return original_runner