Skip to content

Commit cbb6e49

Browse files
XinranTangcopybara-github
authored andcommitted
feat: Add a app level config for resumable applications
PiperOrigin-RevId: 811272046
1 parent c6b6b6f commit cbb6e49

File tree

6 files changed

+118
-13
lines changed

6 files changed

+118
-13
lines changed

src/google/adk/agents/invocation_context.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@
1515
from __future__ import annotations
1616

1717
from typing import Optional
18-
from typing import TYPE_CHECKING
1918
import uuid
2019

2120
from google.genai import types
@@ -24,14 +23,14 @@
2423
from pydantic import Field
2524
from pydantic import PrivateAttr
2625

26+
from ..apps.app import ResumabilityConfig
2727
from ..artifacts.base_artifact_service import BaseArtifactService
2828
from ..auth.credential_service.base_credential_service import BaseCredentialService
2929
from ..events.event import Event
3030
from ..memory.base_memory_service import BaseMemoryService
3131
from ..plugins.plugin_manager import PluginManager
3232
from ..sessions.base_session_service import BaseSessionService
3333
from ..sessions.session import Session
34-
from ..utils.feature_decorator import working_in_progress
3534
from .active_streaming_tool import ActiveStreamingTool
3635
from .base_agent import BaseAgent
3736
from .context_cache_config import ContextCacheConfig
@@ -189,6 +188,9 @@ class InvocationContext(BaseModel):
189188
run_config: Optional[RunConfig] = None
190189
"""Configurations for live agents under this invocation."""
191190

191+
resumability_config: Optional[ResumabilityConfig] = None
192+
"""The resumability config that applies to all agents under this invocation."""
193+
192194
plugin_manager: PluginManager = Field(default_factory=PluginManager)
193195
"""The manager for keeping track of plugins in this invocation."""
194196

@@ -220,7 +222,6 @@ def app_name(self) -> str:
220222
def user_id(self) -> str:
221223
return self.session.user_id
222224

223-
@working_in_progress("incomplete feature, don't use yet")
224225
def get_events(
225226
self,
226227
current_invocation: bool = False,

src/google/adk/apps/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@
1313
# limitations under the License.
1414

1515
from .app import App
16+
from .app import ResumabilityConfig
1617

1718
__all__ = [
1819
'App',
20+
'ResumabilityConfig',
1921
]

src/google/adk/apps/app.py

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,28 @@
2626
from ..utils.feature_decorator import experimental
2727

2828

29+
@experimental
30+
class ResumabilityConfig(BaseModel):
31+
"""The config of the resumability for an application.
32+
33+
The "resumability" in ADK refers to the ability to:
34+
1. pause an invocation upon a long running function call.
35+
2. resume an invocation from the last event, if it's paused or failed midway
36+
through.
37+
38+
Note: ADK resumes the invocation in a best-effort manner. Edge cases include:
39+
1. The invocation crashed before receiving the response of a tool call, once
40+
resumed, the same tool call will be invoked again.
41+
2. If agent transfer forms a loop (root->sub1->sub2->root[pause]), once
42+
resumed, the agent transfer loop will be ignored.
43+
"""
44+
45+
is_resumable: bool = False
46+
"""Whether the app supports agent resumption.
47+
If enabled, the feature will be enabled for all agents in the app.
48+
"""
49+
50+
2951
@experimental
3052
class App(BaseModel):
3153
"""Represents an LLM-backed agentic application.
@@ -57,3 +79,9 @@ class App(BaseModel):
5779

5880
context_cache_config: Optional[ContextCacheConfig] = None
5981
"""Context cache configuration that applies to all LLM agents in the app."""
82+
83+
resumability_config: Optional[ResumabilityConfig] = None
84+
"""
85+
The config of the resumability for the application.
86+
If configured, will be applied to all agents in the app.
87+
"""

src/google/adk/runners.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
from .agents.llm_agent import LlmAgent
3737
from .agents.run_config import RunConfig
3838
from .apps.app import App
39+
from .apps.app import ResumabilityConfig
3940
from .artifacts.base_artifact_service import BaseArtifactService
4041
from .artifacts.in_memory_artifact_service import InMemoryArtifactService
4142
from .auth.credential_service.base_credential_service import BaseCredentialService
@@ -74,6 +75,8 @@ class Runner:
7475
session_service: The session service for the runner.
7576
memory_service: The memory service for the runner.
7677
credential_service: The credential service for the runner.
78+
context_cache_config: The context cache config for the runner.
79+
resumability_config: The resumability config for the application.
7780
"""
7881

7982
app_name: str
@@ -90,6 +93,10 @@ class Runner:
9093
"""The memory service for the runner."""
9194
credential_service: Optional[BaseCredentialService] = None
9295
"""The credential service for the runner."""
96+
context_cache_config: Optional[ContextCacheConfig] = None
97+
"""The context cache config for the runner."""
98+
resumability_config: Optional[ResumabilityConfig] = None
99+
"""The resumability config for the application."""
93100

94101
def __init__(
95102
self,
@@ -110,11 +117,11 @@ def __init__(
110117
`ValueError`. Providing `app` is the recommended way to create a runner.
111118
112119
Args:
120+
app: An optional `App` instance. If provided, `app_name` and `agent`
121+
should not be specified.
113122
app_name: The application name of the runner. Required if `app` is not
114123
provided.
115124
agent: The root agent to run. Required if `app` is not provided.
116-
app: An optional `App` instance. If provided, `app_name` and `agent`
117-
should not be specified.
118125
plugins: Deprecated. A list of plugins for the runner. Please use the
119126
`app` argument to provide plugins instead.
120127
artifact_service: The artifact service for the runner.
@@ -126,9 +133,13 @@ def __init__(
126133
ValueError: If `app` is provided along with `app_name` or `plugins`, or
127134
if `app` is not provided but either `app_name` or `agent` is missing.
128135
"""
129-
self.app_name, self.agent, self.context_cache_config, plugins = (
130-
self._validate_runner_params(app, app_name, agent, plugins)
131-
)
136+
(
137+
self.app_name,
138+
self.agent,
139+
self.context_cache_config,
140+
self.resumability_config,
141+
plugins,
142+
) = self._validate_runner_params(app, app_name, agent, plugins)
132143
self.artifact_service = artifact_service
133144
self.session_service = session_service
134145
self.memory_service = memory_service
@@ -142,7 +153,11 @@ def _validate_runner_params(
142153
agent: Optional[BaseAgent],
143154
plugins: Optional[List[BasePlugin]],
144155
) -> tuple[
145-
str, BaseAgent, Optional[ContextCacheConfig], Optional[List[BasePlugin]]
156+
str,
157+
BaseAgent,
158+
Optional[ContextCacheConfig],
159+
Optional[ResumabilityConfig],
160+
Optional[List[BasePlugin]],
146161
]:
147162
"""Validates and extracts runner parameters.
148163
@@ -153,7 +168,8 @@ def _validate_runner_params(
153168
plugins: A list of plugins for the runner.
154169
155170
Returns:
156-
A tuple containing (app_name, agent, context_cache_config, plugins).
171+
A tuple containing (app_name, agent, context_cache_config,
172+
resumability_config, plugins).
157173
158174
Raises:
159175
ValueError: If parameters are invalid.
@@ -174,20 +190,22 @@ def _validate_runner_params(
174190
agent = app.root_agent
175191
plugins = app.plugins
176192
context_cache_config = app.context_cache_config
193+
resumability_config = app.resumability_config
177194
elif not app_name or not agent:
178195
raise ValueError(
179196
'Either app or both app_name and agent must be provided.'
180197
)
181198
else:
182199
context_cache_config = None
200+
resumability_config = None
183201

184202
if plugins:
185203
warnings.warn(
186204
'The `plugins` argument is deprecated. Please use the `app` argument'
187205
' to provide plugins instead.',
188206
DeprecationWarning,
189207
)
190-
return app_name, agent, context_cache_config, plugins
208+
return app_name, agent, context_cache_config, resumability_config, plugins
191209

192210
def run(
193211
self,
@@ -264,6 +282,7 @@ async def run_async(
264282
user_id: The user ID of the session.
265283
session_id: The session ID of the session.
266284
new_message: A new message to append to the session.
285+
state_delta: Optional state changes to apply to the session.
267286
run_config: The run config for the agent.
268287
269288
Yields:
@@ -687,6 +706,7 @@ def _new_invocation_context(
687706
user_content=new_message,
688707
live_request_queue=live_request_queue,
689708
run_config=run_config,
709+
resumability_config=self.resumability_config,
690710
)
691711

692712
def _new_invocation_context_for_live(

tests/unittests/apps/test_apps.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from google.adk.agents.base_agent import BaseAgent
1818
from google.adk.agents.context_cache_config import ContextCacheConfig
1919
from google.adk.apps.app import App
20+
from google.adk.apps.app import ResumabilityConfig
2021
from google.adk.plugins.base_plugin import BasePlugin
2122

2223

@@ -68,25 +69,48 @@ def test_app_initialization_with_cache_config(self):
6869
assert app.context_cache_config.ttl_seconds == 3600
6970
assert app.context_cache_config.min_tokens == 1024
7071

72+
def test_app_initialization_with_resumability_config(self):
73+
"""Test that the app is initialized correctly with app config."""
74+
mock_agent = Mock(spec=BaseAgent)
75+
resumability_config = ResumabilityConfig(
76+
is_resumable=True,
77+
)
78+
app = App(
79+
name="test_app",
80+
root_agent=mock_agent,
81+
resumability_config=resumability_config,
82+
)
83+
84+
assert app.name == "test_app"
85+
assert app.root_agent == mock_agent
86+
assert app.resumability_config == resumability_config
87+
assert app.resumability_config.is_resumable
88+
7189
def test_app_with_all_components(self):
7290
"""Test app with all components: agent, plugins, and cache config."""
7391
mock_agent = Mock(spec=BaseAgent)
7492
mock_plugin = Mock(spec=BasePlugin)
7593
cache_config = ContextCacheConfig(
7694
cache_intervals=20, ttl_seconds=7200, min_tokens=2048
7795
)
96+
resumability_config = ResumabilityConfig(
97+
is_resumable=True,
98+
)
7899

79100
app = App(
80101
name="full_test_app",
81102
root_agent=mock_agent,
82103
plugins=[mock_plugin],
83104
context_cache_config=cache_config,
105+
resumability_config=resumability_config,
84106
)
85107

86108
assert app.name == "full_test_app"
87109
assert app.root_agent == mock_agent
88110
assert app.plugins == [mock_plugin]
89111
assert app.context_cache_config == cache_config
112+
assert app.resumability_config == resumability_config
113+
assert app.resumability_config.is_resumable
90114

91115
def test_app_cache_config_defaults(self):
92116
"""Test that cache config has proper defaults when created."""
@@ -118,3 +142,29 @@ def test_app_context_cache_config_is_optional(self):
118142
context_cache_config=None,
119143
)
120144
assert app.context_cache_config is None
145+
146+
def test_app_resumability_config_defaults(self):
147+
"""Test that app config has proper defaults when created."""
148+
mock_agent = Mock(spec=BaseAgent)
149+
150+
app = App(
151+
name="default_resumability_config_app",
152+
root_agent=mock_agent,
153+
resumability_config=ResumabilityConfig(),
154+
)
155+
assert app.resumability_config is not None
156+
assert not app.resumability_config.is_resumable # Default
157+
158+
def test_app_resumability_config_is_optional(self):
159+
"""Test that resumability_config is truly optional."""
160+
mock_agent = Mock(spec=BaseAgent)
161+
162+
app = App(name="no_resumability_config_app", root_agent=mock_agent)
163+
assert app.resumability_config is None
164+
165+
app = App(
166+
name="explicit_none_resumability_config_app",
167+
root_agent=mock_agent,
168+
resumability_config=None,
169+
)
170+
assert app.resumability_config is None

tests/unittests/test_runners.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
from google.adk.agents.invocation_context import InvocationContext
2020
from google.adk.agents.llm_agent import LlmAgent
2121
from google.adk.apps.app import App
22+
from google.adk.apps.app import ResumabilityConfig
2223
from google.adk.artifacts.in_memory_artifact_service import InMemoryArtifactService
2324
from google.adk.events.event import Event
2425
from google.adk.plugins.base_plugin import BasePlugin
@@ -565,6 +566,7 @@ def test_runner_validate_params_return_order(self):
565566
name="order_test_app",
566567
root_agent=self.root_agent,
567568
context_cache_config=cache_config,
569+
resumability_config=ResumabilityConfig(is_resumable=True),
568570
)
569571

570572
runner = Runner(
@@ -574,14 +576,15 @@ def test_runner_validate_params_return_order(self):
574576
)
575577

576578
# Test the validation method directly
577-
app_name, agent, context_cache_config, plugins = (
579+
app_name, agent, context_cache_config, resumability_config, plugins = (
578580
runner._validate_runner_params(app, None, None, None)
579581
)
580582

581583
assert app_name == "order_test_app"
582584
assert agent == self.root_agent
583585
assert context_cache_config == cache_config
584586
assert context_cache_config.cache_intervals == 25
587+
assert resumability_config == app.resumability_config
585588
assert plugins == []
586589

587590
def test_runner_validate_params_without_app(self):
@@ -593,13 +596,14 @@ def test_runner_validate_params_without_app(self):
593596
artifact_service=self.artifact_service,
594597
)
595598

596-
app_name, agent, context_cache_config, plugins = (
599+
app_name, agent, context_cache_config, resumability_config, plugins = (
597600
runner._validate_runner_params(None, "test_app", self.root_agent, None)
598601
)
599602

600603
assert app_name == "test_app"
601604
assert agent == self.root_agent
602605
assert context_cache_config is None
606+
assert resumability_config is None
603607
assert plugins is None
604608

605609
def test_runner_app_name_and_agent_extracted_correctly(self):

0 commit comments

Comments
 (0)