fix: remove duplicate session GET when using API server, unbreak auto_session_create when using API server

Co-authored-by: Sasha Sobran <asobran@google.com>
PiperOrigin-RevId: 874188082
This commit is contained in:
Sasha Sobran
2026-02-23 12:01:24 -08:00
committed by Copybara-Service
parent 2dbd1f25bd
commit 445dc189e9
5 changed files with 203 additions and 76 deletions
+55 -39
View File
@@ -68,6 +68,7 @@ from ..auth.credential_service.base_credential_service import BaseCredentialServ
from ..errors.already_exists_error import AlreadyExistsError
from ..errors.input_validation_error import InputValidationError
from ..errors.not_found_error import NotFoundError
from ..errors.session_not_found_error import SessionNotFoundError
from ..evaluation.base_eval_service import InferenceConfig
from ..evaluation.base_eval_service import InferenceRequest
from ..evaluation.constants import MISSING_EVAL_DEPENDENCIES_MESSAGE
@@ -1558,53 +1559,68 @@ class AdkWebServer:
@app.post("/run", response_model_exclude_none=True)
async def run_agent(req: RunAgentRequest) -> list[Event]:
session = await self.session_service.get_session(
app_name=req.app_name, user_id=req.user_id, session_id=req.session_id
)
if not session:
raise HTTPException(status_code=404, detail="Session not found")
runner = await self.get_runner_async(req.app_name)
async with Aclosing(
runner.run_async(
user_id=req.user_id,
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]
try:
async with Aclosing(
runner.run_async(
user_id=req.user_id,
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]
except SessionNotFoundError as e:
raise HTTPException(status_code=404, detail=str(e)) from e
logger.info("Generated %s events in agent run", len(events))
logger.debug("Events generated: %s", events)
return events
@app.post("/run_sse")
async def run_agent_sse(req: RunAgentRequest) -> StreamingResponse:
# SSE endpoint
session = await self.session_service.get_session(
app_name=req.app_name, user_id=req.user_id, session_id=req.session_id
stream_mode = StreamingMode.SSE if req.streaming else StreamingMode.NONE
runner = await self.get_runner_async(req.app_name)
agen = runner.run_async(
user_id=req.user_id,
session_id=req.session_id,
new_message=req.new_message,
state_delta=req.state_delta,
run_config=RunConfig(streaming_mode=stream_mode),
invocation_id=req.invocation_id,
)
if not session:
raise HTTPException(status_code=404, detail="Session not found")
# Eagerly advance the generator to trigger session validation
# before the streaming response is created. This lets us return
# a proper HTTP 404 for missing sessions without a redundant
# get_session call — the Runner's single _get_or_create_session
# call is the only one that runs.
first_event = None
first_error = None
try:
first_event = await anext(agen)
except SessionNotFoundError as e:
await agen.aclose()
raise HTTPException(status_code=404, detail=str(e)) from e
except StopAsyncIteration:
await agen.aclose()
except Exception as e:
first_error = e
# Convert the events to properly formatted SSE
async def event_generator():
try:
stream_mode = (
StreamingMode.SSE if req.streaming else StreamingMode.NONE
)
runner = await self.get_runner_async(req.app_name)
async with Aclosing(
runner.run_async(
user_id=req.user_id,
session_id=req.session_id,
new_message=req.new_message,
state_delta=req.state_delta,
run_config=RunConfig(streaming_mode=stream_mode),
invocation_id=req.invocation_id,
)
) as agen:
async for event in agen:
async with Aclosing(agen):
try:
if first_error:
raise first_error
async def all_events():
if first_event is not None:
yield first_event
async for event in agen:
yield event
async for event in all_events():
# ADK Web renders artifacts from `actions.artifactDelta`
# during part processing *and* during action processing
# 1) the original event with `artifactDelta` cleared (content)
@@ -1630,9 +1646,9 @@ class AdkWebServer:
"Generated event in agent run streaming: %s", sse_event
)
yield f"data: {sse_event}\n\n"
except Exception as e:
logger.exception("Error in event_generator: %s", e)
yield f"data: {json.dumps({'error': str(e)})}\n\n"
except Exception as e:
logger.exception("Error in event_generator: %s", e)
yield f"data: {json.dumps({'error': str(e)})}\n\n"
# Returns a streaming response with the proper media type for SSE
return StreamingResponse(
@@ -0,0 +1,28 @@
# Copyright 2026 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# 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.
from __future__ import annotations
from .not_found_error import NotFoundError
class SessionNotFoundError(ValueError, NotFoundError):
"""Raised when a session cannot be found.
Inherits from both ValueError (for backward compatibility) and NotFoundError
(for semantic consistency with the project's error hierarchy).
"""
def __init__(self, message="Session not found."):
super().__init__(message)
+5 -3
View File
@@ -45,6 +45,7 @@ from .artifacts.base_artifact_service import BaseArtifactService
from .artifacts.in_memory_artifact_service import InMemoryArtifactService
from .auth.credential_service.base_credential_service import BaseCredentialService
from .code_executors.built_in_code_executor import BuiltInCodeExecutor
from .errors.session_not_found_error import SessionNotFoundError
from .events.event import Event
from .events.event import EventActions
from .flows.llm_flows import contents
@@ -358,7 +359,7 @@ class Runner:
This helper first attempts to retrieve the session. If not found and
auto_create_session is True, it creates a new session with the provided
identifiers. Otherwise, it raises a ValueError with a helpful message.
identifiers. Otherwise, it raises a SessionNotFoundError.
Args:
user_id: The user ID of the session.
@@ -368,7 +369,8 @@ class Runner:
The existing or newly created `Session`.
Raises:
ValueError: If the session is not found and auto_create_session is False.
SessionNotFoundError: If the session is not found and
auto_create_session is False.
"""
session = await self.session_service.get_session(
app_name=self.app_name, user_id=user_id, session_id=session_id
@@ -380,7 +382,7 @@ class Runner:
)
else:
message = self._format_session_not_found_message(session_id)
raise ValueError(message)
raise SessionNotFoundError(message)
return session
def run(