Skip to content

RemoteA2aAgent fetches the agent card and creates a new httpx client on every turn聽#7629

Description

@ishan-1010

馃敶 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:

  1. pip install "google-adk[a2a]" (I ran it on main, 8909689)
  2. 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

  1. added
    a2a[Component] This issue is related a2a support inside ADK.
    on Oct 22, 2026
  2. sanketpatil06 commented on Oct 23, 2026

    @sanketpatil06

    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 RemoteA2aAgent instance you passed in.

    #7672 is under review, and we will post updates there.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

a2a[Component] This issue is related a2a support inside ADK.

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions