Skip to content

Commit e8025fa

Browse files
amitjborateclaude
andcommitted
feat(cli): add --app_name to decouple session/artifact storage from folder name
When agents are served via `adk web` / `adk api_server` by passing a directory, the app_name used to key sessions and artifacts is derived from the agent's folder name. Two servers that load an identically-named agent folder against a shared session/artifact backend therefore write to the same app_name and unintentionally share each other's sessions. There was no supported way to override the app_name on the directory-launch path. This adds an optional app_name override: - `get_fast_api_app(app_name=...)` - `--app_name` CLI flag on `adk web` and `adk api_server` - `ADK_APP_NAME` environment variable (fallback) When set, the session and artifact services are wrapped so their `app_name` is pinned to the override, while the agent loader, REST routes and web UI keep using the folder name. Servers loading the same folder can thus be given distinct app_names to isolate their persisted data (or the same app_name to share it, on purpose) without renaming the folder. The change is opt-in: when no override is supplied the services are returned unchanged, so existing deployments on any backend are unaffected. The wrapper operates at the BaseSessionService/BaseArtifactService abstraction (it only rewrites the app_name string), so it is storage-agnostic. Tests cover the wrapper logic, the get_fast_api_app wiring, and a backend matrix (in-memory, DatabaseSessionService via sqlite+aiosqlite -- the same class Postgres/MySQL use -- and SqliteSessionService). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 40cf97b commit e8025fa

4 files changed

Lines changed: 429 additions & 0 deletions

File tree

src/google/adk/cli/cli_tools_click.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1714,6 +1714,20 @@ def decorator(func):
17141714
),
17151715
default=None,
17161716
)
1717+
@click.option(
1718+
"--app_name",
1719+
type=str,
1720+
help=(
1721+
"Optional. Overrides the application name used to store and look up"
1722+
" sessions and artifacts. By default the app_name is the agent's"
1723+
" folder name, so multiple servers loading an identically-named"
1724+
" agent folder against a shared backend share the same sessions."
1725+
" Set a distinct value per server to keep their persisted data"
1726+
" isolated without renaming the folder. Also settable via the"
1727+
" ADK_APP_NAME environment variable."
1728+
),
1729+
default=None,
1730+
)
17171731
@functools.wraps(func)
17181732
@click.pass_context
17191733
def wrapper(ctx, *args, **kwargs):
@@ -1788,6 +1802,7 @@ def cli_web(
17881802
logo_text: str | None = None,
17891803
logo_image_url: str | None = None,
17901804
trigger_sources: list[str] | None = None,
1805+
app_name: str | None = None,
17911806
):
17921807
"""Starts a FastAPI server with Web UI for agents.
17931808
@@ -1828,6 +1843,7 @@ async def _lifespan(app: FastAPI):
18281843

18291844
app = get_fast_api_app(
18301845
agents_dir=agents_dir,
1846+
app_name=app_name,
18311847
session_service_uri=session_service_uri,
18321848
artifact_service_uri=artifact_service_uri,
18331849
memory_service_uri=memory_service_uri,
@@ -1929,6 +1945,7 @@ def cli_api_server(
19291945
with_ui: bool = False,
19301946
gemini_enterprise_app_name: str | None = None,
19311947
express_mode: bool = False,
1948+
app_name: str | None = None,
19321949
):
19331950
"""Starts a FastAPI server for agents.
19341951
@@ -1954,6 +1971,7 @@ def cli_api_server(
19541971
config = uvicorn.Config(
19551972
get_fast_api_app(
19561973
agents_dir=agents_dir,
1974+
app_name=app_name,
19571975
session_service_uri=session_service_uri,
19581976
artifact_service_uri=artifact_service_uri,
19591977
memory_service_uri=memory_service_uri,

src/google/adk/cli/fast_api.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -404,6 +404,7 @@ def get_fast_api_app(
404404
*,
405405
agents_dir: str,
406406
agent_loader: BaseAgentLoader | None = None,
407+
app_name: str | None = None,
407408
session_service_uri: str | None = None,
408409
session_db_kwargs: Mapping[str, Any] | None = None,
409410
artifact_service_uri: str | None = None,
@@ -443,6 +444,13 @@ def get_fast_api_app(
443444
services.py/yaml), and as a base for local storage.
444445
agent_loader: An optional custom loader for retrieving agent instances. If
445446
not provided, a default AgentLoader targeting agents_dir is used.
447+
app_name: Optional override for the application name used to store and look
448+
up sessions and artifacts. By default the app_name is the agent's folder
449+
name, which means two servers loading an identically-named agent folder
450+
against a shared backend share the same session/artifact namespace. Set a
451+
distinct value per server to keep their persisted data isolated without
452+
renaming the folder. Falls back to the ``ADK_APP_NAME`` environment
453+
variable when not provided.
446454
session_service_uri: A URI defining the backend for session persistence.
447455
Supports schemes like 'memory://', 'sqlite://', 'postgresql://',
448456
'mysql://', or 'agentengine://'. Defaults to per-agent local SQLite
@@ -562,6 +570,20 @@ def get_fast_api_app(
562570
except ValueError as exc:
563571
raise click.ClickException(str(exc)) from exc
564572

573+
# Pin the app_name used for persisted storage when an override is supplied
574+
# (via the app_name argument or the ADK_APP_NAME environment variable). This
575+
# keeps sessions/artifacts isolated across servers that load an
576+
# identically-named agent folder against a shared backend.
577+
app_name = app_name or os.environ.get("ADK_APP_NAME")
578+
if app_name:
579+
from .utils.app_name_override import maybe_override_app_name
580+
581+
session_service, artifact_service = maybe_override_app_name(
582+
app_name,
583+
session_service=session_service,
584+
artifact_service=artifact_service,
585+
)
586+
565587
# Build the Credential service
566588
credential_service = InMemoryCredentialService()
567589

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
"""Service wrappers that pin the ``app_name`` used for persisted storage.
16+
17+
When agents are served via ``adk web`` / ``adk api_server`` by pointing at a
18+
directory, the ``app_name`` used to key sessions and artifacts is derived from
19+
the agent's *folder name*. Two servers that load an identically-named agent
20+
folder against a shared backend therefore write to the same ``app_name`` and
21+
share each other's sessions.
22+
23+
These thin decorators let a server pin a distinct ``app_name`` for its
24+
persisted data without renaming the folder (so the loader, the REST routes and
25+
the web UI keep using the folder name, while storage is namespaced by the
26+
override). They are wired up from :func:`get_fast_api_app` when an override is
27+
supplied via the ``app_name`` argument, the ``--app_name`` CLI flag, or the
28+
``ADK_APP_NAME`` environment variable.
29+
30+
The wrappers only rewrite the ``app_name`` keyword argument; every other
31+
argument and any implementation-specific attribute is delegated unchanged.
32+
"""
33+
34+
from __future__ import annotations
35+
36+
from typing import Any
37+
38+
from ...artifacts.base_artifact_service import BaseArtifactService
39+
from ...events.event import Event
40+
from ...sessions.base_session_service import BaseSessionService
41+
from ...sessions.session import Session
42+
43+
44+
class _AppNameOverrideBase:
45+
"""Holds the wrapped service and the override, delegating unknown attrs."""
46+
47+
def __init__(self, delegate: Any, app_name: str) -> None:
48+
self._delegate = delegate
49+
self._app_name = app_name
50+
51+
def __getattr__(self, name: str) -> Any:
52+
# __getattr__ only runs for attributes not found normally. Guard the two
53+
# private attributes so a lookup before __init__ completes (e.g. during
54+
# copy/pickle) raises AttributeError instead of recursing forever.
55+
if name in ("_delegate", "_app_name"):
56+
raise AttributeError(name)
57+
return getattr(self._delegate, name)
58+
59+
60+
class AppNameOverrideSessionService(_AppNameOverrideBase, BaseSessionService):
61+
"""A :class:`BaseSessionService` that forces a fixed ``app_name``."""
62+
63+
async def create_session(self, **kwargs: Any) -> Session:
64+
kwargs["app_name"] = self._app_name
65+
return await self._delegate.create_session(**kwargs)
66+
67+
async def get_session(self, **kwargs: Any):
68+
kwargs["app_name"] = self._app_name
69+
return await self._delegate.get_session(**kwargs)
70+
71+
async def list_sessions(self, **kwargs: Any):
72+
kwargs["app_name"] = self._app_name
73+
return await self._delegate.list_sessions(**kwargs)
74+
75+
async def delete_session(self, **kwargs: Any) -> None:
76+
kwargs["app_name"] = self._app_name
77+
return await self._delegate.delete_session(**kwargs)
78+
79+
async def get_user_state(self, **kwargs: Any) -> dict[str, Any]:
80+
kwargs["app_name"] = self._app_name
81+
return await self._delegate.get_user_state(**kwargs)
82+
83+
async def append_event(self, session: Session, event: Event) -> Event:
84+
# ``session.app_name`` already carries the override, because the session
85+
# was created or fetched through this wrapper, so no rewrite is needed.
86+
return await self._delegate.append_event(session=session, event=event)
87+
88+
async def flush(self) -> None:
89+
return await self._delegate.flush()
90+
91+
92+
class AppNameOverrideArtifactService(_AppNameOverrideBase, BaseArtifactService):
93+
"""A :class:`BaseArtifactService` that forces a fixed ``app_name``."""
94+
95+
async def save_artifact(self, **kwargs: Any) -> int:
96+
kwargs["app_name"] = self._app_name
97+
return await self._delegate.save_artifact(**kwargs)
98+
99+
async def load_artifact(self, **kwargs: Any):
100+
kwargs["app_name"] = self._app_name
101+
return await self._delegate.load_artifact(**kwargs)
102+
103+
async def list_artifact_keys(self, **kwargs: Any) -> list[str]:
104+
kwargs["app_name"] = self._app_name
105+
return await self._delegate.list_artifact_keys(**kwargs)
106+
107+
async def delete_artifact(self, **kwargs: Any) -> None:
108+
kwargs["app_name"] = self._app_name
109+
return await self._delegate.delete_artifact(**kwargs)
110+
111+
async def list_versions(self, **kwargs: Any) -> list[int]:
112+
kwargs["app_name"] = self._app_name
113+
return await self._delegate.list_versions(**kwargs)
114+
115+
async def list_artifact_versions(self, **kwargs: Any):
116+
kwargs["app_name"] = self._app_name
117+
return await self._delegate.list_artifact_versions(**kwargs)
118+
119+
async def get_artifact_version(self, **kwargs: Any):
120+
kwargs["app_name"] = self._app_name
121+
return await self._delegate.get_artifact_version(**kwargs)
122+
123+
124+
def maybe_override_app_name(
125+
app_name: str | None,
126+
*,
127+
session_service: BaseSessionService,
128+
artifact_service: BaseArtifactService,
129+
) -> tuple[BaseSessionService, BaseArtifactService]:
130+
"""Wraps the services to pin ``app_name`` when an override is provided.
131+
132+
Args:
133+
app_name: The override application name. When falsy the services are
134+
returned unchanged.
135+
session_service: The session service to wrap.
136+
artifact_service: The artifact service to wrap.
137+
138+
Returns:
139+
A ``(session_service, artifact_service)`` tuple, wrapped when an override
140+
is supplied and unchanged otherwise.
141+
"""
142+
if not app_name:
143+
return session_service, artifact_service
144+
return (
145+
AppNameOverrideSessionService(session_service, app_name),
146+
AppNameOverrideArtifactService(artifact_service, app_name),
147+
)

0 commit comments

Comments
 (0)