-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Description
Summary
RemoteA2aAgent._handle_a2a_response() throws IndexError: list index out of range when processing initial TASK_REQUIRED responses that have no message parts. This is a valid A2A protocol scenario that the ADK fails to handle safely.
Bug Location
File: google/adk/agents/remote_a2a_agent.py
Line: 411
Method: RemoteA2aAgent._handle_a2a_response()
Problematic Code
# Line 410-411
if task and task.status and task.status.state == TaskState.submitted:
event.content.parts[0].thought = True # ← IndexError when parts is empty!Root Cause
The A2A Protocol Flow
When a remote A2A agent returns TASK_REQUIRED, the protocol sequence is:
-
Initial Response (Task envelope):
{ "id": "task-123", "status": { "state": "submitted", "message": null }, "parts": [] }✅ No parts - this is valid and expected
-
Status Updates (via streaming/polling):
{ "status": { "state": "working", "message": { "parts": [...] } } } -
Final Response:
{ "status": { "state": "completed", "message": { "parts": [{ "text": "..." }] } } }
The ADK Contradiction
The ADK's own event converter correctly handles empty parts:
File: google/adk/a2a/converters/event_converter.py
Lines: 288-301
def convert_a2a_message_to_event(...):
if not a2a_message.parts:
logger.warning("A2A message has no parts, creating event with empty content")
return Event(
...
content=genai_types.Content(role="model", parts=[]), # ← Empty list
)But then remote_a2a_agent.py:411 tries to access parts[0] without checking if the list is empty.
Impact
Severity: High
This bug affects all RemoteA2aAgent usage when remote agents use TASK_REQUIRED responses:
-
❌ Direct Execution (via
Runner):runner = Runner(agent=remote_a2a_agent, ...) async for event in runner.run_async(...): # ← Throws IndexError
-
❌ Orchestrator Delegation (as sub_agent):
orchestrator = LlmAgent( sub_agents=[remote_a2a_agent], # ← Tool calls fail with IndexError )
-
✅ Non-TASK responses work fine (direct message responses with parts)
Error Manifestation
Logs:
WARNING | A2A message has no parts, creating event with empty content
ERROR | A2A request failed: list index out of range
Effect:
- Direct execution: Error propagates to caller
- Orchestrator: Tool returns
{"result": null}, breaks agent delegation
Reproduction Steps
Prerequisites
- Remote A2A agent that returns TASK_REQUIRED responses
- ADK with
RemoteA2aAgentandstreaming=Trueconfiguration
Minimal Reproduction
from google.adk.agents.remote_a2a_agent import RemoteA2aAgent
from google.adk.runners import Runner
from google.adk.sessions.in_memory_session_service import InMemorySessionService
from a2a.client.client_factory import ClientFactory, ClientConfig
from a2a.types import TransportProtocol
import grpc.aio
# Configure RemoteA2aAgent
agent_card = ... # Agent card from remote service
channel_factory = lambda url: grpc.aio.insecure_channel(url.replace("grpc://", ""))
client_config = ClientConfig(
grpc_channel_factory=channel_factory,
supported_transports=[TransportProtocol.grpc],
streaming=True,
polling=True,
)
agent = RemoteA2aAgent(
name="test_agent",
agent_card=agent_card,
a2a_client_factory=ClientFactory(config=client_config),
)
# Execute agent
runner = Runner(agent=agent, app_name="test", session_service=InMemorySessionService())
session = await session_service.create_session(app_name="test", user_id="user-1")
# This will throw IndexError when remote agent returns TASK_REQUIRED
async for event in runner.run_async(
user_id=session.user_id,
session_id=session.id,
new_message={"parts": [{"text": "test query"}]},
):
print(event) # ← Never reached, IndexError thrown firstExpected: Agent responds successfully after TASK completion
Actual: IndexError: list index out of range at line 411
Proposed Fix
Option 1: Add Bounds Check (Minimal, Recommended)
File: google/adk/agents/remote_a2a_agent.py
Line: 410-411
# Current (buggy)
if task and task.status and task.status.state == TaskState.submitted:
event.content.parts[0].thought = True
# Fixed
if task and task.status and task.status.state == TaskState.submitted:
if event.content and event.content.parts:
event.content.parts[0].thought = True
# If no parts, skip marking as thought (valid for initial TASK responses)Option 2: Mark All Parts as Thought (More Robust)
if task and task.status and task.status.state == TaskState.submitted:
if event.content and event.content.parts:
for part in event.content.parts:
part.thought = TrueThis handles cases where there might be multiple parts, not just the first one.
Workarounds (User-Side)
Until this is fixed in ADK, users can apply the following workarounds:
Workaround: Monkey-Patch ADK Method
Patch RemoteA2aAgent._handle_a2a_response at application startup:
File: src/common/adk_patches.py
import logging
from typing import Any
logger = logging.getLogger(__name__)
def patch_remote_a2a_agent():
"""Patch RemoteA2aAgent to fix IndexError bug at line 411."""
try:
from google.adk.agents.remote_a2a_agent import RemoteA2aAgent
from google.adk.a2a.converters.event_converter import convert_a2a_task_to_event
from a2a.types import TaskState
original_handle = RemoteA2aAgent._handle_a2a_response
async def patched_handle(self, a2a_response: Any, ctx: Any):
"""Patched version with bounds check."""
if isinstance(a2a_response, tuple):
task, update = a2a_response
if update is None:
event = convert_a2a_task_to_event(task, self.name, ctx)
# FIXED: Add bounds check before accessing parts[0]
if task and task.status and task.status.state == TaskState.submitted:
if event.content and event.content.parts:
event.content.parts[0].thought = True
event.custom_metadata = event.custom_metadata or {}
event.custom_metadata["a2a:task_id"] = task.id
if task.context_id:
event.custom_metadata["a2a:context_id"] = task.context_id
return event
return await original_handle(self, a2a_response, ctx)
RemoteA2aAgent._handle_a2a_response = patched_handle
logger.info("✓ Applied ADK patch: RemoteA2aAgent._handle_a2a_response")
except Exception as e:
logger.error(f"Failed to apply ADK patch: {e}", exc_info=True)
# Apply at application startup
def initialize_adk_patches():
patch_remote_a2a_agent()Usage:
# In main.py or startup
from src.common.adk_patches import initialize_adk_patches
initialize_adk_patches() # Apply patches before creating agentsAdditional Context
Environment
- Python: 3.13
- ADK: google-adk>=0.1.0
- A2A SDK: a2a-sdk[grpc]>=0.3.0
- gRPC: grpcio>=1.60.0
Related Components
google/adk/agents/remote_a2a_agent.py- Bug locationgoogle/adk/a2a/converters/event_converter.py- Creates events with empty partsa2a-sdk- Defines TASK protocol
Acknowledgments
This bug was identified while building a hierarchical multi-agent orchestration system using ADK's RemoteA2aAgent with gRPC-based A2A agents returning TASK_REQUIRED responses per the A2A protocol specification.