Skip to content
Open
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-creation is disabled.
"""
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
23 changes: 23 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,12 @@ async def dummy_run_async(
run_config: Optional[RunConfig] = None,
invocation_id: Optional[str] = None,
):

if not invocation_id and not new_message:
if state_delta:
logger.warning("state_delta ignored in no-op resume")
return

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


def test_agent_run_resume_without_message(test_app, create_test_session):
"""Test that /run allows resuming a session without providing a new message."""
info = create_test_session
url = "/run"
payload = {
"app_name": info["app_name"],
"user_id": info["user_id"],
"session_id": info["session_id"],
"streaming": False,
}

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

# Verify the web server and dummy runner work together to return success
assert response.status_code == 200
assert response.json() == []

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