Skip to content

Commit bd062ec

Browse files
google-genai-botcopybara-github
authored andcommitted
perf: lazy-load service registries and split apps.app to cut cold start ~8%
PiperOrigin-RevId: 916278514
1 parent 6e6621c commit bd062ec

10 files changed

Lines changed: 229 additions & 111 deletions

File tree

src/google/adk/agents/invocation_context.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@
2525
from pydantic import Field
2626
from pydantic import PrivateAttr
2727

28-
from ..apps.app import EventsCompactionConfig
29-
from ..apps.app import ResumabilityConfig
28+
from ..apps._configs import EventsCompactionConfig
29+
from ..apps._configs import ResumabilityConfig
3030
from ..artifacts.base_artifact_service import BaseArtifactService
3131
from ..auth.auth_credential import AuthCredential
3232
from ..auth.credential_service.base_credential_service import BaseCredentialService

src/google/adk/apps/__init__.py

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,28 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from .app import App
16-
from .app import ResumabilityConfig
15+
from __future__ import annotations
16+
17+
import importlib
18+
from typing import TYPE_CHECKING
19+
20+
if TYPE_CHECKING:
21+
from ._configs import ResumabilityConfig
22+
from .app import App
1723

1824
__all__ = [
1925
'App',
2026
'ResumabilityConfig',
2127
]
28+
29+
_LAZY_MEMBERS: dict[str, str] = {
30+
'App': 'app',
31+
'ResumabilityConfig': '_configs',
32+
}
33+
34+
35+
def __getattr__(name: str):
36+
if name in _LAZY_MEMBERS:
37+
module = importlib.import_module(f'{__name__}.{_LAZY_MEMBERS[name]}')
38+
return vars(module)[name]
39+
raise AttributeError(f'module {__name__!r} has no attribute {name!r}')

src/google/adk/apps/_configs.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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+
from __future__ import annotations
16+
17+
from typing import Optional
18+
19+
from pydantic import BaseModel
20+
from pydantic import ConfigDict
21+
from pydantic import Field
22+
from pydantic import model_validator
23+
24+
from ..utils.feature_decorator import experimental
25+
from .base_events_summarizer import BaseEventsSummarizer
26+
27+
28+
@experimental
29+
class ResumabilityConfig(BaseModel):
30+
"""The config of the resumability for an application.
31+
32+
The "resumability" in ADK refers to the ability to:
33+
1. pause an invocation upon a long-running function call.
34+
2. resume an invocation from the last event, if it's paused or failed midway
35+
through.
36+
37+
Note: ADK resumes the invocation in a best-effort manner:
38+
1. Tool call to resume needs to be idempotent because we only guarantee
39+
an at-least-once behavior once resumed.
40+
2. Any temporary / in-memory state will be lost upon resumption.
41+
"""
42+
43+
is_resumable: bool = False
44+
"""Whether the app supports agent resumption.
45+
If enabled, the feature will be enabled for all agents in the app.
46+
"""
47+
48+
49+
@experimental
50+
class EventsCompactionConfig(BaseModel):
51+
"""The config of event compaction for an application."""
52+
53+
model_config = ConfigDict(
54+
arbitrary_types_allowed=True,
55+
extra="forbid",
56+
)
57+
58+
summarizer: Optional[BaseEventsSummarizer] = None
59+
"""The event summarizer to use for compaction."""
60+
61+
compaction_interval: int
62+
"""The number of *new* user-initiated invocations that, once
63+
fully represented in the session's events, will trigger a compaction."""
64+
65+
overlap_size: int
66+
"""The number of preceding invocations to include from the
67+
end of the last compacted range. This creates an overlap between consecutive
68+
compacted summaries, maintaining context."""
69+
70+
token_threshold: Optional[int] = Field(
71+
default=None,
72+
gt=0,
73+
)
74+
"""Post-invocation token threshold trigger.
75+
76+
If set, ADK will attempt a post-invocation compaction when the most recently
77+
observed prompt token count meets or exceeds this threshold.
78+
"""
79+
80+
event_retention_size: Optional[int] = Field(default=None, ge=0)
81+
"""Post-invocation raw event retention size.
82+
83+
If token-based post-invocation compaction is triggered, this keeps the last N
84+
raw events un-compacted.
85+
"""
86+
87+
@model_validator(mode="after")
88+
def _validate_token_params(self) -> EventsCompactionConfig:
89+
token_threshold_set = self.token_threshold is not None
90+
retention_size_set = self.event_retention_size is not None
91+
if token_threshold_set != retention_size_set:
92+
raise ValueError(
93+
"token_threshold and event_retention_size must be set together."
94+
)
95+
return self

src/google/adk/apps/app.py

Lines changed: 9 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,16 @@
2222

2323
from ..agents.base_agent import BaseAgent
2424
from ..agents.context_cache_config import ContextCacheConfig
25-
from ..apps.base_events_summarizer import BaseEventsSummarizer
2625
from ..plugins.base_plugin import BasePlugin
27-
from ..utils.feature_decorator import experimental
26+
from ._configs import EventsCompactionConfig
27+
from ._configs import ResumabilityConfig
28+
29+
__all__ = [
30+
"App",
31+
"EventsCompactionConfig",
32+
"ResumabilityConfig",
33+
"validate_app_name",
34+
]
2835

2936

3037
def validate_app_name(name: str) -> None:
@@ -38,76 +45,6 @@ def validate_app_name(name: str) -> None:
3845
raise ValueError("App name cannot be 'user'; reserved for end-user input.")
3946

4047

41-
@experimental
42-
class ResumabilityConfig(BaseModel):
43-
"""The config of the resumability for an application.
44-
45-
The "resumability" in ADK refers to the ability to:
46-
1. pause an invocation upon a long-running function call.
47-
2. resume an invocation from the last event, if it's paused or failed midway
48-
through.
49-
50-
Note: ADK resumes the invocation in a best-effort manner:
51-
1. Tool call to resume needs to be idempotent because we only guarantee
52-
an at-least-once behavior once resumed.
53-
2. Any temporary / in-memory state will be lost upon resumption.
54-
"""
55-
56-
is_resumable: bool = False
57-
"""Whether the app supports agent resumption.
58-
If enabled, the feature will be enabled for all agents in the app.
59-
"""
60-
61-
62-
@experimental
63-
class EventsCompactionConfig(BaseModel):
64-
"""The config of event compaction for an application."""
65-
66-
model_config = ConfigDict(
67-
arbitrary_types_allowed=True,
68-
extra="forbid",
69-
)
70-
71-
summarizer: Optional[BaseEventsSummarizer] = None
72-
"""The event summarizer to use for compaction."""
73-
74-
compaction_interval: int
75-
"""The number of *new* user-initiated invocations that, once
76-
fully represented in the session's events, will trigger a compaction."""
77-
78-
overlap_size: int
79-
"""The number of preceding invocations to include from the
80-
end of the last compacted range. This creates an overlap between consecutive
81-
compacted summaries, maintaining context."""
82-
83-
token_threshold: Optional[int] = Field(
84-
default=None,
85-
gt=0,
86-
)
87-
"""Post-invocation token threshold trigger.
88-
89-
If set, ADK will attempt a post-invocation compaction when the most recently
90-
observed prompt token count meets or exceeds this threshold.
91-
"""
92-
93-
event_retention_size: Optional[int] = Field(default=None, ge=0)
94-
"""Post-invocation raw event retention size.
95-
96-
If token-based post-invocation compaction is triggered, this keeps the last N
97-
raw events un-compacted.
98-
"""
99-
100-
@model_validator(mode="after")
101-
def _validate_token_params(self) -> EventsCompactionConfig:
102-
token_threshold_set = self.token_threshold is not None
103-
retention_size_set = self.event_retention_size is not None
104-
if token_threshold_set != retention_size_set:
105-
raise ValueError(
106-
"token_threshold and event_retention_size must be set together."
107-
)
108-
return self
109-
110-
11148
class App(BaseModel):
11249
"""Represents an LLM-backed agentic application.
11350

src/google/adk/artifacts/__init__.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,34 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
from __future__ import annotations
16+
17+
import importlib
18+
from typing import TYPE_CHECKING
19+
1520
from .base_artifact_service import BaseArtifactService
16-
from .file_artifact_service import FileArtifactService
17-
from .gcs_artifact_service import GcsArtifactService
18-
from .in_memory_artifact_service import InMemoryArtifactService
21+
22+
if TYPE_CHECKING:
23+
from .file_artifact_service import FileArtifactService
24+
from .gcs_artifact_service import GcsArtifactService
25+
from .in_memory_artifact_service import InMemoryArtifactService
1926

2027
__all__ = [
2128
'BaseArtifactService',
2229
'FileArtifactService',
2330
'GcsArtifactService',
2431
'InMemoryArtifactService',
2532
]
33+
34+
_LAZY_MEMBERS: dict[str, str] = {
35+
'FileArtifactService': 'file_artifact_service',
36+
'GcsArtifactService': 'gcs_artifact_service',
37+
'InMemoryArtifactService': 'in_memory_artifact_service',
38+
}
39+
40+
41+
def __getattr__(name: str):
42+
if name in _LAZY_MEMBERS:
43+
module = importlib.import_module(f'{__name__}.{_LAZY_MEMBERS[name]}')
44+
return vars(module)[name]
45+
raise AttributeError(f'module {__name__!r} has no attribute {name!r}')

src/google/adk/flows/llm_flows/single_flow.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@
2222
from . import _nl_planning
2323
from . import _output_schema_processor
2424
from . import basic
25-
from . import compaction
2625
from . import contents
2726
from . import context_cache_processor
2827
from . import identity
@@ -36,6 +35,7 @@
3635

3736
def _create_request_processors():
3837
"""Create the standard request processor list for a single-agent flow."""
38+
from . import compaction
3939
from ...auth import auth_preprocessor
4040

4141
return [

src/google/adk/memory/__init__.py

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,27 +11,35 @@
1111
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
14-
import logging
14+
15+
from __future__ import annotations
16+
17+
import importlib
18+
from typing import TYPE_CHECKING
1519

1620
from .base_memory_service import BaseMemoryService
17-
from .in_memory_memory_service import InMemoryMemoryService
18-
from .vertex_ai_memory_bank_service import VertexAiMemoryBankService
1921

20-
logger = logging.getLogger('google_adk.' + __name__)
22+
if TYPE_CHECKING:
23+
from .in_memory_memory_service import InMemoryMemoryService
24+
from .vertex_ai_memory_bank_service import VertexAiMemoryBankService
25+
from .vertex_ai_rag_memory_service import VertexAiRagMemoryService
2126

2227
__all__ = [
2328
'BaseMemoryService',
2429
'InMemoryMemoryService',
2530
'VertexAiMemoryBankService',
31+
'VertexAiRagMemoryService',
2632
]
2733

28-
try:
29-
from .vertex_ai_rag_memory_service import VertexAiRagMemoryService
34+
_LAZY_MEMBERS: dict[str, str] = {
35+
'InMemoryMemoryService': 'in_memory_memory_service',
36+
'VertexAiMemoryBankService': 'vertex_ai_memory_bank_service',
37+
'VertexAiRagMemoryService': 'vertex_ai_rag_memory_service',
38+
}
39+
3040

31-
__all__.append('VertexAiRagMemoryService')
32-
except ImportError:
33-
logger.debug(
34-
'The Vertex SDK is not installed. If you want to use the'
35-
' VertexAiRagMemoryService please install it. If not, you can ignore this'
36-
' warning.'
37-
)
41+
def __getattr__(name: str):
42+
if name in _LAZY_MEMBERS:
43+
module = importlib.import_module(f'{__name__}.{_LAZY_MEMBERS[name]}')
44+
return vars(module)[name]
45+
raise AttributeError(f'module {__name__!r} has no attribute {name!r}')

src/google/adk/plugins/__init__.py

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
# Copyright 2026 Google LLC
22
#
33
# 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 in obtain a copy of the License at
4+
# you may in obtain a copy of the License at
65
#
76
# http://www.apache.org/licenses/LICENSE-2.0
87
#
@@ -12,11 +11,18 @@
1211
# See the License for the specific language governing permissions and
1312
# limitations under the License.
1413

14+
from __future__ import annotations
15+
16+
import importlib
17+
from typing import TYPE_CHECKING
18+
1519
from .base_plugin import BasePlugin
16-
from .debug_logging_plugin import DebugLoggingPlugin
17-
from .logging_plugin import LoggingPlugin
1820
from .plugin_manager import PluginManager
19-
from .reflect_retry_tool_plugin import ReflectAndRetryToolPlugin
21+
22+
if TYPE_CHECKING:
23+
from .debug_logging_plugin import DebugLoggingPlugin
24+
from .logging_plugin import LoggingPlugin
25+
from .reflect_retry_tool_plugin import ReflectAndRetryToolPlugin
2026

2127
__all__ = [
2228
'BasePlugin',
@@ -25,3 +31,16 @@
2531
'PluginManager',
2632
'ReflectAndRetryToolPlugin',
2733
]
34+
35+
_LAZY_MEMBERS: dict[str, str] = {
36+
'DebugLoggingPlugin': 'debug_logging_plugin',
37+
'LoggingPlugin': 'logging_plugin',
38+
'ReflectAndRetryToolPlugin': 'reflect_retry_tool_plugin',
39+
}
40+
41+
42+
def __getattr__(name: str):
43+
if name in _LAZY_MEMBERS:
44+
module = importlib.import_module(f'{__name__}.{_LAZY_MEMBERS[name]}')
45+
return vars(module)[name]
46+
raise AttributeError(f'module {__name__!r} has no attribute {name!r}')

0 commit comments

Comments
 (0)