From 4179470ac5923b57112b835e8321bc6e1020e749 Mon Sep 17 00:00:00 2001 From: sarojrout Date: Tue, 6 Jan 2026 14:25:04 -0800 Subject: [PATCH 1/5] feat(samples): Add agent_tool_resilience sample Demonstrates timeout protection, automatic retry, and dynamic fallback patterns for multi-agent workflows using AgentTool. --- .../samples/agent_tool_resilience/README.md | 37 ++ .../samples/agent_tool_resilience/__init__.py | 18 + .../samples/agent_tool_resilience/agent.py | 325 ++++++++++++++++++ 3 files changed, 380 insertions(+) create mode 100644 contributing/samples/agent_tool_resilience/README.md create mode 100644 contributing/samples/agent_tool_resilience/__init__.py create mode 100644 contributing/samples/agent_tool_resilience/agent.py diff --git a/contributing/samples/agent_tool_resilience/README.md b/contributing/samples/agent_tool_resilience/README.md new file mode 100644 index 0000000000..40f0b2c4ad --- /dev/null +++ b/contributing/samples/agent_tool_resilience/README.md @@ -0,0 +1,37 @@ +# AgentTool Resilience: Timeout, Retry, and Redirect Patterns + +This sample demonstrates how to handle failures, timeouts, and partial results from downstream agents in multi-agent workflows using ADK. + +## Running the Demo + +```bash +adk web contributing/samples/agent_tool_resilience +``` + +Then in the web UI, select `agent_tool_resilience` from the dropdown and try: +1. Simple query: "What is quantum computing?" +2. Complex query: (very detailed research request) +3. Timeout scenario: Set timeout to 5 seconds in `agent.py` and use a complex query + +## Features Demonstrated + +- **Timeout Protection**: Custom `TimeoutAgentTool` wrapper adds timeout handling to sub-agents +- **Automatic Retry**: `ReflectAndRetryToolPlugin` handles retries with structured guidance +- **Dynamic Fallback**: Coordinator agent routes to alternative agents when primary fails +- **Error Recovery**: Specialized agent provides user-friendly error analysis + +## Expected Behavior + +1. **Normal Operation**: Primary agent handles the query successfully +2. **Timeout Scenario**: Primary times out → Fallback agent is automatically tried +3. **Failure Scenario**: Primary fails → Retry → Fallback → Error recovery agent provides guidance + +## Architecture + +The sample includes: +- `coordinator_agent` - Routes requests and handles errors +- `research_agent_primary` - Primary agent with timeout protection (5s) +- `research_agent_fallback` - Fallback agent with longer timeout (60s) +- `error_recovery_agent` - Analyzes failures and provides recommendations + +For detailed documentation, see `README_EXTENSIVE.md`. diff --git a/contributing/samples/agent_tool_resilience/__init__.py b/contributing/samples/agent_tool_resilience/__init__.py new file mode 100644 index 0000000000..4304300668 --- /dev/null +++ b/contributing/samples/agent_tool_resilience/__init__.py @@ -0,0 +1,18 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from . import agent + +__all__ = ['agent'] + diff --git a/contributing/samples/agent_tool_resilience/agent.py b/contributing/samples/agent_tool_resilience/agent.py new file mode 100644 index 0000000000..6831d34011 --- /dev/null +++ b/contributing/samples/agent_tool_resilience/agent.py @@ -0,0 +1,325 @@ +# Copyright 2025 Google LLC +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Sample demonstrating AgentTool resilience: timeout, retry, and redirect patterns. + +This sample shows how to handle failures, timeouts, and partial results +from downstream agents in multi-agent workflows, including: +- Timeout protection for sub-agents +- Automatic retry with ReflectAndRetryToolPlugin +- Dynamic rerouting to alternative agents +- Error handling without leaking complexity to users +""" + +import asyncio +from typing import Any + +from google.adk import Agent +from google.adk.apps import App +from google.adk.plugins import ReflectAndRetryToolPlugin +from google.adk.tools import AgentTool +from google.adk.tools.google_search_tool import google_search +from google.adk.tools.tool_context import ToolContext + +# Uncomment to use Ollama (free, local) instead of Gemini: +# from google.adk.models.lite_llm import LiteLlm +# OLLAMA_MODEL = LiteLlm(model="ollama_chat/mistral-small3.1") +# Then replace model='gemini-2.5-pro' with model=OLLAMA_MODEL + + +# ============================================================================ +# Custom TimeoutAgentTool Wrapper +# ============================================================================ + +class TimeoutAgentTool(AgentTool): + """AgentTool with timeout protection. + + This wrapper adds timeout handling to AgentTool, catching TimeoutError + and returning a structured error response that ReflectAndRetryToolPlugin + can process. + """ + + def __init__( + self, + agent, + timeout: float = 30.0, + timeout_error_message: str = "Sub-agent execution timed out", + **kwargs + ): + """Initialize TimeoutAgentTool. + + Args: + agent: The agent to wrap. + timeout: Timeout in seconds for sub-agent execution. + timeout_error_message: Custom error message for timeout. + **kwargs: Additional arguments passed to AgentTool. + """ + super().__init__(agent, **kwargs) + self.timeout = timeout + self.timeout_error_message = timeout_error_message + + async def run_async( + self, + *, + args: dict[str, Any], + tool_context: ToolContext, + ) -> Any: + """Run with timeout protection.""" + try: + return await asyncio.wait_for( + super().run_async(args=args, tool_context=tool_context), + timeout=self.timeout + ) + except asyncio.TimeoutError: + # Return structured error that ReflectAndRetryToolPlugin can handle + return { + "error": "TimeoutError", + "message": self.timeout_error_message, + "timeout_seconds": self.timeout, + "agent_name": self.agent.name, + } + + async def run_async_with_events( + self, + *, + args: dict[str, Any], + tool_context: ToolContext, + ) -> Any: + """Run with timeout protection and event streaming. + + Note: Timeout for async generators requires careful handling. + This implementation uses a task-based approach with timeout monitoring. + """ + import time + from google.genai import types + from google.adk.events.event import Event + + start_time = time.time() + agen = super().run_async_with_events( + args=args, tool_context=tool_context + ) + + try: + while True: + # Check overall timeout + elapsed = time.time() - start_time + if elapsed >= self.timeout: + # Timeout exceeded + yield Event( + content=types.Content( + role='assistant', + parts=[ + types.Part.from_text( + text=f"Timeout: {self.timeout_error_message}" + ) + ], + ), + ) + return + + # Calculate remaining time + remaining = self.timeout - elapsed + if remaining <= 0: + yield Event( + content=types.Content( + role='assistant', + parts=[ + types.Part.from_text( + text=f"Timeout: {self.timeout_error_message}" + ) + ], + ), + ) + return + + # Get next event with timeout check + try: + event = await asyncio.wait_for( + agen.__anext__(), + timeout=min(remaining, 0.5) # Check frequently + ) + yield event + except StopAsyncIteration: + # Generator finished normally + break + except asyncio.TimeoutError: + # This iteration timed out, but check overall timeout + if time.time() - start_time >= self.timeout: + yield Event( + content=types.Content( + role='assistant', + parts=[ + types.Part.from_text( + text=f"Timeout: {self.timeout_error_message}" + ) + ], + ), + ) + return + # Otherwise, continue waiting for next event + continue + except Exception: + # Re-raise other exceptions + raise + + +# ============================================================================ +# Sub-Agents with Different Characteristics +# ============================================================================ + +# Primary agent - may be slow or fail +research_agent_primary = Agent( + name='research_agent_primary', + model='gemini-2.5-flash', + description='Primary research agent for complex queries (may be slow)', + instruction=""" + You are a thorough research assistant. When given a research task: + 1. Acknowledge the task + 2. ALWAYS use the google_search tool to find current information + 3. Break down the information into detailed steps + 4. Provide a comprehensive summary based on the search results + + IMPORTANT: You MUST use google_search for every research query. Do not + respond without searching first. Be thorough and detailed in your responses. + """, + tools=[google_search], +) + +# Fallback agent - faster, simpler +research_agent_fallback = Agent( + name='research_agent_fallback', + model='gemini-2.5-flash', + description='Fallback research agent for simpler queries or when primary fails', + instruction=""" + You are a research assistant focused on quick, concise answers. + When given a research task: + 1. ALWAYS use the google_search tool first to find information + 2. Provide a direct, well-structured response based on the search results + 3. Keep your response concise without excessive detail + + IMPORTANT: You MUST use google_search for every research query. Do not + respond without searching first. + """, + tools=[google_search], +) + +# Specialized agent for error recovery +error_recovery_agent = Agent( + name='error_recovery_agent', + model='gemini-2.5-flash', + description='Agent that handles error scenarios and provides alternative approaches', + instruction=""" + You are an error recovery specialist. When you receive an error message + or failure report, analyze what went wrong and suggest: + 1. What the error means + 2. Why it might have occurred + 3. Alternative approaches to achieve the goal + 4. Recommendations for the user + + Be helpful and constructive in your analysis. + """, +) + + +# ============================================================================ +# Coordinator Agent with Resilience Patterns +# ============================================================================ + +coordinator_agent = Agent( + name='coordinator_agent', + model='gemini-2.5-flash', + description='Coordinator that manages research tasks with resilience', + instruction=""" + You are a coordinator agent that manages research tasks by delegating to + specialized sub-agents. Your role is to ensure tasks complete successfully + even when individual agents fail or timeout. + + **Tool Selection Strategy:** + 1. **Primary Tool (research_agent_primary)**: Use for complex, detailed + research tasks. This agent is thorough but may be slower. + 2. **Fallback Tool (research_agent_fallback)**: Use when: + - The primary agent times out or fails + - The query is simple and doesn't need deep research + - You need a quick answer + 3. **Error Recovery Tool (error_recovery_agent)**: Use when: + - Multiple attempts have failed + - You need to understand what went wrong + - You need alternative approaches suggested + + **Error Handling Protocol:** + - If research_agent_primary returns an error or timeout: + 1. First, try research_agent_fallback with the same query + 2. If that also fails, use error_recovery_agent to analyze the failure + 3. Present the error_recovery_agent's analysis to the user + 4. Suggest next steps based on the analysis + + **User Communication:** + - Always present results clearly, even if they come from fallback agents + - If errors occur, explain what happened and what you tried + - Never expose internal error details or retry counts to users + - Frame fallbacks as "using a different approach" rather than "fallback" + + **Example Flow:** + User: "Research quantum computing applications" + 1. Try research_agent_primary + 2. If timeout/error → Try research_agent_fallback + 3. If still fails → Use error_recovery_agent to understand why + 4. Present final result or error analysis to user + """, + tools=[ + # Primary agent with timeout protection + # For testing timeouts, set a very short timeout (e.g., 5.0 seconds) + # For production, use a longer timeout (e.g., 30.0 seconds) + TimeoutAgentTool( + agent=research_agent_primary, + timeout=5.0, # Change to 5.0 for timeout testing + timeout_error_message="Primary research agent timed out after 30 seconds", + skip_summarization=True, + ), + # Fallback agent timeout + # For testing: Set to 5.0 to test full failure chain (primary → fallback → error recovery) + # For production: Set to 60.0 to allow fallback to succeed after primary timeout + TimeoutAgentTool( + agent=research_agent_fallback, + timeout=5.0, # Set to 60.0 to test successful fallback after primary timeout + timeout_error_message="Fallback research agent timed out", + skip_summarization=True, + ), + # Error recovery agent + AgentTool( + agent=error_recovery_agent, + skip_summarization=True, + ), + ], +) + +# ============================================================================ +# App Configuration with Retry Plugin +# ============================================================================ + +# Configure retry plugin for automatic retry handling +retry_plugin = ReflectAndRetryToolPlugin( + max_retries=2, # Allow 2 retries per tool before giving up + throw_exception_if_retry_exceeded=False, # Return guidance instead of raising + tracking_scope=None, # Use default (per-invocation) +) + +app = App( + name='agent_tool_resilience', + root_agent=coordinator_agent, + plugins=[retry_plugin], +) + +root_agent = coordinator_agent + From e6a2b07aeb578f0f40ae15a5aab13bfb014ae2e2 Mon Sep 17 00:00:00 2001 From: sarojrout Date: Tue, 6 Jan 2026 14:30:58 -0800 Subject: [PATCH 2/5] cleaned up comments --- contributing/samples/agent_tool_resilience/agent.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/contributing/samples/agent_tool_resilience/agent.py b/contributing/samples/agent_tool_resilience/agent.py index 6831d34011..95c0492185 100644 --- a/contributing/samples/agent_tool_resilience/agent.py +++ b/contributing/samples/agent_tool_resilience/agent.py @@ -32,11 +32,6 @@ from google.adk.tools.google_search_tool import google_search from google.adk.tools.tool_context import ToolContext -# Uncomment to use Ollama (free, local) instead of Gemini: -# from google.adk.models.lite_llm import LiteLlm -# OLLAMA_MODEL = LiteLlm(model="ollama_chat/mistral-small3.1") -# Then replace model='gemini-2.5-pro' with model=OLLAMA_MODEL - # ============================================================================ # Custom TimeoutAgentTool Wrapper From 70f1fa13cdee887e51fa5a2baa37b2a653142300 Mon Sep 17 00:00:00 2001 From: sarojrout Date: Tue, 6 Jan 2026 22:42:59 -0800 Subject: [PATCH 3/5] review comments incorporated #4086 --- contributing/samples/agent_tool_resilience/README.md | 2 -- contributing/samples/agent_tool_resilience/agent.py | 12 ++++++------ 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/contributing/samples/agent_tool_resilience/README.md b/contributing/samples/agent_tool_resilience/README.md index 40f0b2c4ad..8d36528f72 100644 --- a/contributing/samples/agent_tool_resilience/README.md +++ b/contributing/samples/agent_tool_resilience/README.md @@ -33,5 +33,3 @@ The sample includes: - `research_agent_primary` - Primary agent with timeout protection (5s) - `research_agent_fallback` - Fallback agent with longer timeout (60s) - `error_recovery_agent` - Analyzes failures and provides recommendations - -For detailed documentation, see `README_EXTENSIVE.md`. diff --git a/contributing/samples/agent_tool_resilience/agent.py b/contributing/samples/agent_tool_resilience/agent.py index 95c0492185..1ac4f447e7 100644 --- a/contributing/samples/agent_tool_resilience/agent.py +++ b/contributing/samples/agent_tool_resilience/agent.py @@ -31,6 +31,9 @@ from google.adk.tools import AgentTool from google.adk.tools.google_search_tool import google_search from google.adk.tools.tool_context import ToolContext +import time +from google.genai import types +from google.adk.events.event import Event # ============================================================================ @@ -96,9 +99,6 @@ async def run_async_with_events( Note: Timeout for async generators requires careful handling. This implementation uses a task-based approach with timeout monitoring. """ - import time - from google.genai import types - from google.adk.events.event import Event start_time = time.time() agen = super().run_async_with_events( @@ -278,8 +278,8 @@ async def run_async_with_events( # For production, use a longer timeout (e.g., 30.0 seconds) TimeoutAgentTool( agent=research_agent_primary, - timeout=5.0, # Change to 5.0 for timeout testing - timeout_error_message="Primary research agent timed out after 30 seconds", + timeout=10.0, # Change to 5.0 for timeout testing + timeout_error_message="Primary research agent timed out after 10 seconds", skip_summarization=True, ), # Fallback agent timeout @@ -287,7 +287,7 @@ async def run_async_with_events( # For production: Set to 60.0 to allow fallback to succeed after primary timeout TimeoutAgentTool( agent=research_agent_fallback, - timeout=5.0, # Set to 60.0 to test successful fallback after primary timeout + timeout=60.0, # Set to 60.0 to test successful fallback after primary timeout timeout_error_message="Fallback research agent timed out", skip_summarization=True, ), From b2143805618170d015e0442d5c869a47687392e1 Mon Sep 17 00:00:00 2001 From: sarojrout Date: Wed, 7 Jan 2026 17:32:58 -0800 Subject: [PATCH 4/5] made skip summarization false to prevent the function response event from being marked as final --- .../samples/agent_tool_resilience/README.md | 6 +- .../samples/agent_tool_resilience/agent.py | 115 ++++-------------- 2 files changed, 29 insertions(+), 92 deletions(-) diff --git a/contributing/samples/agent_tool_resilience/README.md b/contributing/samples/agent_tool_resilience/README.md index 8d36528f72..49e1ecbaac 100644 --- a/contributing/samples/agent_tool_resilience/README.md +++ b/contributing/samples/agent_tool_resilience/README.md @@ -10,7 +10,11 @@ adk web contributing/samples/agent_tool_resilience Then in the web UI, select `agent_tool_resilience` from the dropdown and try: 1. Simple query: "What is quantum computing?" -2. Complex query: (very detailed research request) +2. Complex query: (very detailed research request) as "Research quantum computing applications in healthcare, finance, cryptography, + logistics, weather prediction, drug discovery, and machine learning. + For each domain, provide: historical context, current state-of-the-art, + technical challenges, recent breakthroughs in 2024, comparison with classical + approaches, economic impact, and future roadmap for the next 10 years." 3. Timeout scenario: Set timeout to 5 seconds in `agent.py` and use a complex query ## Features Demonstrated diff --git a/contributing/samples/agent_tool_resilience/agent.py b/contributing/samples/agent_tool_resilience/agent.py index 1ac4f447e7..d7ac4ce215 100644 --- a/contributing/samples/agent_tool_resilience/agent.py +++ b/contributing/samples/agent_tool_resilience/agent.py @@ -88,85 +88,6 @@ async def run_async( "agent_name": self.agent.name, } - async def run_async_with_events( - self, - *, - args: dict[str, Any], - tool_context: ToolContext, - ) -> Any: - """Run with timeout protection and event streaming. - - Note: Timeout for async generators requires careful handling. - This implementation uses a task-based approach with timeout monitoring. - """ - - start_time = time.time() - agen = super().run_async_with_events( - args=args, tool_context=tool_context - ) - - try: - while True: - # Check overall timeout - elapsed = time.time() - start_time - if elapsed >= self.timeout: - # Timeout exceeded - yield Event( - content=types.Content( - role='assistant', - parts=[ - types.Part.from_text( - text=f"Timeout: {self.timeout_error_message}" - ) - ], - ), - ) - return - - # Calculate remaining time - remaining = self.timeout - elapsed - if remaining <= 0: - yield Event( - content=types.Content( - role='assistant', - parts=[ - types.Part.from_text( - text=f"Timeout: {self.timeout_error_message}" - ) - ], - ), - ) - return - - # Get next event with timeout check - try: - event = await asyncio.wait_for( - agen.__anext__(), - timeout=min(remaining, 0.5) # Check frequently - ) - yield event - except StopAsyncIteration: - # Generator finished normally - break - except asyncio.TimeoutError: - # This iteration timed out, but check overall timeout - if time.time() - start_time >= self.timeout: - yield Event( - content=types.Content( - role='assistant', - parts=[ - types.Part.from_text( - text=f"Timeout: {self.timeout_error_message}" - ) - ], - ), - ) - return - # Otherwise, continue waiting for next event - continue - except Exception: - # Re-raise other exceptions - raise # ============================================================================ @@ -233,13 +154,16 @@ async def run_async_with_events( coordinator_agent = Agent( name='coordinator_agent', - model='gemini-2.5-flash', + model='gemini-2.5-flash-lite', description='Coordinator that manages research tasks with resilience', instruction=""" You are a coordinator agent that manages research tasks by delegating to specialized sub-agents. Your role is to ensure tasks complete successfully even when individual agents fail or timeout. + **CRITICAL WORKFLOW REQUIREMENT - READ THIS FIRST:** + After calling ANY tool and receiving its response, you MUST IMMEDIATELY generate a text response explaining the results to the user. Tool calls are NOT the final answer - they are just one step. You MUST always provide a final text response to complete the conversation. If you only call a tool without generating text afterward, the user will receive no response. + **Tool Selection Strategy:** 1. **Primary Tool (research_agent_primary)**: Use for complex, detailed research tasks. This agent is thorough but may be slower. @@ -261,26 +185,36 @@ async def run_async_with_events( **User Communication:** - Always present results clearly, even if they come from fallback agents + - After receiving any tool response, immediately provide a helpful text explanation to the user - If errors occur, explain what happened and what you tried - Never expose internal error details or retry counts to users - Frame fallbacks as "using a different approach" rather than "fallback" **Example Flow:** - User: "Research quantum computing applications" - 1. Try research_agent_primary - 2. If timeout/error → Try research_agent_fallback - 3. If still fails → Use error_recovery_agent to understand why - 4. Present final result or error analysis to user + User: "What is quantum computing?" + 1. Call research_agent_primary with request="What is quantum computing?" + 2. Wait for the tool response + 3. **MUST**: Generate a text response presenting the results to the user + + If the primary agent times out or fails: + 4. Call research_agent_fallback with the same request + 5. Wait for the tool response + 6. **MUST**: Generate a text response presenting the results to the user + + Remember: Tool calls are not the final answer - you must always follow up with a text response explaining the results to the user. """, tools=[ # Primary agent with timeout protection # For testing timeouts, set a very short timeout (e.g., 5.0 seconds) # For production, use a longer timeout (e.g., 30.0 seconds) + # NOTE: skip_summarization=False is required for the coordinator to continue + # after tool calls. If True, the function response event is marked as final + # and the LLM flow stops, preventing the coordinator from generating a response. TimeoutAgentTool( agent=research_agent_primary, - timeout=10.0, # Change to 5.0 for timeout testing - timeout_error_message="Primary research agent timed out after 10 seconds", - skip_summarization=True, + timeout=30.0, # Change to 5.0 for timeout testing + timeout_error_message="Primary research agent timed out after 30 seconds", + skip_summarization=False, # Must be False for coordinator to continue ), # Fallback agent timeout # For testing: Set to 5.0 to test full failure chain (primary → fallback → error recovery) @@ -289,12 +223,12 @@ async def run_async_with_events( agent=research_agent_fallback, timeout=60.0, # Set to 60.0 to test successful fallback after primary timeout timeout_error_message="Fallback research agent timed out", - skip_summarization=True, + skip_summarization=False, # Must be False for coordinator to continue ), # Error recovery agent AgentTool( agent=error_recovery_agent, - skip_summarization=True, + skip_summarization=False, # Must be False for coordinator to continue ), ], ) @@ -307,7 +241,6 @@ async def run_async_with_events( retry_plugin = ReflectAndRetryToolPlugin( max_retries=2, # Allow 2 retries per tool before giving up throw_exception_if_retry_exceeded=False, # Return guidance instead of raising - tracking_scope=None, # Use default (per-invocation) ) app = App( From b8af42a44309d2c51e3fbdacf53af6446310cbbe Mon Sep 17 00:00:00 2001 From: sarojrout Date: Thu, 8 Jan 2026 22:10:34 -0800 Subject: [PATCH 5/5] fixed linting and removed unused imports #4086 --- .../samples/agent_tool_resilience/__init__.py | 1 - .../samples/agent_tool_resilience/agent.py | 35 ++++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/contributing/samples/agent_tool_resilience/__init__.py b/contributing/samples/agent_tool_resilience/__init__.py index 4304300668..3d21a562d3 100644 --- a/contributing/samples/agent_tool_resilience/__init__.py +++ b/contributing/samples/agent_tool_resilience/__init__.py @@ -15,4 +15,3 @@ from . import agent __all__ = ['agent'] - diff --git a/contributing/samples/agent_tool_resilience/agent.py b/contributing/samples/agent_tool_resilience/agent.py index d7ac4ce215..e6c68b586b 100644 --- a/contributing/samples/agent_tool_resilience/agent.py +++ b/contributing/samples/agent_tool_resilience/agent.py @@ -31,15 +31,12 @@ from google.adk.tools import AgentTool from google.adk.tools.google_search_tool import google_search from google.adk.tools.tool_context import ToolContext -import time -from google.genai import types -from google.adk.events.event import Event - # ============================================================================ # Custom TimeoutAgentTool Wrapper # ============================================================================ + class TimeoutAgentTool(AgentTool): """AgentTool with timeout protection. @@ -52,8 +49,8 @@ def __init__( self, agent, timeout: float = 30.0, - timeout_error_message: str = "Sub-agent execution timed out", - **kwargs + timeout_error_message: str = 'Sub-agent execution timed out', + **kwargs, ): """Initialize TimeoutAgentTool. @@ -77,19 +74,18 @@ async def run_async( try: return await asyncio.wait_for( super().run_async(args=args, tool_context=tool_context), - timeout=self.timeout + timeout=self.timeout, ) except asyncio.TimeoutError: # Return structured error that ReflectAndRetryToolPlugin can handle return { - "error": "TimeoutError", - "message": self.timeout_error_message, - "timeout_seconds": self.timeout, - "agent_name": self.agent.name, + 'error': 'TimeoutError', + 'message': self.timeout_error_message, + 'timeout_seconds': self.timeout, + 'agent_name': self.agent.name, } - # ============================================================================ # Sub-Agents with Different Characteristics # ============================================================================ @@ -116,7 +112,9 @@ async def run_async( research_agent_fallback = Agent( name='research_agent_fallback', model='gemini-2.5-flash', - description='Fallback research agent for simpler queries or when primary fails', + description=( + 'Fallback research agent for simpler queries or when primary fails' + ), instruction=""" You are a research assistant focused on quick, concise answers. When given a research task: @@ -134,7 +132,9 @@ async def run_async( error_recovery_agent = Agent( name='error_recovery_agent', model='gemini-2.5-flash', - description='Agent that handles error scenarios and provides alternative approaches', + description=( + 'Agent that handles error scenarios and provides alternative approaches' + ), instruction=""" You are an error recovery specialist. When you receive an error message or failure report, analyze what went wrong and suggest: @@ -213,7 +213,9 @@ async def run_async( TimeoutAgentTool( agent=research_agent_primary, timeout=30.0, # Change to 5.0 for timeout testing - timeout_error_message="Primary research agent timed out after 30 seconds", + timeout_error_message=( + 'Primary research agent timed out after 30 seconds' + ), skip_summarization=False, # Must be False for coordinator to continue ), # Fallback agent timeout @@ -222,7 +224,7 @@ async def run_async( TimeoutAgentTool( agent=research_agent_fallback, timeout=60.0, # Set to 60.0 to test successful fallback after primary timeout - timeout_error_message="Fallback research agent timed out", + timeout_error_message='Fallback research agent timed out', skip_summarization=False, # Must be False for coordinator to continue ), # Error recovery agent @@ -250,4 +252,3 @@ async def run_async( ) root_agent = coordinator_agent -