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.stale[Status] Issues which have been marked inactive since there is no user response

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions