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
- Deploy an agent that streams a multi-chunk text answer to Agent Engine via
A2aAgent.
- From a parent agent, delegate to it through
RemoteA2aAgent with streaming on.
- Log every event
RemoteA2aAgent._run_async_impl yields for the turn.
- 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.
Describe the bug
When
RemoteA2aAgentdelegates to a streaming A2A agent, every event it yields for the turn haspartial=True. It never yields a terminalpartial=Falseevent 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
RemoteA2aAgentshould emit a final aggregated non-partial event on the streaming path, and asked us to file it separately.Versions
google-adkat PR fix(a2a): keep streamed artifacts when a Task snapshot arrives mid-stream #7230 head (chelsealong/adk-python@0fe68deb,__version__2.9.2), on client and server. fix(a2a): keep streamed artifacts when a Task snapshot arrives mid-stream #7230 is required to get past thenonexistent artifact_iderror. This bug is separate and is still present with fix(a2a): keep streamed artifacts when a Task snapshot arrives mid-stream #7230 applied.A2aAgent,northamerica-northeast1.RemoteA2aAgentwith streaming enabled, calling/a2a/.../message:stream.To reproduce
A2aAgent.RemoteA2aAgentwith streaming on.RemoteA2aAgent._run_async_implyields for the turn.Expected behavior
COMPLETED,FAILED,CANCELEDorINPUT_REQUIRED),RemoteA2aAgentyields one non-partialEventwhose content is the full accumulated answer. ADK persists it like any other final response.Actual behavior
partial=True.TASK_STATE_COMPLETED, with 13 status updates and 1 artifact update in our run, but no non-partial event is yielded.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
RemoteA2aAgentto fix this on our side. The subclass:thought=Trueprogress text;Eventwith the buffered text. On 2.9.2 this is the path every streamed turn takes.We used a subclass rather than the
a2a_artifact_update_converterhook 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
RemoteA2aAgentdo 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.