Description
Describe the bug
When a sub-agent within a google.adk.agents.LoopAgent
signals termination using tool_context.actions.escalate = True
, the loop terminates, but an opentelemetry.context
error (ValueError: <Token ...> was created in a different Context
) occurs immediately after the sub-agent's GeneratorExit
. This appears to be an issue with OpenTelemetry context handling during the abrupt termination of the agent's async generator, triggered by the ADK escalation mechanism.
To Reproduce
Steps to reproduce the behavior:
- Install the Google ADK (
google-adk
) v0.3 - Create a
LoopAgent
that runs a sub-agent (e.g., anLlmAgent
as shown in the code below). - Define a tool for the sub-agent that accepts
tool_context: ToolContext
as an argument (seeprocess_backlog_file
example below). - Implement logic within the tool to set
tool_context.actions.escalate = True
under a specific condition (e.g., when a backlog file is empty). - Run the
LoopAgent
(e.g., viaadk web
or programmatically) and trigger the condition in the sub-agent's tool that setsescalate = True
. - Observe the application logs for the
ValueError: ... was created in a different Context
traceback immediately following theGeneratorExit
.
Expected behavior
The LoopAgent
should terminate cleanly when escalate
is set to True
by the sub-agent's tool, without any subsequent OpenTelemetry context detachment errors in the traceback.
Screenshots
(Not applicable, traceback provided below)
Desktop (please complete the following information):
- OS: Windows
- Python version(python -V): Python 3.13.2
- ADK version(pip show google-adk): 0.3.0
Additional context
- This issue appears related to known context propagation challenges within
opentelemetry-python
when dealing with async generators and abrupt exits, particularly involvingcontextvars
. See related discussion: open-telemetry/opentelemetry-python#2606 - The core ADK escalation mechanism (
tool_context.actions.escalate = True
) seems to function correctly in triggering the loop termination, but the subsequent cleanup phase within the ADK's flow hits this OpenTelemetry context issue.
Traceback:
2025-05-01 18:10:32,719 - INFO - fast_api.py:634 - Generated event in agent run streaming: {"content":{"parts":[{"functionResponse":{"id":"adk-92371e11-2c3e-4da0-b4a4-d301ae5caba3","name":"process_backlog_file","response":{"status":"empty","message":"Backlog is empty or not found."}}}],"role":"user"},"invocation_id":"e-0dae2740-4239-4fe5-9ec9-e740fc59c411","author":"BacklogReaderAgent_PoC","actions":{"state_delta":{},"artifact_delta":{},"escalate":true,"requested_auth_configs":{}},"id":"LNVzw0lc","timestamp":1746137432.719034}
2025-05-01 18:10:32,719 - ERROR - init.py:157 - Failed to detach context
Traceback (most recent call last):
File "C:\path\to\project\src\venv\Lib\site-packages\opentelemetry\trace_init_.py", line 587, in use_span
yield span
File "C:\path\to\project\src\venv\Lib\site-packages\opentelemetry\sdk\trace_init_.py", line 1105, in start_as_current_span
yield span
File "C:\path\to\project\src\venv\Lib\site-packages\opentelemetry\trace_init_.py", line 452, in start_as_current_span
yield span
File "C:\path\to\project\src\venv\Lib\site-packages\google\adk\agents\base_agent.py", line 142, in run_async
yield event
GeneratorExit
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "C:\path\to\project\src\venv\Lib\site-packages\opentelemetry\context_init_.py", line 155, in detach
RUNTIME_CONTEXT.detach(token)
~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^
File "C:\path\to\project\src\venv\Lib\site-packages\opentelemetry\context\contextvars_context.py", line 53, in detach
self.current_context.reset(token)
~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^
ValueError: <Token var=<ContextVar name='current_context' default={} at 0x000001F4CAA97A10> at 0x000001F4D72A7FC0> was created in a different Context
2025-05-01 18:10:32,722 - ERROR - init.py:157 - Failed to detach context
Traceback (most recent call last):
File "C:\path\to\project\src\venv\Lib\site-packages\opentelemetry\trace_init.py", line 587, in use_span
yield span
File "C:\path\to\project\src\venv\Lib\site-packages\opentelemetry\sdk\trace_init.py", line 1105, in start_as_current_span
yield span
File "C:\path\to\project\src\venv\Lib\site-packages\opentelemetry\trace_init_.py", line 452, in start_as_current_span
yield span
File "C:\path\to\project\src\venv\Lib\site-packages\google\adk\flows\llm_flows\base_llm_flow.py", line 487, in _call_llm_async
yield llm_response
GeneratorExit
During handling of the above exception, another exception occurred:
Traceback (most recent call last):
File "C:\path\to\project\src\venv\Lib\site-packages\opentelemetry\context_init_.py", line 155, in detach
_RUNTIME_CONTEXT.detach(token)
~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^
File "C:\path\to\project\src\venv\Lib\site-packages\opentelemetry\context\contextvars_context.py", line 53, in detach
self._current_context.reset(token)
~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^^^
ValueError: <Token var=<ContextVar name='current_context' default={} at 0x000001F4CAA97A10> at 0x000001F4D732CDC0> was created in a different Context
Relevant Code Snippets (Illustrative):
Loop Agent Definition (sleepy_dev_poc/agent.py
):
# Defines the root LoopAgent for the PoC
import logging
from google.adk.agents import LoopAgent
from .sub_agents.backlog_reader.agent import backlog_reader_agent
from .shared_libraries import constants
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger(__name__)
root_agent = LoopAgent(
name=constants.ROOT_AGENT_NAME,
description="Root agent that loops through the BacklogReaderAgent until the backlog is empty (escalation).",
sub_agents=[backlog_reader_agent]
)
logger.info(f"Initialized {constants.ROOT_AGENT_NAME} with sub-agent: {backlog_reader_agent.name}")
Sub-Agent Tool (sleepy_dev_poc/sub_agents/backlog_reader/tools.py):
# Tool for processing the backlog file
import logging
import os
from typing import Dict, Any, Optional
from google.adk.tools import ToolContext
from ...shared_libraries import constants
# ... (logging setup) ...
def process_backlog_file(tool_context: Optional[ToolContext] = None) -> Dict[str, Any]:
# ... (get file path: abs_file_path) ...
if tool_context is None:
# ... (error handling) ...
return {"status": "error", "message": "Critical Error: ToolContext is missing."}
try:
if not os.path.exists(abs_file_path) or os.path.getsize(abs_file_path) == 0:
logger.info(f"{constants.BACKLOG_READER_AGENT_NAME} - Tool: Backlog file is empty... Signaling escalation.")
tool_context.actions.escalate = True # <<<--- ESCALATION SIGNAL
return {"status": "empty", "message": "Backlog is empty or not found."}
# ... (read/write file logic) ...
tool_context.actions.escalate = False
return {"status": "ok", "task_description": first_line, "message": "Task processed successfully."}
except Exception as e:
# ... (error handling) ...
if tool_context:
tool_context.actions.escalate = True # Escalate on error too
return {"status": "error", "message": f"An error occurred: {str(e)}"}
Sub-Agent Definition (sleepy_dev_poc/sub_agents/backlog_reader/agent.py):
# Defines the BacklogReaderAgent
import logging
from google.adk.agents import LlmAgent
from google.adk.tools import FunctionTool
from ...shared_libraries import constants
from . import tools
from .prompt import BACKLOG_READER_AGENT_PROMPT
# ... (logging setup) ...
process_backlog_tool = FunctionTool(func=tools.process_backlog_file)
backlog_reader_agent = LlmAgent(
name=constants.BACKLOG_READER_AGENT_NAME,
model=constants.MODEL_NAME,
instruction=BACKLOG_READER_AGENT_PROMPT,
tools=[process_backlog_tool],
description="Reads tasks one by one from the backlog file using a tool and signals when empty.",
)
logger.info(f"Initialized {constants.BACKLOG_READER_AGENT_NAME} with model {constants.MODEL_NAME}")