Skip to content

[util-genai] Abandoned streams finalize without closing the wrapped generator #1035

Open

Description

@eternalcuriouslearner

Note

This issue was generated with AI assistance and requires investigation and confirmation prior to being worked on.

When a stream is abandoned, _StreamTelemetry.__del__ ends the invocation without closing the wrapped stream. For generator-backed streams, the generator's cleanup (finally / GeneratorExit handling) then runs after the span has ended and outside the invocation's context, or stays pending while another reference holds the generator. Spans started during that cleanup lose their parent.

Repro (SyncAgentStreamWrapper from #1033, but the behavior is inherited by every SyncStreamWrapper / AsyncStreamWrapper):

def agent_run():
    try:
        yield "a"; yield "b"
    finally:
        with tracer.start_as_current_span("cleanup-work"):
            pass

inv = handler.invoke_local_agent(agent_name="A")
gen = agent_run()
for _ in W(gen, inv):
    break
gc.collect()   # invoke_agent A ends (error.type=_OTHER)
del gen
gc.collect()   # cleanup runs now: agent span already ended, cleanup-work has no parent

Possible direction: close the wrapped stream under _execution_context() before finalizing in __del__. The sync case can call close(); the async case can't await from __del__, so it needs a different approach (e.g. scheduling aclose() on the running loop, or relying on the loop's async generator finalizer hook).

Related: #386, #491. Raised in review of #1033.

Activity

  1. sangkyoonnam commented on Nov 6, 2026

    @sangkyoonnam
    Contributor

    I can take the sync part if you aren't planning to handle it yourself, subject to maintainer agreement on the behavior change.

    The November 6 validation on cd79a039 used fake generators through a SyncStreamWrapper subclass, with suspended and attached invocations. With the regression tests applied, uv run --no-sync pytest util/opentelemetry-util-genai/tests/test_stream.py -q -k closes_wrapped_generator failed both cases in each of three runs (exit 1). With the local sync patch, both passed in each of three runs (exit 0). uv run --no-sync pytest util/opentelemetry-util-genai/tests/test_stream.py -q -k cleanup_span_is_child passed all four full-consumption and explicit-close controls in three runs before and three after (exit 0).

    These are prior validation results. They don't cover real agent instrumentation or retained external generator references. Async and tool-wrapper changes are outside that validation's scope. I'd limit my contribution to your proposed sync change for now. For async cleanup, do the maintainers prefer scheduling aclose() on a running loop or relying on its async generator finalizer hook?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions