Repository navigation
RemoteA2aAgent fetches the agent card and creates a new httpx client on every turn聽#7629
Description
馃敶 Required Information
Describe the Bug:
RemoteA2aAgent caches the resolved card and its httpx client on the instance (_remote_a2a_agent.py#L760-L770, #L913-L928). But the agent tree gets cloned on every invocation (_workflow_graph_utils.py#L150, and clone() copies sub agents at base_agent.py#L356), so the cache ends up on a copy that's thrown away.
So every turn fetches /.well-known/agent-card.json again and opens a new httpx.AsyncClient, even on turns that never transfer to the remote agent. The instance I passed in never gets resolved, so cleanup() / runner.close() can't close those clients. They stay open until GC collects them.
Steps to Reproduce:
pip install "google-adk[a2a]"(I ran it on main, 8909689)- Run the script below. The coordinator answers on its own and never transfers.
import asyncio
from collections import Counter
import httpx
import uvicorn
from google.adk.a2a.utils.agent_to_a2a import to_a2a
from google.adk.agents import Agent
from google.adk.agents.remote_a2a_agent import AGENT_CARD_WELL_KNOWN_PATH, RemoteA2aAgent
from google.adk.models.base_llm import BaseLlm
from google.adk.models.llm_response import LlmResponse
from google.adk.runners import InMemoryRunner
from google.genai import types
class FakeLlm(BaseLlm):
model: str = "fake"
async def generate_content_async(self, llm_request, stream=False):
yield LlmResponse(content=types.Content(role="model", parts=[types.Part(text="ok")]))
hits = Counter()
created = [0]
_init = httpx.AsyncClient.__init__
def _count_init(self, *a, **k):
created[0] += 1
_init(self, *a, **k)
def count_requests(app):
async def wrapped(scope, receive, send):
if scope["type"] == "http":
hits[(scope["method"], scope["path"])] += 1
await app(scope, receive, send)
return wrapped
async def main():
remote_app = to_a2a(Agent(name="weather", model=FakeLlm(), instruction="answer"), port=8771)
server = uvicorn.Server(uvicorn.Config(count_requests(remote_app), port=8771, log_level="warning"))
task = asyncio.create_task(server.serve())
while not server.started:
await asyncio.sleep(0.05)
httpx.AsyncClient.__init__ = _count_init
remote = RemoteA2aAgent(name="weather", agent_card=f"http://localhost:8771{AGENT_CARD_WELL_KNOWN_PATH}")
root = Agent(name="coordinator", model=FakeLlm(), instruction="answer", sub_agents=[remote])
runner = InMemoryRunner(agent=root, app_name="repro")
session = await runner.session_service.create_session(app_name="repro", user_id="u")
for _ in range(5):
msg = types.Content(role="user", parts=[types.Part(text="hi")])
async for _ in runner.run_async(user_id="u", session_id=session.id, new_message=msg):
pass
print("turns: 5 (the coordinator answers itself, it never transfers)")
print("agent card GETs:", hits[("GET", AGENT_CARD_WELL_KNOWN_PATH)])
print("httpx.AsyncClient created:", created[0])
print("card cached on the instance I passed in:", remote._agent_card is not None)
await runner.close()
server.should_exit = True
await task
asyncio.run(main())Expected Behavior:
The card gets fetched once and one httpx client is reused across turns, and cleanup() closes it.
Observed Behavior:
turns: 5 (the coordinator answers itself, it never transfers)
agent card GETs: 5
httpx.AsyncClient created: 5
card cached on the instance I passed in: False
It gets worse when turns do transfer. In a longer run (8 turns, all routed to the remote agent) I saw 16 clients and 12 open connections to the remote before GC.
Side effect: since the instance never resolves its card, description stays '', so the client-side invoke_agent span has an empty gen_ai.agent.description.
Environment Details:
- ADK Library Version (pip show google-adk): 2.12.0 (main at 8909689)
- Desktop OS: macOS
- Python Version (python -V): 3.12.15
Model Information:
- Are you using LiteLLM: No (the repro uses a fake BaseLlm, no API calls)
- Which model is being used: N/A
馃煛 Optional Information
Regression:
Haven't checked older versions.
One way to fix it would be to keep the resolved card and client somewhere the clones share, so they reuse it and cleanup() can still close it. Happy to send a PR if that direction works for you.
Activity
sanketpatil06 commentedon Oct 23, 2026
Thanks @ishan-1010 for the detailed report and repro, and for opening PR.
We have confirmed the issue. The agent tree is cloned on every invocation, so the resolved agent card and httpx client get cached on a copy that's thrown away. They never reach the
RemoteA2aAgentinstance you passed in.#7672 is under review, and we will post updates there.