Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 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
cfbbd30
fix: resolve web server validation and 12+ core mypy errors
Akshat8510 Jan 30, 2026
f496242
fix: resolve mypy type mismatches in test_fast_api.py
Akshat8510 Jan 30, 2026
8de205a
fix: revert no-op logic and resolve remaining mypy errors in runners.py
Akshat8510 Jan 30, 2026
892834e
fix: revert to thread-safe queue and remove unreachable logic per review
Akshat8510 Jan 30, 2026
1e06548
fix: address all final maintainer and bot review comments
Akshat8510 Jan 30, 2026
74f2a14
fix: resolve mypy narrowing error and pass invocation_id in web server
Akshat8510 Jan 30, 2026
ef8f62a
chore: remove unused no-op mock and finalize PR logic
Akshat8510 Jan 30, 2026
ec185a7
Merge branch 'main' into fix/fastapi-resume-4100
Akshat8510 Jan 30, 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
13 changes: 7 additions & 6 deletions 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 Expand Up @@ -369,7 +369,7 @@ def _otel_env_vars_enabled() -> bool:


def _setup_gcp_telemetry(
internal_exporters: list[SpanProcessor] = None,
internal_exporters: list[SpanProcessor] | None = None,
):
if typing.TYPE_CHECKING:
from ..telemetry.setup import OTelHooks
Expand Down Expand Up @@ -411,7 +411,7 @@ def _setup_gcp_telemetry(


def _setup_telemetry_from_env(
internal_exporters: list[SpanProcessor] = None,
internal_exporters: list[SpanProcessor] | None = None,
):
from ..telemetry.setup import maybe_set_otel_providers

Expand Down Expand Up @@ -507,7 +507,7 @@ def __init__(
# Internal properties we want to allow being modified from callbacks.
self.runners_to_clean: set[str] = set()
self.current_app_name_ref: SharedValue[str] = SharedValue(value="")
self.runner_dict = {}
self.runner_dict: dict[str, Runner] = {}
self.url_prefix = url_prefix

async def get_runner_async(self, app_name: str) -> Runner:
Expand Down Expand Up @@ -707,8 +707,8 @@ def get_fast_api_app(
A FastAPI app instance.
"""
# Properties we don't need to modify from callbacks
trace_dict = {}
session_trace_dict = {}
trace_dict: dict[str, Any] = {}
session_trace_dict: dict[str, list[int]] = {}
# Set up a file system watcher to detect changes in the agents directory.
observer = Observer()
setup_observer(observer, self)
Expand Down Expand Up @@ -1413,6 +1413,7 @@ async def run_agent(req: RunAgentRequest) -> list[Event]:
session_id=req.session_id,
new_message=req.new_message,
state_delta=req.state_delta,
invocation_id=req.invocation_id,
)
) as agen:
events = [event async for event in agen]
Expand Down
19 changes: 8 additions & 11 deletions src/google/adk/runners.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
from typing import Any
from typing import AsyncGenerator
from typing import Callable
from typing import cast
from typing import Generator
from typing import List
from typing import Optional
Expand Down Expand Up @@ -414,7 +415,7 @@ def run(
The events generated by the agent.
"""
run_config = run_config or RunConfig()
event_queue = queue.Queue()
event_queue: queue.Queue[Optional[Event]] = queue.Queue()

async def _invoke_run_async():
try:
Expand Down Expand Up @@ -481,8 +482,8 @@ 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,
or if both `invocation_id` and `new_message` are `None`.
"""
run_config = run_config or RunConfig()

Expand All @@ -497,6 +498,7 @@ 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 '
Expand Down Expand Up @@ -1002,7 +1004,7 @@ async def run_live(
)
if not session:
session = await self._get_or_create_session(
user_id=user_id, session_id=session_id
user_id=cast(str, user_id), session_id=cast(str, session_id)
)
invocation_context = self._new_invocation_context_for_live(
session,
Expand Down Expand Up @@ -1321,7 +1323,7 @@ async def _setup_context_for_resumed_invocation(

# Step 1: Maybe retrieve a previous user message for the invocation.
user_message = new_message or self._find_user_message_for_invocation(
session.events, invocation_id
session.events, cast(str, invocation_id)
)
if not user_message:
raise ValueError(
Expand Down Expand Up @@ -1537,12 +1539,7 @@ async def close(self):

logger.info('Runner closed.')

if sys.version_info < (3, 11):
Self = 'Runner' # pylint: disable=invalid-name
else:
from typing import Self # pylint: disable=g-import-not-at-top

async def __aenter__(self) -> Self:
async def __aenter__(self) -> 'Runner':
"""Async context manager entry."""
return self

Expand Down
34 changes: 31 additions & 3 deletions tests/unittests/cli/test_fast_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@
from google.adk.sessions.state import State
from google.genai import types
from pydantic import BaseModel
from pydantic import Field
import pytest

# Configure logging to help diagnose server startup issues
Expand Down Expand Up @@ -132,6 +133,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 All @@ -154,9 +156,9 @@ class _MockEvalCaseResult(BaseModel):
user_id: str
session_id: str
eval_set_file: str
eval_metric_results: list = {}
overall_eval_metric_results: list = ({},)
eval_metric_result_per_invocation: list = {}
eval_metric_results: list = Field(default_factory=list)
overall_eval_metric_results: list = Field(default_factory=list)
eval_metric_result_per_invocation: list = Field(default_factory=list)


#################################################
Expand Down Expand Up @@ -1336,5 +1338,31 @@ def test_builder_save_rejects_traversal(builder_test_client, tmp_path):
assert not (tmp_path / "app" / "tmp" / "escape.yaml").exists()


@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_success(
test_app, create_test_session, extra_payload
):
"""Test that /run allows resuming a session with only an invocation_id."""
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,
**extra_payload,
}
response = test_app.post(url, json=payload)
assert response.status_code == 200


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