From 30b2ed3ef8ee6d3633743c0db00533683d3342d8 Mon Sep 17 00:00:00 2001 From: Akshat8510 Date: Fri, 13 Feb 2026 09:11:29 -0800 Subject: [PATCH] fix(web): allow session resume without new message Merge https://github.com/google/adk-python/pull/4185 **Description** This PR resolves #4100 by making the `new_message` field optional in the `RunAgentRequest` model. **The Problem:** When attempting to resume an agent session via the FastAPI web server, the request would fail with a `422 Unprocessable Entity` if `new_message` was omitted. This prevented "resume-only" workflows where a user just wants to wake up an existing session. **The Solution:** Updated `RunAgentRequest.new_message` to be `Optional[types.Content] = None`. The underlying `runner.run_async` logic already supports `None` for resuming purposes, so no further logic changes were required. **Verification:** Verified that `RunAgentRequest` now validates successfully when `new_message` is missing, defaulting the field to `None`. Co-authored-by: Liang Wu COPYBARA_INTEGRATE_REVIEW=https://github.com/google/adk-python/pull/4185 from Akshat8510:fix/fastapi-resume-4100 b6d252636aa5f96186507fccf47a278fe733a362 PiperOrigin-RevId: 869761179 --- src/google/adk/cli/adk_web_server.py | 2 +- tests/unittests/cli/test_fast_api.py | 21 +++++++++++++++++---- 2 files changed, 18 insertions(+), 5 deletions(-) diff --git a/src/google/adk/cli/adk_web_server.py b/src/google/adk/cli/adk_web_server.py index 3536afb5..c61f855f 100644 --- a/src/google/adk/cli/adk_web_server.py +++ b/src/google/adk/cli/adk_web_server.py @@ -204,7 +204,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 diff --git a/tests/unittests/cli/test_fast_api.py b/tests/unittests/cli/test_fast_api.py index 1ab1d41f..913e11ae 100755 --- a/tests/unittests/cli/test_fast_api.py +++ b/tests/unittests/cli/test_fast_api.py @@ -18,9 +18,7 @@ import logging import os from pathlib import Path import signal -import sys import tempfile -import time from typing import Any from typing import Optional from unittest.mock import AsyncMock @@ -38,14 +36,12 @@ from google.adk.errors.input_validation_error import InputValidationError from google.adk.evaluation.eval_case import EvalCase from google.adk.evaluation.eval_case import Invocation from google.adk.evaluation.eval_result import EvalSetResult -from google.adk.evaluation.eval_set import EvalSet from google.adk.evaluation.in_memory_eval_sets_manager import InMemoryEvalSetsManager from google.adk.events.event import Event from google.adk.events.event_actions import EventActions from google.adk.runners import Runner from google.adk.sessions.in_memory_session_service import InMemorySessionService from google.adk.sessions.session import Session -from google.adk.sessions.state import State from google.genai import types from pydantic import BaseModel import pytest @@ -1530,6 +1526,23 @@ 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_success( + test_app, create_test_session +): + """Test that /run allows resuming a session with only an invocation_id, without 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"], + "invocation_id": "test_invocation_id", + "streaming": False, + } + response = test_app.post(url, json=payload) + assert response.status_code == 200 + + def test_health_endpoint(test_app): """Test the health endpoint.""" response = test_app.get("/health")