Skip to content

RemoteA2aAgent emits only partial events on the streaming path, never a final aggregated non-partial event, so the specialist's answer is never persisted #7255

Description

@virktapvir

Describe the bug

When RemoteA2aAgent delegates to a streaming A2A agent, every event it yields for the turn has partial=True. It never yields a terminal partial=False event carrying the complete answer.

ADK does not persist partial events to the session. As a result, the parent agent never receives the remote agent's answer as a final response. Live streaming to a UI looks correct, but the answer is missing from session history and from anything that reads the final event.

This was confirmed with Google Cloud Support on the case that also covered #6680 / #7230. Support agreed that RemoteA2aAgent should emit a final aggregated non-partial event on the streaming path, and asked us to file it separately.

Versions

To reproduce

  1. Deploy an agent that streams a multi-chunk text answer to Agent Engine via A2aAgent.
  2. From a parent agent, delegate to it through RemoteA2aAgent with streaming on.
  3. Log every event RemoteA2aAgent._run_async_impl yields for the turn.
  4. Inspect the session after the turn.

Expected behavior

  • Partial events stream through as they arrive.
  • When the remote task reaches a terminal state (COMPLETED, FAILED, CANCELED or INPUT_REQUIRED), RemoteA2aAgent yields one non-partial Event whose content is the full accumulated answer. ADK persists it like any other final response.

Actual behavior

  • All answer-bearing events have partial=True.
  • The stream reaches TASK_STATE_COMPLETED, with 13 status updates and 1 artifact update in our run, but no non-partial event is yielded.
  • The specialist's answer is absent from the parent session.

Related history
On 2.6.3, the old path set event.partial = not update.last_chunk. The final non-partial event then carried only the last delta, not the full answer. The in-code comment explains the trade-off: converting the accumulated task would re-emit earlier chunks. Aggregating the text of the partials into the terminal event avoids that duplication while still producing a complete final event.

Workaround
We subclass RemoteA2aAgent to fix this on our side. The subclass:

  • passes partial events through unchanged, so live streaming still works;
  • buffers their non-thought text parts, excluding thought=True progress text;
  • if a terminal non-partial event arrives, rewrites its text to the full buffer and keeps any non-text parts;
  • if the stream ends on partials only, emits one non-partial Event with the buffered text. On 2.9.2 this is the path every streamed turn takes.
class AccumulatingRemoteA2aAgent(RemoteA2aAgent):
    async def _run_async_impl(self, ctx):
        buffered = []
        async for event in super()._run_async_impl(ctx):
            texts = [p.text for p in (event.content.parts if event.content else [])
                     if getattr(p, "text", None) and not getattr(p, "thought", False)]
            if not texts:
                yield event
                continue
            if event.partial:
                buffered.extend(texts)
                yield event
                continue
            if buffered:
                # rewrite terminal event's text parts to "".join(buffered + texts)
                ...
            yield event
        if buffered:
            yield Event(
                author=self.name,
                content=types.Content(parts=[types.Part(text="".join(buffered))]),
                invocation_id=ctx.invocation_id,
                branch=ctx.branch,
            )

We used a subclass rather than the a2a_artifact_update_converter hook because that hook only exists on _handle_a2a_response_v2. That path runs only when the response advertises the new-integration A2A extension, and our custom executor doesn't advertise it.

Suggested fix
Have RemoteA2aAgent do this natively on the streaming path: accumulate answer text from partial events and emit one aggregated non-partial event when the remote task reaches a terminal state. A stream that closes without reaching a terminal state should surface an error instead of returning silently.

Related: #6680, #7230.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

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

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions