SpanImpl.__exit__ (braintrust/logger.py:4919-4923) logs any non-None exc_type as a span error:
def __exit__(self, exc_type, exc_value, tb) -> None:
try:
if exc_type is not None:
self.log_internal(dict(error=stringify_exception(exc_type, exc_value, tb)))
...
This doesn't distinguish real exceptions from BaseException-only control-flow signals like GeneratorExit and asyncio.CancelledError, which represent normal cleanup, not failures.
This fires reliably when tracing google-adk (2.4.0) agents with sub_agents. ADK's node scheduler (google/adk/workflow/_llm_agent_wrapper.py:381-409) wraps every agent run in async with aclosing(run_method) as run_iter: and does:
if event.actions.transfer_to_agent:
...
transferred = True
break # closes run_iter via aclosing.__aexit__ -> throws GeneratorExit
The GeneratorExit propagates through the integration's own aclosing wrappers in braintrust/integrations/adk/tracing.py (_agent_run_async_wrapper, _flow_run_async_wrapper, _flow_call_llm_async_wrapper), each suspended at yield event inside a with start_span(...) block. That with block's __exit__ is SpanImpl.__exit__, which then logs the GeneratorExit as an error.
Impact: any orchestrator agent with sub_agents gets a false error logged on essentially every delegation step, since transfer_to_agent triggers this on every hand-off. A single-agent setup with no sub_agents never hits the transfer_to_agent branch, so the bug is easy to miss until you have a multi-agent workflow.
Suggested fix: in SpanImpl.__exit__, only treat it as an error when exc_type is a real Exception (or explicitly exclude GeneratorExit / asyncio.CancelledError):
if exc_type is not None and issubclass(exc_type, Exception):
self.log_internal(dict(error=stringify_exception(exc_type, exc_value, tb)))
Repro: an ADK LlmAgent with one or more sub_agents, instrumented via braintrust.wrappers.adk.setup_adk, run through InMemoryRunner. Every transfer_to_agent delegation logs a bogus error on the corresponding span.
Versions: braintrust==0.30.1, google-adk==2.4.0
SpanImpl.__exit__(braintrust/logger.py:4919-4923) logs any non-Noneexc_typeas a span error:This doesn't distinguish real exceptions from
BaseException-only control-flow signals likeGeneratorExitandasyncio.CancelledError, which represent normal cleanup, not failures.This fires reliably when tracing
google-adk(2.4.0) agents withsub_agents. ADK's node scheduler (google/adk/workflow/_llm_agent_wrapper.py:381-409) wraps every agent run inasync with aclosing(run_method) as run_iter:and does:The
GeneratorExitpropagates through the integration's ownaclosingwrappers inbraintrust/integrations/adk/tracing.py(_agent_run_async_wrapper,_flow_run_async_wrapper,_flow_call_llm_async_wrapper), each suspended atyield eventinside awith start_span(...)block. Thatwithblock's__exit__isSpanImpl.__exit__, which then logs theGeneratorExitas anerror.Impact: any orchestrator agent with
sub_agentsgets a false error logged on essentially every delegation step, sincetransfer_to_agenttriggers this on every hand-off. A single-agent setup with nosub_agentsnever hits thetransfer_to_agentbranch, so the bug is easy to miss until you have a multi-agent workflow.Suggested fix: in
SpanImpl.__exit__, only treat it as an error whenexc_typeis a realException(or explicitly excludeGeneratorExit/asyncio.CancelledError):Repro: an ADK
LlmAgentwith one or moresub_agents, instrumented viabraintrust.wrappers.adk.setup_adk, run throughInMemoryRunner. Everytransfer_to_agentdelegation logs a boguserroron the corresponding span.Versions:
braintrust==0.30.1,google-adk==2.4.0