fix: Update remote_a2a_agent to better handle streaming events and avoid duplicate responses

Currently, the A2A Task -> ADK event conversion is producing the same events on the last two update events (the last is a status update marking the task complete)

The change here based on A2AClientEvent(task, update):
 - if the update == None: handle the non-streaming task case and also streaming case for the initial task creation event
 - if the update = TaskStatusUpdateEvent AND a message is set: emit an event with that message
 - if a task status update AND no message is set: don't emit event (for example, the final status update)
 - if the update is ArtifactUpdateEvent and it's final artifact: emit the event

PiperOrigin-RevId: 812878869
This commit is contained in:
Google Team Member
2025-09-29 11:42:49 -07:00
committed by Copybara-Service
parent b1ee013347
commit 8e5f361264
2 changed files with 266 additions and 11 deletions
+34 -7
View File
@@ -36,6 +36,8 @@ try:
from a2a.types import Message as A2AMessage
from a2a.types import Part as A2APart
from a2a.types import Role
from a2a.types import TaskArtifactUpdateEvent as A2ATaskArtifactUpdateEvent
from a2a.types import TaskStatusUpdateEvent as A2ATaskStatusUpdateEvent
from a2a.types import TransportProtocol as A2ATransport
except ImportError as e:
import sys
@@ -393,7 +395,7 @@ class RemoteA2aAgent(BaseAgent):
async def _handle_a2a_response(
self, a2a_response: A2AClientEvent | A2AMessage, ctx: InvocationContext
) -> Event:
) -> Optional[Event]:
"""Handle A2A response and convert to Event.
Args:
@@ -401,14 +403,37 @@ class RemoteA2aAgent(BaseAgent):
ctx: The invocation context
Returns:
Event object representing the response
Event object representing the response, or None if no event should be
emitted.
"""
try:
if isinstance(a2a_response, tuple):
# ClientEvent is a tuple of the absolute Task state and the last update.
# We only need the Task state.
task = a2a_response[0]
event = convert_a2a_task_to_event(task, self.name, ctx)
task, update = a2a_response
if update is None:
# This is the initial response for a streaming task or the complete
# response for a non-streaming task, which is the full task state.
# We process this to get the initial message.
event = convert_a2a_task_to_event(task, self.name, ctx)
elif isinstance(update, A2ATaskStatusUpdateEvent) and update.message:
# This is a streaming task status update with a message.
event = convert_a2a_message_to_event(update.message, self.name, ctx)
elif isinstance(update, A2ATaskArtifactUpdateEvent) and (
not update.append or update.last_chunk
):
# This is a streaming task artifact update.
# We only handle full artifact updates and ignore partial updates.
# Note: Depends on the server implementation, there is no clear
# definition of what a partial update is currently. We use the two
# signals:
# 1. append: True for partial updates, False for full updates.
# 2. last_chunk: True for full updates, False for partial updates.
event = convert_a2a_task_to_event(task, self.name, ctx)
else:
# This is a streaming update without a message (e.g. status change)
# or an partial artifact update. We don't emit an event for these
# for now.
return None
event.custom_metadata = event.custom_metadata or {}
event.custom_metadata[A2A_METADATA_PREFIX + "task_id"] = task.id
if task.context_id:
@@ -416,7 +441,7 @@ class RemoteA2aAgent(BaseAgent):
task.context_id
)
# Otherwise, it's a regular A2AMessage.
# Otherwise, it's a regular A2AMessage for non-streaming responses.
elif isinstance(a2a_response, A2AMessage):
event = convert_a2a_message_to_event(a2a_response, self.name, ctx)
event.custom_metadata = event.custom_metadata or {}
@@ -492,6 +517,8 @@ class RemoteA2aAgent(BaseAgent):
logger.debug(build_a2a_response_log(a2a_response))
event = await self._handle_a2a_response(a2a_response, ctx)
if not event:
continue
# Add metadata about the request and response
event.custom_metadata = event.custom_metadata or {}