Skip to content

Agents under different parents may share a name: no warning, and find_agent() silently resolves to the first #7110

Closed

Description

@vasilisnasopoulos

🔴 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_agent resolves the target with root_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 with root_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 verified find_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:

  1. pip install google-adk==2.9.0
  2. Save the script under Minimal Reproduction Code as repro.py
  3. 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 on main)
  • 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_agent returns 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_agent round 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

  1. surajksharma07 commented on Sep 14, 2026

    @surajksharma07
    Collaborator

    @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.

  2. vasilisnasopoulos commented on Sep 14, 2026

    @vasilisnasopoulos
    Author

    Ran #7116 against my trees. It catches the reported case and I could not get a false positive out of it.

    tree main #7116 expected
    1. cousins both named alpha (the report) no warning warns warn
    2. control — all unique, four levels deep no warning no warning silent
    3. control — descendant named like the root no warning no warning see below
    4. grandchild shares its own ancestor's name no warning warns warn
    5. control — subtree built separately, then attached no warning no warning silent
    6. control — 400-deep chain, all unique no warning no warning silent

    Case 6 was the one I expected to break it, since visit is recursive. It does not — no RecursionError at 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 in seen_names. A descendant that shares the root's name is not flagged:

    root = BaseAgent(name='root', sub_agents=[
        BaseAgent(name='b1', sub_agents=[BaseAgent(name='root')])])
    # no warning
    root.find_agent('root') is root          # True

    and the descendant named root is unreachable by name, because find_agent tests self.name == name before 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_names is a @field_validator classmethod, so it has no instance to read the owning name off — but name is declared before sub_agents on the model, so taking info: ValidationInfo and seeding seen_names with info.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, and tests/unittests/agents is still 474 passed / 1 skipped / 2 xfailed.

    Agreed on find_agent staying 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.

  3. surajksharma07 commented on Sep 14, 2026

    @surajksharma07
    Collaborator

    @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.

  4. removed
    request clarification[Status] The maintainer need clarification or more information from the author
    on Sep 14, 2026
  5. added
    agent engine[Component] This issue is related to Vertex AI Agent Engine
    on Sep 15, 2026
  6. added a commit that references this issue on Oct 16, 2026
    6031bb1
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

agent engine[Component] This issue is related to Vertex AI Agent Engine

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions