Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
fe24e72
fix(web): allow session resume without new message (#4100)
Akshat8510 Jan 17, 2026
1f4f0aa
fix(web): adds a unit test verifying that the /run endpoint accepts r…
Akshat8510 Jan 17, 2026
929fe2e
fix: allow agent session resumption without new message
Akshat8510 Jan 17, 2026
1afb89a
Style: cleanup unit test by removing unnecessary logging and blank lines
Akshat8510 Jan 17, 2026
d10c1d3
fix: add observability logging for no-op resume and cleanup tests
Akshat8510 Jan 17, 2026
00b0f20
fix: align dummy runner logic with real runner and update docstring
Akshat8510 Jan 17, 2026
2987931
docs: remove misleading comment in unit test per bot feedback
Akshat8510 Jan 17, 2026
c2511c5
fix: handle state_delta warning in no-op resume and cleanup dummy runner
Akshat8510 Jan 18, 2026
ac9f904
fix: the formatting
Akshat8510 Jan 21, 2026
1fdf7ef
fix: finalize session resumption logic and test coverage
Akshat8510 Jan 24, 2026
5af99c7
fix: align mock warning with production and update docstring
Akshat8510 Jan 24, 2026
e3ed269
test: improve robustness of no-op resume warning assertion
Akshat8510 Jan 24, 2026
d8d3401
test: improve robustness of no-op resume warning assertion
Akshat8510 Jan 24, 2026
c5ac29a
fix: implement graceful session resumption and separate test concerns
Akshat8510 Jan 24, 2026
a397ea3
refactor: use targeted mocks for session resumption tests per bot fee…
Akshat8510 Jan 24, 2026
222cd2d
refactor: remove logic duplication in global mock per bot feedback
Akshat8510 Jan 24, 2026
1faac6f
refactor: use parameterized tests for session resumption scenarios
Akshat8510 Jan 24, 2026
74bb3db
style: use explicit empty loop for async generator mock per bot feedback
Akshat8510 Jan 24, 2026
ee6ab89
Merge branch 'main' into fix/fastapi-resume-4100
Akshat8510 Jan 26, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/google/adk/cli/adk_web_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,7 @@ class RunAgentRequest(common.BaseModel):
app_name: str
user_id: str
session_id: str
new_message: types.Content
new_message: Optional[types.Content] = None
streaming: bool = False
state_delta: Optional[dict[str, Any]] = None
# for resume long-running functions
Expand Down
21 changes: 15 additions & 6 deletions src/google/adk/runners.py
Original file line number Diff line number Diff line change
Expand Up @@ -481,8 +481,7 @@ async def run_async(
The events generated by the agent.

Raises:
ValueError: If the session is not found; If both invocation_id and
new_message are None.
ValueError: If the session is not found and `auto_create_session` is False.
"""
run_config = run_config or RunConfig()

Expand All @@ -497,12 +496,22 @@ async def _run_with_trace(
session = await self._get_or_create_session(
user_id=user_id, session_id=session_id
)

if not invocation_id and not new_message:
raise ValueError(
'Running an agent requires either a new_message or an '
'invocation_id to resume a previous invocation. '
f'Session: {session_id}, User: {user_id}'
if state_delta:
logger.warning(
'state_delta provided without new_message or invocation_id for '
'session %s. The state_delta will be ignored.',
session_id,
)
logger.info(
'Performing no-op resume for session %s: no new_message or '
'invocation_id.',
session_id,
)
# If nothing is provided, this is a no-op resume. We return early
# without yielding any events.
return

if invocation_id:
if (
Expand Down
39 changes: 39 additions & 0 deletions tests/unittests/cli/test_fast_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@ async def dummy_run_async(
run_config: Optional[RunConfig] = None,
invocation_id: Optional[str] = None,
):

run_config = run_config or RunConfig()
yield _event_1()
await asyncio.sleep(0)
Expand Down Expand Up @@ -1411,5 +1412,43 @@ def test_builder_save_rejects_traversal(builder_test_client, tmp_path):
assert not (tmp_path / "app" / "tmp" / "escape.yaml").exists()


async def _noop_run_async(*args, **kwargs):
"""A mock that does nothing and yields no events for no-op resume tests."""
for item in []:
yield item


@pytest.mark.parametrize(
"extra_payload",
[
{},
{"state_delta": {"some_key": "some_value"}},
],
ids=["no_state_delta", "with_state_delta"],
)
def test_agent_run_resume_without_message(
test_app, create_test_session, monkeypatch, extra_payload
):
"""Test that /run allows resuming a session without providing a new message."""
# Override the global mock with a specific no-op mock for this test
monkeypatch.setattr("google.adk.runners.Runner.run_async", _noop_run_async)

info = create_test_session
url = "/run"
payload = {
"app_name": info["app_name"],
"user_id": info["user_id"],
"session_id": info["session_id"],
"streaming": False,
**extra_payload,
}

response = test_app.post(url, json=payload)

# Verify the web server handles the request and returns success
assert response.status_code == 200
assert response.json() == []


if __name__ == "__main__":
pytest.main(["-xvs", __file__])
37 changes: 36 additions & 1 deletion tests/unittests/test_runners.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import importlib
import logging
from pathlib import Path
import sys
import textwrap
from typing import AsyncGenerator
from typing import Optional
from unittest import mock
from unittest.mock import AsyncMock

from google.adk.agents.base_agent import BaseAgent
Expand Down Expand Up @@ -1321,5 +1322,39 @@ def test_infer_agent_origin_detects_mismatch_for_user_agent(
assert "actual_name" in runner._app_name_alignment_hint


@pytest.mark.asyncio
async def test_run_async_no_op_resume_logging(caplog):
"""Verifies that the actual Runner logic logs a warning when state_delta is ignored."""
from google.adk.runners import Runner

# 1. Setup dependencies
mock_agent = mock.MagicMock()
mock_agent.name = "test_agent"

# Added app_name="test_app" to satisfy validation
runner = Runner(
app_name="test_app",
agent=mock_agent,
session_service=mock.AsyncMock(),
)

# 2. Capture logs while running the actual logic in runners.py
with caplog.at_level(logging.WARNING):
# Call run_async without message or invocation_id, but WITH state_delta
async for _ in runner.run_async(
user_id="user",
session_id="session",
new_message=None,
state_delta={"test": 1},
):
pass

# 3. This verifies the REAL warning logic in src/google/adk/runners.py
assert any(
"state_delta provided without new_message" in r.message
for r in caplog.records
)


if __name__ == "__main__":
pytest.main([__file__])