Skip to content

Commit 987de91

Browse files
Ensure ADK runners are closed after executions
1 parent df9efcb commit 987de91

File tree

2 files changed

+44
-6
lines changed

2 files changed

+44
-6
lines changed

typescript-sdk/integrations/adk-middleware/python/src/ag_ui_adk/adk_agent.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -834,20 +834,21 @@ async def _run_adk_in_background(
834834
app_name: App name
835835
event_queue: Queue for emitting events
836836
"""
837+
runner: Optional[Runner] = None
837838
try:
838839
# Agent is already prepared with tools and SystemMessage instructions (if any)
839840
# from _start_background_execution, so no additional agent copying needed here
840-
841+
841842
# Create runner
842843
runner = self._create_runner(
843844
adk_agent=adk_agent,
844845
user_id=user_id,
845846
app_name=app_name
846847
)
847-
848+
848849
# Create RunConfig
849850
run_config = self._run_config_factory(input)
850-
851+
851852
# Ensure session exists
852853
await self._ensure_session_exists(
853854
app_name, user_id, input.thread_id, input.state
@@ -994,9 +995,20 @@ async def _run_adk_in_background(
994995
await event_queue.put(None)
995996
finally:
996997
# Background task cleanup completed
997-
# Note: toolset cleanup is handled by garbage collection
998-
# since toolset is now embedded in the agent's tools
999-
pass
998+
# Ensure the ADK runner releases any resources (e.g. toolsets)
999+
if runner is not None:
1000+
close_method = getattr(runner, "close", None)
1001+
if close_method is not None:
1002+
try:
1003+
close_result = close_method()
1004+
if inspect.isawaitable(close_result):
1005+
await close_result
1006+
except Exception as close_error:
1007+
logger.warning(
1008+
"Error while closing ADK runner for thread %s: %s",
1009+
input.thread_id,
1010+
close_error,
1011+
)
10001012

10011013
async def _cleanup_stale_executions(self):
10021014
"""Clean up stale executions."""

typescript-sdk/integrations/adk-middleware/python/tests/test_adk_agent.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ async def test_run_basic_flow(self, adk_agent, sample_input, mock_agent):
113113
with patch.object(adk_agent, '_create_runner') as mock_create_runner:
114114
# Create a mock runner
115115
mock_runner = AsyncMock()
116+
mock_runner.close = AsyncMock()
116117
mock_event = Mock()
117118
mock_event.id = "event1"
118119
mock_event.author = "test_agent"
@@ -139,6 +140,31 @@ async def mock_run_async(*args, **kwargs):
139140
assert len(events) >= 2 # At least RUN_STARTED and RUN_FINISHED
140141
assert events[0].type == EventType.RUN_STARTED
141142
assert events[-1].type == EventType.RUN_FINISHED
143+
mock_runner.close.assert_awaited_once()
144+
145+
@pytest.mark.asyncio
146+
async def test_runner_close_called_on_run_error(self, adk_agent, sample_input):
147+
"""Runner.close should still be awaited when execution errors."""
148+
149+
with patch.object(adk_agent, '_create_runner') as mock_create_runner:
150+
mock_runner = AsyncMock()
151+
mock_runner.close = AsyncMock()
152+
153+
async def failing_run_async(*args, **kwargs):
154+
if False: # pragma: no cover - keep async generator semantics
155+
yield None
156+
raise RuntimeError("boom")
157+
158+
mock_runner.run_async = failing_run_async
159+
mock_create_runner.return_value = mock_runner
160+
161+
events = []
162+
async for event in adk_agent.run(sample_input):
163+
events.append(event)
164+
165+
# Ensure RUN_ERROR emitted and runner closed
166+
assert any(event.type == EventType.RUN_ERROR for event in events)
167+
mock_runner.close.assert_awaited_once()
142168

143169
@pytest.mark.asyncio
144170
async def test_turn_complete_falls_back_to_streaming_translator(

0 commit comments

Comments
 (0)