Repository navigation
Agents under different parents may share a name: no warning, and find_agent() silently resolves to the first #7110
Description
🔴 Required Information
Describe the Bug:
validate_sub_agents_unique_names checks one sub_agents list at a time, so two agents that share a name under different parents are accepted with no warning. Every by-name resolution then silently picks the first match in a depth-first walk, and the other agent cannot be reached by name at all.
find_agent (base_agent.py:512) returns the first descendant whose name matches. Two callers make that visible:
llm_agent.py:1001—transfer_to_agentresolves the target withroot_agent.find_agent(agent_name). The second agent can never be transferred to._agent_router.py:133— a resumed invocation picks the agent to continue withroot_agent.find_agent(event.author). When two agents share a name, the one that resumes is whichever the walk reaches first, which is not necessarily the one that authored the event. I have verifiedfind_agent's behaviour by execution; this second consequence follows from reading the call, and I have not reproduced a full resume.
The sibling case is already treated as a problem — validate_sub_agents_unique_names logs "Found duplicate sub-agent names ... All sub-agents must have unique names." The check just does not extend past one level.
The node layer already solves the same ambiguity: the docstring of restore_branch_from_history says nodes are "matched by their static path (run ids stripped) so that two nodes sharing a name (e.g. the same sub-agent mounted under two parents) are disambiguated". That disambiguation is not available to find_agent.
Steps to Reproduce:
pip install google-adk==2.9.0- Save the script under Minimal Reproduction Code as
repro.py python repro.py
Expected Behavior:
Either the uniqueness check covers the whole tree rather than one list of siblings, or by-name resolution is path-aware the way node matching already is.
Observed Behavior:
A. two agents named "alpha", under DIFFERENT parents
warning at construction? NO
find_agent("alpha") -> the one under branch1
the one under branch2 is reachable by name? False <- unreachable
B. NEGATIVE CONTROL — two agents named "alpha" as SIBLINGS
warning at construction? YES
C. NEGATIVE CONTROL — unique names
find_agent("beta") -> under branch2 (correct)
B is the control that matters: the existing validator does fire, so the case is already recognised — it is the scope of the check that differs, not the intent.
Environment Details:
- ADK Library Version:
google-adk 2.9.0(same code onmain) - Desktop OS: macOS 26.6.2 (arm64)
- Python Version: 3.12.13
Model Information:
- Are you using LiteLLM: No
- Which model is being used: N/A — no model is involved
🟡 Optional Information
Additional Context — scope, stated restrictively:
- Verified by execution: construction raises no warning for the cousin case, and
find_agentreturns the first match so the second agent is unreachable by name. - Not verified by execution: the effect on a real resumed invocation, or on a live
transfer_to_agentround trip with a model. Those follow from the two call sites above, which I read rather than ran. - Whether this is worth fixing depends on how likely you consider a tree with repeated names. I am not claiming it is common — only that nothing reports it, and that the sibling check shows the intent is for names to be unique.
- I have deliberately not proposed a patch: widening the check to the whole tree would reject trees that work today, which is a compatibility decision that is yours.
Minimal Reproduction Code:
"""Two agents can share a name when they sit under different parents.
Nothing warns, and every by-name resolution silently picks the first."""
import logging, io
from google.adk.agents.base_agent import BaseAgent
log = io.StringIO()
logging.getLogger('google_adk').addHandler(logging.StreamHandler(log))
logging.getLogger('google_adk').setLevel(logging.WARNING)
def build(label, tree):
log.truncate(0); log.seek(0)
root = tree()
warned = 'duplicate' in log.getvalue().lower()
print(f'{label}\n warning at construction? {"YES" if warned else "NO"}')
return root
root_a = build('A. two agents named "alpha", under DIFFERENT parents',
lambda: BaseAgent(name='root', sub_agents=[
BaseAgent(name='branch1', sub_agents=[BaseAgent(name='alpha')]),
BaseAgent(name='branch2', sub_agents=[BaseAgent(name='alpha')]),
]))
first = root_a.find_agent('alpha')
under_b2 = root_a.find_agent('branch2').find_agent('alpha')
print(f' find_agent("alpha") -> the one under {first.parent_agent.name}')
print(f' the one under branch2 is reachable by name? '
f'{first is under_b2} <- unreachable\n')
root_b = build('B. NEGATIVE CONTROL — two agents named "alpha" as SIBLINGS',
lambda: BaseAgent(name='root', sub_agents=[
BaseAgent(name='alpha'), BaseAgent(name='alpha')]))
print()
root_c = build('C. NEGATIVE CONTROL — unique names',
lambda: BaseAgent(name='root', sub_agents=[
BaseAgent(name='branch1', sub_agents=[BaseAgent(name='alpha')]),
BaseAgent(name='branch2', sub_agents=[BaseAgent(name='beta')]),
]))
print(f' find_agent("beta") -> under {root_c.find_agent("beta").parent_agent.name} (correct)')How often has this issue occurred?:
- Always (100%) — deterministic; no model, no network, no timing involved.
Activity
surajksharma07 commentedon Sep 14, 2026
@vasilisnasopoulos The sibling check never looks past one level so two cousins sharing a name slip through with no warning and find_agent()/transfer_to_agent just grab whichever one the DFS reaches first. #7116 already widens that check across the whole tree so before we take it further could you (or @gyanu2507) run it against your own agent trees to confirm it doesn't flag anything that's working fine today? Worth noting it only adds the warning, find_agent() still resolves to the first match by design so cross-branch collisions are something to avoid not something it fully guards against yet. Once that holds up on your side happy to move it straight into review instead of a separate PR.
vasilisnasopoulos commentedon Sep 14, 2026
Ran #7116 against my trees. It catches the reported case and I could not get a false positive out of it.
alpha(the report)Case 6 was the one I expected to break it, since
visitis recursive. It does not — noRecursionErrorat 400 levels.tests/unittests/agents: 474 passed, 1 skipped, 2 xfailed on both main and the branch, byte-identical. No regressions.One gap left, which is case 3. The validator walks
value, the list being assigned, so the agent the list is attached to is never inseen_names. A descendant that shares the root's name is not flagged:and the descendant named
rootis unreachable by name, becausefind_agenttestsself.name == namebefore descending (base_agent.py:512). Same class of silent shadowing as the cousin case, from the other direction. Whether that belongs in this PR or a follow-up is your call.validate_sub_agents_unique_namesis a@field_validatorclassmethod, so it has no instance to read the owning name off — butnameis declared beforesub_agentson the model, so takinginfo: ValidationInfoand seedingseen_nameswithinfo.data.get('name')is enough, no restructuring. I tried exactly that on top of your branch: case 3 starts warning, cases 2/5/6 stay silent, andtests/unittests/agentsis still 474 passed / 1 skipped / 2 xfailed.Agreed on
find_agentstaying first-match. The warning is what was missing; resolution semantics are a documented choice, and cross-branch collisions being something to avoid is a fine answer as long as something says so out loud.surajksharma07 commentedon Sep 14, 2026
@vasilisnasopoulos Nice, that matrix is exactly what we needed. No false positives across 6 trees and 474/1/2 identical on both branches settles it for the cousin case.
Case 3 is a fair catch and it's the same bug from the other direction: the validator only ever seeds seen_names from value (the list) never from the agent that list belongs to. Your fix is the right shape too. Since name is declared before sub_agents on the model (line 141 vs 166) info.data.get('name') will already be populated when the sub_agents validator runs so no restructuring needed.
Given it's a few lines in the same function you're already touching easiest path is folding it into #7116 rather than opening a second PR. @gyanu2507 could you push that diff onto your branch so it goes through review as one change? Once that's in find_agent() staying first-match is fine as a documented resolution rule, not something this PR needs to solve.
fix: warn when cousin agents share a name