Skip to content

Crash while exiting loop agent in adk web #501

@ThomasSilloway

Description

@ThomasSilloway

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:

  1. Install the Google ADK (google-adk) v0.3
  2. Create a LoopAgent that runs a sub-agent (e.g., an LlmAgent as shown in the code below).
  3. Define a tool for the sub-agent that accepts tool_context: ToolContext as an argument (see process_backlog_file example below).
  4. Implement logic within the tool to set tool_context.actions.escalate = True under a specific condition (e.g., when a backlog file is empty).
  5. Run the LoopAgent (e.g., via adk web or programmatically) and trigger the condition in the sub-agent's tool that sets escalate = True.
  6. Observe the application logs for the ValueError: ... was created in a different Context traceback immediately following the GeneratorExit.

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 involving contextvars. 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}")

Metadata

Metadata

Assignees

Labels

coreIssues related to the core interface and implementation

Type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions