Skip to content

Commit 6f7db7f

Browse files
committed
fix: exit connection cleanly on expected GoAway signal in bidi streaming
Receive the GoAway signal from the Gemini Live API, set a flag on the InvocationContext indicating reconnection is requested, and exit the receive generator cleanly instead of raising a ConnectionClosed exception. This avoids throwing expected session-recycling exceptions into custom client wrappers, which helps prevent false alarms in custom client log monitors. Co-authored-by: Kathy Wu <wukathy@google.com> PiperOrigin-RevId: 937586604
1 parent e72bf9b commit 6f7db7f

2 files changed

Lines changed: 26 additions & 5 deletions

File tree

src/google/adk/flows/llm_flows/base_llm_flow.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@
5757
# Prefix used by toolset auth credential IDs
5858
TOOLSET_AUTH_CREDENTIAL_ID_PREFIX = '_adk_toolset_auth_'
5959

60+
61+
class _ReconnectSentinel(Event):
62+
"""Internal sentinel event to signal a silent reconnection request."""
63+
64+
6065
if TYPE_CHECKING:
6166
from ...agents.llm_agent import LlmAgent
6267
from ...models.base_llm import BaseLlm
@@ -577,6 +582,7 @@ async def run_live(
577582
self._send_to_model(llm_connection, invocation_context)
578583
)
579584

585+
should_reconnect = False
580586
try:
581587
async with Aclosing(
582588
self._receive_from_model(
@@ -587,6 +593,9 @@ async def run_live(
587593
)
588594
) as agen:
589595
async for event in agen:
596+
if isinstance(event, _ReconnectSentinel):
597+
should_reconnect = True
598+
break
590599
# Empty event means the queue is closed.
591600
if not event:
592601
break
@@ -667,6 +676,9 @@ async def run_live(
667676
await send_task
668677
except asyncio.CancelledError:
669678
pass
679+
if should_reconnect:
680+
continue
681+
break
670682
except (ConnectionClosed, ConnectionClosedOK) as e:
671683
# If we have a session resumption handle, we attempt to reconnect.
672684
# This handle is updated dynamically during the session.
@@ -805,9 +817,9 @@ def get_author_for_event(llm_response: LlmResponse) -> str:
805817
if llm_response.go_away:
806818
logger.info(f'Received go away signal: {llm_response.go_away}')
807819
# The server signals that it will close the connection soon.
808-
# We proactively raise ConnectionClosed to trigger the reconnection
809-
# logic in run_live, which will use the latest session handle.
810-
raise ConnectionClosed(None, None)
820+
# We yield a sentinel event to request reconnection internally.
821+
yield _ReconnectSentinel()
822+
return
811823

812824
model_response_event = Event(
813825
id=Event.new_id(),

tests/unittests/flows/llm_flows/test_base_llm_flow.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
from google.adk.agents.run_config import RunConfig
2323
from google.adk.events.event import Event
2424
from google.adk.flows.llm_flows.base_llm_flow import _handle_after_model_callback
25+
from google.adk.flows.llm_flows.base_llm_flow import _ReconnectSentinel
2526
from google.adk.flows.llm_flows.base_llm_flow import BaseLlmFlow
2627
from google.adk.models.google_llm import Gemini
2728
from google.adk.models.google_llm import GoogleLLMVariant
@@ -728,15 +729,23 @@ async def mock_receive_2():
728729
) as mock_connect:
729730
mock_connect.return_value.__aenter__ = mock_aenter
730731

732+
yielded_events = []
731733
try:
732-
async for _ in flow.run_live(invocation_context):
733-
pass
734+
async for event in flow.run_live(invocation_context):
735+
yielded_events.append(event)
734736
except StopError:
735737
pass
736738

737739
# Verify that we attempted to connect twice (initial + reconnect after go_away).
738740
assert mock_connect.call_count == 2
739741

742+
# Verify that the internal _ReconnectSentinel is not leaked/yielded to the caller.
743+
assert not any(isinstance(e, _ReconnectSentinel) for e in yielded_events)
744+
745+
# Verify we yielded the expected response after reconnection.
746+
assert len(yielded_events) == 1
747+
assert yielded_events[0].content.parts[0].text == 'hi'
748+
740749

741750
@pytest.mark.asyncio
742751
async def test_run_live_no_reconnect_without_handle():

0 commit comments

Comments
 (0)