SyncStreamWrapper / AsyncStreamWrapper are wrapt proxies that finalize telemetry from close() / __exit__ / __aexit__ and from iteration reaching its end. When the wrapped stream is a generator and the caller stops consuming it early, none of those run: the caller drops the proxy, and GC finalizes the underlying generator directly, so the span is never ended.
This is normal traffic for agent instrumentations that wrap generator-returning APIs. For example QwenPaw's ACP server breaks out of the async for when the client cancels a turn:
async for msg, _is_last in runner.query_handler(msgs, request=request):
if cancel_event.is_set():
break
Effects: the invoke_agent span is never exported and never recorded in gen_ai.client.operation.duration, and because the invocation attached its context at start and only detaches on finalize, later spans in the same task become children of the span that never ended.
Two related gaps:
AsyncStreamWrapper expects the stream to expose async close(), but async generators expose aclose(). Instrumentations have to override aclose / close / __aexit__ to bridge that.
- No proxy-based wrapper can catch GC teardown at all. Catching it requires the wrapper to itself be an async generator, so
GeneratorExit reaches a finally:
async def _driver():
try:
async for item in gen:
yield item
finally:
...finalize telemetry...
await gen.aclose()
This shape does finalize on the break path above, and it keeps the returned object an async_generator, which is closer to the SDK's original type than a proxy is.
Suggest a generator-aware wrapper in opentelemetry.util.genai.stream so instrumentations wrapping generator APIs (qwen-agent, the proposed qwenpaw package in #311) get correct finalization instead of each working around it.
SyncStreamWrapper/AsyncStreamWrapperare wrapt proxies that finalize telemetry fromclose()/__exit__/__aexit__and from iteration reaching its end. When the wrapped stream is a generator and the caller stops consuming it early, none of those run: the caller drops the proxy, and GC finalizes the underlying generator directly, so the span is never ended.This is normal traffic for agent instrumentations that wrap generator-returning APIs. For example QwenPaw's ACP server breaks out of the
async forwhen the client cancels a turn:Effects: the
invoke_agentspan is never exported and never recorded ingen_ai.client.operation.duration, and because the invocation attached its context at start and only detaches on finalize, later spans in the same task become children of the span that never ended.Two related gaps:
AsyncStreamWrapperexpects the stream to exposeasync close(), but async generators exposeaclose(). Instrumentations have to overrideaclose/close/__aexit__to bridge that.GeneratorExitreaches afinally:This shape does finalize on the
breakpath above, and it keeps the returned object anasync_generator, which is closer to the SDK's original type than a proxy is.Suggest a generator-aware wrapper in
opentelemetry.util.genai.streamso instrumentations wrapping generator APIs (qwen-agent, the proposed qwenpaw package in #311) get correct finalization instead of each working around it.