Skip to content

Sync stream_query always ends with 'RuntimeError: coroutine raised StopIteration' #6093

@jimmisimp

Description

@jimmisimp

🔴 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:

  1. Install google-adk==2.2.0.
  2. Deploy any ADK agent to Vertex AI Agent Engine (served via the ADK API server).
  3. Call the engine with class_method: "stream_query" (e.g. remote_agent.stream_query(...) from the vertexai SDK), which hits /api/stream_reasoning_engine.
  4. 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_responsefast_api.py:797 json_generatorfast_api.py:919 _aiter_from_iterstarlette/concurrency.py run_in_threadpoolanyio/_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.

Metadata

Metadata

Assignees

Labels

web[Component] This issue will be transferred to adk-web

Type

No fields configured for Bug.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions