Skip to content

[Bug] IndexError in RemoteA2aAgent TASK Response Handling #3769

@jfr4nc0

Description

@jfr4nc0

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:

  1. Initial Response (Task envelope):

    {
      "id": "task-123",
      "status": { "state": "submitted", "message": null },
      "parts": []
    }

    No parts - this is valid and expected

  2. Status Updates (via streaming/polling):

    {
      "status": {
        "state": "working",
        "message": { "parts": [...] }
      }
    }
  3. 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:

  1. Direct Execution (via Runner):

    runner = Runner(agent=remote_a2a_agent, ...)
    async for event in runner.run_async(...):  # ← Throws IndexError
  2. Orchestrator Delegation (as sub_agent):

    orchestrator = LlmAgent(
        sub_agents=[remote_a2a_agent],  # ← Tool calls fail with IndexError
    )
  3. 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 RemoteA2aAgent and streaming=True configuration

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 first

Expected: 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 = True

This 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 agents

Additional 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 location
  • google/adk/a2a/converters/event_converter.py - Creates events with empty parts
  • a2a-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.

Metadata

Metadata

Assignees

Labels

a2a[Component] This issue is related a2a support inside ADK.

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions