🔴 Required Information
Describe the Bug:
On Agent Engine deployments served by the ADK API server, every call to the /api/stream_reasoning_engine route with a synchronous streaming class_method (e.g. stream_query) ends with RuntimeError: coroutine raised StopIteration after the last chunk is streamed.
The cause is the sync-to-async adapter in src/google/adk/cli/fast_api.py (lines 916–922 in v2.2.0):
async def _aiter_from_iter(iterator):
while True:
try:
chunk = await run_in_threadpool(next, iterator)
yield chunk
except StopIteration:
break
The except StopIteration is unreachable. When the iterator is exhausted, next() raises StopIteration inside the worker thread, anyio sets it on a future, and it propagates out of the run_in_threadpool coroutine frame. Python forbids StopIteration escaping a coroutine and converts it to RuntimeError("coroutine raised StopIteration") before the except clause ever sees it.
Steps to Reproduce:
- Install
google-adk==2.2.0.
- Deploy any ADK agent to Vertex AI Agent Engine (served via the ADK API server).
- Call the engine with
class_method: "stream_query" (e.g. remote_agent.stream_query(...) from the vertexai SDK), which hits /api/stream_reasoning_engine.
- All chunks stream successfully, then the server logs
RuntimeError: coroutine raised StopIteration.
The mechanism reproduces standalone in ~15 lines (see below).
Expected Behavior:
The stream terminates cleanly when the sync generator is exhausted, with no exception.
Observed Behavior:
After the final chunk, the server raises:
RuntimeError: coroutine raised StopIteration
with a traceback through starlette/responses.py stream_response → fast_api.py:797 json_generator → fast_api.py:919 _aiter_from_iter → starlette/concurrency.py run_in_threadpool → anyio/_backends/_asyncio.py run_sync_in_worker_thread. The full reasoning-engine stderr log is attached below.
Environment Details:
- ADK Library Version (pip show google-adk): 2.2.0
- Desktop OS: Linux (Agent Engine container, Python 3.11); also reproduced on macOS
- Python Version (python -V): 3.11
Model Information:
- Are you using LiteLLM: No
- Which model is being used: N/A (bug is in the FastAPI streaming adapter, model-agnostic)
🟡 Optional Information
Regression:
Unknown; the adapter exists in the current latest release (2.2.0).
Logs:
ERROR: Exception in ASGI application
Traceback (most recent call last):
File ".../starlette/responses.py", line 250, in stream_response
async for chunk in self.body_iterator:
File ".../google/adk/cli/fast_api.py", line 797, in json_generator
async for chunk in output:
File ".../google/adk/cli/fast_api.py", line 919, in _aiter_from_iter
chunk = await run_in_threadpool(next, iterator)
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File ".../starlette/concurrency.py", line 32, in run_in_threadpool
return await anyio.to_thread.run_sync(func)
File ".../anyio/to_thread.py", line 63, in run_sync
return await get_async_backend().run_sync_in_worker_thread(
File ".../anyio/_backends/_asyncio.py", line 2518, in run_sync_in_worker_thread
return await future
^^^^^^^^^^^^
RuntimeError: coroutine raised StopIteration
Screenshots / Video:
N/A
Additional Context:
Suggested fix: Stop relying on StopIteration crossing an await boundary:
_SENTINEL = object()
async def _aiter_from_iter(iterator):
while True:
chunk = await run_in_threadpool(next, iterator, _SENTINEL)
if chunk is _SENTINEL:
break
yield chunk
Minimal Reproduction Code:
import asyncio
from starlette.concurrency import run_in_threadpool
async def _aiter_from_iter(iterator):
# exact copy of google/adk/cli/fast_api.py:916-922 (v2.2.0)
while True:
try:
chunk = await run_in_threadpool(next, iterator)
yield chunk
except StopIteration:
break
async def main():
def gen():
yield 1
yield 2
async for c in _aiter_from_iter(gen()):
print("chunk:", c)
asyncio.run(main())
# chunk: 1
# chunk: 2
# RuntimeError: coroutine raised StopIteration
How often has this issue occurred?:
- Always (100%) — on every sync streaming request once the generator is exhausted.
🔴 Required Information
Describe the Bug:
On Agent Engine deployments served by the ADK API server, every call to the
/api/stream_reasoning_engineroute with a synchronous streamingclass_method(e.g.stream_query) ends withRuntimeError: coroutine raised StopIterationafter the last chunk is streamed.The cause is the sync-to-async adapter in
src/google/adk/cli/fast_api.py(lines 916–922 in v2.2.0):The
except StopIterationis unreachable. When the iterator is exhausted,next()raisesStopIterationinside the worker thread, anyio sets it on a future, and it propagates out of therun_in_threadpoolcoroutine frame. Python forbidsStopIterationescaping a coroutine and converts it toRuntimeError("coroutine raised StopIteration")before theexceptclause ever sees it.Steps to Reproduce:
google-adk==2.2.0.class_method: "stream_query"(e.g.remote_agent.stream_query(...)from the vertexai SDK), which hits/api/stream_reasoning_engine.RuntimeError: coroutine raised StopIteration.The mechanism reproduces standalone in ~15 lines (see below).
Expected Behavior:
The stream terminates cleanly when the sync generator is exhausted, with no exception.
Observed Behavior:
After the final chunk, the server raises:
with a traceback through
starlette/responses.pystream_response→fast_api.py:797 json_generator→fast_api.py:919 _aiter_from_iter→starlette/concurrency.py run_in_threadpool→anyio/_backends/_asyncio.py run_sync_in_worker_thread. The full reasoning-engine stderr log is attached below.Environment Details:
Model Information:
🟡 Optional Information
Regression:
Unknown; the adapter exists in the current latest release (2.2.0).
Logs:
Screenshots / Video:
N/A
Additional Context:
Suggested fix: Stop relying on
StopIterationcrossing anawaitboundary:Minimal Reproduction Code:
How often has this issue occurred?: