Describe the bug
When a BaseAgent subclass wraps another agent and exits early from iterating over self.main_agent.run_async(ctx) (via return or break inside async for), multiple Failed to detach context errors are logged by OpenTelemetry. This happens because base_agent.run_async() wraps the generator yield in tracer.start_as_current_span(), and when GeneratorExit propagates through the generator chain, the OTel context tokens cannot be detached — they were created in a different contextvars.Context.
google-adk version: 1.27.2
Python version: 3.13
opentelemetry-api version: 1.33.0
This appears to be the same root cause as #423 and #1670 (both closed), but with a distinct and reproducible trigger path.
To Reproduce
Create a BaseAgent subclass that wraps another agent and conditionally exits early:
from google.adk.agents import BaseAgent, LlmAgent
from google.adk.events.event import Event
from typing import AsyncGenerator
from typing_extensions import override
class WrapperAgent(BaseAgent):
main_agent: BaseAgent
@override
async def _run_async_impl(self, ctx) -> AsyncGenerator[Event, None]:
async for event in self.main_agent.run_async(ctx):
if should_block(event):
yield create_redirect_event(ctx)
return # <-- triggers GeneratorExit on inner generator chain
yield event
When should_block() returns True and the wrapper does return inside the async for loop, the inner generator (main_agent.run_async(ctx)) is abandoned. Python sends GeneratorExit into the generator chain, which unwinds through base_agent.run_async():
base_agent.run_async()
→ tracer.start_as_current_span('invoke_agent ...') # attaches OTel context token
→ Aclosing(self._run_async_impl(ctx))
→ async for event in agen:
→ yield event # GeneratorExit thrown here
# __exit__ tries to detach token → ValueError
Error output
ERROR:opentelemetry.context:Failed to detach context
Traceback (most recent call last):
File ".../opentelemetry/trace/__init__.py", line 589, in use_span
yield span
File ".../opentelemetry/sdk/trace/__init__.py", line 1105, in start_as_current_span
yield span
File ".../google/adk/agents/base_agent.py", line 304, in run_async
yield event
GeneratorExit
During handling of the above exception, another exception occurred:
File ".../opentelemetry/context/__init__.py", line 155, in detach
_RUNTIME_CONTEXT.detach(token)
File ".../opentelemetry/context/contextvars_context.py", line 53, in detach
self._current_context.reset(token)
ValueError: <Token ...> was created in a different Context
This repeats for each nesting level in the agent/tracing hierarchy (4 errors for a typical pipeline).
Root cause analysis
base_agent.run_async() at line 294 uses a synchronous context manager (tracer.start_as_current_span()) wrapping an async generator's yield point:
with tracer.start_as_current_span(f'invoke_agent {self.name}') as span:
# ...
async with Aclosing(self._run_async_impl(ctx)) as agen:
async for event in agen:
yield event # <-- GeneratorExit enters here
The start_as_current_span context manager calls context.attach() on __enter__ (storing a contextvars.Token) and context.detach(token) on __exit__. When GeneratorExit is thrown into the generator, __exit__ fires, but the ContextVar.reset(token) fails because the token was created in a different contextvars.Context (the async context has shifted between generator suspension and cleanup).
Workaround
Application-level: Suppress the OTel context logger:
logging.getLogger("opentelemetry.context").setLevel(logging.CRITICAL)
Wrapper agent level: Use break instead of return inside the async for loop, and yield the redirect event after the loop exits. This ensures the inner generator's cleanup (including OTel span detach) completes before the wrapper yields:
@override
async def _run_async_impl(self, ctx) -> AsyncGenerator[Event, None]:
redirect_event = None
async for event in self.main_agent.run_async(ctx):
if should_block(event):
redirect_event = create_redirect_event(ctx)
break # inner generator closed here, in same async context
yield event
if redirect_event is not None:
yield redirect_event
Note: The break workaround may or may not resolve the issue depending on whether Python's async for cleanup after break preserves the contextvars.Context. In our testing, it reduces but may not fully eliminate the errors.
Suggested fix
Consider wrapping the tracing in base_agent.run_async() with GeneratorExit-safe cleanup:
async def run_async(self, parent_context):
with tracer.start_as_current_span(f'invoke_agent {self.name}') as span:
ctx = self._create_invocation_context(parent_context)
tracing.trace_agent_invocation(span, self, ctx)
try:
if event := await self._handle_before_agent_callback(ctx):
yield event
if ctx.end_invocation:
return
async with Aclosing(self._run_async_impl(ctx)) as agen:
async for event in agen:
yield event
if ctx.end_invocation:
return
if event := await self._handle_after_agent_callback(ctx):
yield event
except GeneratorExit:
# Suppress OTel context detach errors during generator cleanup
span.end()
return
Or use an async-generator-safe tracing wrapper that doesn't rely on contextvars.Token for cleanup.
Related issues
Describe the bug
When a
BaseAgentsubclass wraps another agent and exits early from iterating overself.main_agent.run_async(ctx)(viareturnorbreakinsideasync for), multipleFailed to detach contexterrors are logged by OpenTelemetry. This happens becausebase_agent.run_async()wraps the generator yield intracer.start_as_current_span(), and whenGeneratorExitpropagates through the generator chain, the OTel context tokens cannot be detached — they were created in a differentcontextvars.Context.google-adk version: 1.27.2
Python version: 3.13
opentelemetry-api version: 1.33.0
This appears to be the same root cause as #423 and #1670 (both closed), but with a distinct and reproducible trigger path.
To Reproduce
Create a
BaseAgentsubclass that wraps another agent and conditionally exits early:When
should_block()returnsTrueand the wrapper doesreturninside theasync forloop, the inner generator (main_agent.run_async(ctx)) is abandoned. Python sendsGeneratorExitinto the generator chain, which unwinds throughbase_agent.run_async():Error output
This repeats for each nesting level in the agent/tracing hierarchy (4 errors for a typical pipeline).
Root cause analysis
base_agent.run_async()at line 294 uses a synchronous context manager (tracer.start_as_current_span()) wrapping an async generator'syieldpoint:The
start_as_current_spancontext manager callscontext.attach()on__enter__(storing acontextvars.Token) andcontext.detach(token)on__exit__. WhenGeneratorExitis thrown into the generator,__exit__fires, but theContextVar.reset(token)fails because the token was created in a differentcontextvars.Context(the async context has shifted between generator suspension and cleanup).Workaround
Application-level: Suppress the OTel context logger:
Wrapper agent level: Use
breakinstead ofreturninside theasync forloop, and yield the redirect event after the loop exits. This ensures the inner generator's cleanup (including OTel span detach) completes before the wrapper yields:Note: The
breakworkaround may or may not resolve the issue depending on whether Python'sasync forcleanup afterbreakpreserves thecontextvars.Context. In our testing, it reduces but may not fully eliminate the errors.Suggested fix
Consider wrapping the tracing in
base_agent.run_async()withGeneratorExit-safe cleanup:Or use an async-generator-safe tracing wrapper that doesn't rely on
contextvars.Tokenfor cleanup.Related issues