Skip to content

Stray <app_name> directory created under agents_dir on server paths becomes a phantom agent in list_agents() #6667

Description

@aweussom

Summary

When an App's name differs from its on-disk agent directory name, the local storage services create <agents_dir>/<app_name>/.adk/.... Because <app_name> is not a dotted name, AgentLoader.list_agents() picks it up as an agent directory, advertises it, and then fails to load it.

cli/cli.py already prevents this by passing an app_name_to_dir mapping. The server entry points — cli/fast_api.py, cli/api_server.py, cli/adk_web_server.py — never construct that mapping, so the guard doesn't apply to adk web / adk api_server / any AdkApp-style server.

Reproduction

google-adk 2.6.3, Python 3.12.3, clean venv, Ubuntu 24.04 (WSL2).

repro/agents/
  myagent/
    agent.py
# repro/agents/myagent/agent.py — note the export is the *correct* one
from google.adk.agents import Agent
from google.adk.apps.app import App

app = App(name="judge", root_agent=Agent(name="judge", model="gemini-3-flash-preview"))

Then reproduce what the server path does — resolve local storage for this app with no app_name_to_dir mapping:

from google.adk.cli.utils.local_storage import create_local_artifact_service
from google.adk.cli.utils.dot_adk_folder import dot_adk_folder_for_agent
from google.adk.cli.utils.agent_loader import AgentLoader

root = "repro/agents"
print(AgentLoader(agents_dir=root).list_agents())     # ['myagent']

storage_key = "judge"                                  # app.name, unmapped
folder = dot_adk_folder_for_agent(agents_root=root, app_name=storage_key)
create_local_artifact_service(base_dir=folder.agent_dir)

print(AgentLoader(agents_dir=root).list_agents())     # ['judge', 'myagent']  <-- phantom
AgentLoader(agents_dir=root).load_agent("judge")

Result:

['myagent']
['judge', 'myagent']
ValueError: No root_agent found for 'judge'. Searched in 'judge.agent.root_agent',
'judge.root_agent' and 'judge/root_agent.yaml'.

A non-hidden repro/agents/judge/ now exists, list_agents() advertises it, and loading it fails. The only real agent in the tree is myagent.

Cause

LocalStorage*Service._get_service derives the on-disk location from the app name, falling back to the app name verbatim when no mapping is supplied (cli/utils/local_storage.py):

storage_key = self._app_name_to_dir.get(app_name, app_name)
folder = dot_adk_folder_for_agent(
    agents_root=self._agents_root, app_name=storage_key
)
base_dir = folder.agent_dir

dot_adk_folder_for_agent_resolve_agent_dir joins agents_root / app_name, so an unmapped app name becomes a sibling directory of the real agents. AgentLoader.list_agents() then treats it as an agent, since it filters only dotted names and __pycache__:

agent_names = [
    x for x in os.listdir(base_path)
    if os.path.isdir(os.path.join(base_path, x))
    and not x.startswith(".")
    and x != "__pycache__"
]

cli/cli.py builds the mapping that avoids all of this:

app_name_to_dir = None
if isinstance(agent_or_app, App) and agent_or_app.name != agent_folder_name:
    app_name_to_dir = {agent_or_app.name: agent_folder_name}

Confirming the same call with that mapping — no stray directory, and artifacts land in the right place:

storage_key      : myagent
dirs in root     : ['myagent']
list_agents()    : ['myagent']
artifacts landed : myagent/.adk/artifacts

grep -n app_name_to_dir cli/fast_api.py cli/api_server.py cli/adk_web_server.py returns nothing, so the mapping is never built on those paths.

Impact

Two things, in increasing order of annoyance:

  1. list_agents() advertises an agent that cannot be loaded. Anything enumerating agents — the dev UI dropdown, /list-apps — offers a broken entry, and selecting it produces "No root_agent found" for a directory the user never created.
  2. Artifacts and the SQLite session DB are written outside the agent's own directory, so per-agent local state silently lands in the wrong place and is not picked up next run.

It also has a way of becoming permanent in container deployments. The stray directory is created in the working tree during local development, and gcloud run deploy --source uploads the working tree, so it gets baked into the image; the deployed container then starts with a phantom agent directory it cannot load. A .gcloudignore entry works around it once you know the name to exclude, which you only learn by reading the loader source.

Suggested fix

Build app_name_to_dir in the server entry points the same way cli/cli.py does, so all paths agree — the logic already exists and just isn't wired up.

Belt and braces, and independent of the above: list_agents() could skip directories that contain no loadable agent, rather than assuming every non-dotted subdirectory is one. That would also stop unrelated stray directories from showing up as agents.

Environment

google-adk 2.6.3, Python 3.12.3, Ubuntu 24.04 (WSL2). Also present on 1.28.0, where it was first hit.

Related: #6606 is a different failure with a similar-looking symptom — there the export is root_agent = App(...), which fails the loader's type check before any storage service runs. This one requires the correct app = App(...) export, since the agent has to load successfully for the artifact service to be resolved at all.

Metadata

Metadata

Assignees

Labels

cli[Component] This issue is related to clirequest clarification[Status] The maintainer need clarification or more information from the author

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions