Skip to content

Python: handle streamed A2A update events#4919

Open
sztoplover-bit wants to merge 2 commits intomicrosoft:mainfrom
sztoplover-bit:fix/a2a-stream-update-events
Open

Python: handle streamed A2A update events#4919
sztoplover-bit wants to merge 2 commits intomicrosoft:mainfrom
sztoplover-bit:fix/a2a-stream-update-events

Conversation

@sztoplover-bit
Copy link

@sztoplover-bit sztoplover-bit commented Mar 26, 2026

Summary

  • preserve A2A task update events when mapping streamed responses
  • surface TaskArtifactUpdateEvent chunks as AgentResponseUpdate items so streaming callers receive incremental output
  • avoid re-emitting terminal task artifacts after streamed update events have already produced the content

Verification

  • ./.venv/bin/pytest python/packages/a2a/tests/test_a2a_agent.py -q
  • validated a minimal TaskArtifactUpdateEvent streaming script now yields ['Hello']

Fixes #4901

@markwallace-microsoft
Copy link
Member

Python Test Coverage

Python Test Coverage Report •
FileStmtsMissCoverMissing
packages/a2a/agent_framework_a2a
   _agent.py2282190%352, 357, 359, 485, 500–502, 504, 518, 539, 560, 580, 594, 608, 620–621, 663–664, 693–695
TOTAL27986341787% 

Python Unit Test Overview

Tests Skipped Failures Errors Time
5431 20 💤 0 ❌ 0 🔥 1m 30s ⏱️

Copy link
Contributor

@giles17 giles17 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Automated Code Review

Reviewers: 4 | Confidence: 82%

✓ Correctness

This PR correctly fixes the issue where streaming TaskArtifactUpdateEvent events were silently discarded in _map_a2a_stream. The new _updates_from_task_update_event method properly extracts content from both TaskArtifactUpdateEvent (using artifact.parts) and TaskStatusUpdateEvent (using status.message.parts). The deduplication logic in _updates_from_task prevents terminal tasks from re-emitting artifacts that were already streamed incrementally, which is validated by the second test. The code correctly gates the new streaming behavior behind the emit_intermediate flag, preserving backward compatibility for non-streaming calers like poll_task. The implementation is sound and the tests cover both the basic streaming case and the deduplication edge case.

✓ Security Reliability

This PR correctly fixes the core bug where TaskArtifactUpdateEvent and TaskStatusUpdateEvent content was silently dropped during streaming. The new _updates_from_task_update_event method properly extracts incremental content from both event types. However, the deduplication logic that unconditionally suppresses terminal task artifact processing (return []) when emit_intermediate=True introduces a potential silent data-loss path: if an A2A server sends artifacts only on the terminal Task object (without preceding TaskArtifactUpdateEvent chunks), those artifacts are silently discarded because the code bypasses _parse_messages_from_task. The base branch does not have this issue — terminal tasks always fall through to artifact extraction. No security concerns were identified.

✓ Test Coverage

The PR correctly fixes the core bug where TaskArtifactUpdateEvent was silently discarded during streaming. Two new tests cover the primary artifact streaming path and the deduplication scenario (terminal task artifacts not re-emitted after streaming chunks). However, the new _updates_from_task_update_event method has an untested branch for TaskStatusUpdateEvent with message content, and the deduplication guard (if status.state in TERMINAL_TASK_STATES: return []) could suppress terminal task artifacts when no prior events carried content — an edge case with no test coverage.

✓ Design Approach

The PR correctly fixes the core bug: TaskArtifactUpdateEvent chunks were previously discarded entirely (bound to _update_event and ignored). The new _updates_from_task_update_event helper properly maps artifact update events to AgentResponseUpdate objects. The de-duplication logic (early-return [] when the terminal TaskStatusUpdateEvent has no message) is intentional and aligns with A2A spec semantics — the final status event is a completion signal, not a content carrier. One genuine design concern remains: the early-return on TERMINAL_TASK_STATES inside the emit_intermediate guard also short-circuits the background=True terminal-artifact path. If emit_intermediate=True and background=True are both active and the terminal event has no message, the terminal task's parsed artifacts are silently dropped — a behavioral regression vs. the prior code where terminal artifacts were always parsed. Additionally, TaskArtifactUpdateEvent.append (replace vs. append semantics) is not surfaced as a first-class field on AgentResponseUpdate, making it impossible for callers to correctly reconstruct content without reaching into raw_representation.


Automated review by giles17's agents

Comment on lines 420 to +426
"""
status = task.status

if emit_intermediate and update_event is not None:
if event_updates := self._updates_from_task_update_event(update_event):
return event_updates
if status.state in TERMINAL_TASK_STATES:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reliability concern: the deduplication guard returns [] for ALL terminal tasks when emit_intermediate=True and update_event is present, regardless of whether any prior artifact events actually delivered content. If no TaskArtifactUpdateEvent chunks were previously streamed (e.g., the server only puts artifacts on the terminal Task), those artifacts are silently lost — a regression from the base branch which always processes terminal task artifacts. This also bypasses the background=True terminal-artifact path. Consider guarding with a check that prior streaming content was yielded, or falling through to _parse_messages_from_task(task) as a safety net when no event content was produced.

Suggested change
"""
status = task.status
if emit_intermediate and update_event is not None:
if event_updates := self._updates_from_task_update_event(update_event):
return event_updates
if status.state in TERMINAL_TASK_STATES:
if status.state in TERMINAL_TASK_STATES:
# If the event itself carried content, we already returned it above.
# Fall through to _parse_messages_from_task only as a safety net
# when no event content was produced (keeps artifacts for servers
# that don't send TaskArtifactUpdateEvents).
pass

)
]

message = update_event.status.message
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Minor: this falls through to access update_event.status.message without an explicit isinstance(update_event, TaskStatusUpdateEvent) check. Currently safe because the union has only two types, but an explicit guard would prevent an AttributeError if the A2A SDK adds new event types in the future.

Suggested change
message = update_event.status.message
if not isinstance(update_event, TaskStatusUpdateEvent):
return []
message = update_event.status.message

Comment on lines +495 to +510

message = update_event.status.message
if message is None or not message.parts:
return []

contents = self._parse_contents_from_a2a(message.parts)
if not contents:
return []

return [
AgentResponseUpdate(
contents=contents,
role="assistant" if message.role == A2ARole.agent else "user",
response_id=update_event.task_id,
raw_representation=update_event,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This TaskStatusUpdateEvent-with-message branch is not exercised by any test. The existing streaming tests use update_event=None, and the new tests only cover TaskArtifactUpdateEvent. A test sending a (working_task, TaskStatusUpdateEvent(status=TaskStatus(state=working, message=A2AMessage(...)))) tuple would verify role mapping and content extraction through this path.

@sztoplover-bit
Copy link
Author

@microsoft-github-policy-service agree

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

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Python: [Bug]: A2AAgent should not ignore _update_event when processing responses

3 participants