Skip to content

fix(a2a): keep every artifact when converting an A2A task to an event - #7201

Open
AtulJoshi1206 wants to merge 1 commit into
google:mainfrom
AtulJoshi1206:fix/a2a-task-artifacts
Open

AtulJoshi1206 wants to merge 1 commit into
google:mainfrom
AtulJoshi1206:fix/a2a-task-artifacts

Conversation

@AtulJoshi1206

Copy link
Copy Markdown

Please ensure you have read the contribution guide before creating a pull request.

Link to Issue or Description of Change

No existing issue; described below following the bug-report structure.

Describe the Bug:

convert_a2a_task_to_event in a2a/converters/event_converter.py builds the ADK event from only the task's last artifact:

if a2a_task.artifacts:
  message = Message(
      message_id="",
      role=_compat.ROLE_AGENT,
      parts=a2a_task.artifacts[-1].parts,
  )

An A2A Task carries one Artifact per output the remote agent produced, so everything before the final artifact is dropped on the way into ADK.

The sibling converter in the same package, to_adk_event.convert_a2a_task_to_event, already flattens them:

artifact_parts = [
    part for artifact in a2a_task.artifacts for part in artifact.parts
]

Two converters for the same A2A shape disagree, and one of them loses data.

Impact:

This is the default client path, not a legacy one. RemoteA2aAgent._handle_a2a_response calls it for the initial response of a streaming task and for the complete response of a non-streaming task. _run_async_impl only routes to _handle_a2a_response_v2 when the peer advertises the new ADK integration extension, so any non-ADK peer, or an ADK peer on the legacy executor, lands here.

Multiple artifacts are the normal result rather than an edge case: ADK's own server-side from_adk_event.convert_event_to_a2a_events mints a fresh artifact_id for every non-partial event, so an agent that produces N events returns N artifacts.

Steps to Reproduce:

On current main (665ec9835), convert a completed task carrying three artifacts:

task.artifacts = [
    Artifact(artifact_id="a1", parts=[make_text_part("FIRST")]),
    Artifact(artifact_id="a2", parts=[make_text_part("SECOND")]),
    Artifact(artifact_id="a3", parts=[make_text_part("THIRD")]),
]
event = convert_a2a_task_to_event(task, "remote")

Observed Behavior:

assert ['THIRD'] == ['FIRST', 'SECOND', 'THIRD']
  At index 0 diff: 'THIRD' != 'FIRST'
  Right contains 2 more items, first extra item: 'SECOND'

The same task through to_adk_event.convert_a2a_task_to_event yields all three.

Expected Behavior:

Every artifact's parts reach the event, in task order, matching the sibling converter.

Solution:

Flatten all artifacts instead of indexing the last, using the same comprehension the sibling converter already uses.

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my change.
  • All unit tests pass locally.

Added test_convert_a2a_task_to_event_keeps_every_artifact beside the existing test_convert_a2a_task_to_event_with_artifacts_priority. Verified it fails on an unmodified event_converter.py and passes with the change. The existing artifact tests use a single artifact, where flattening is the identity, so they are unaffected.

$ pytest tests/unittests/a2a -q
720 passed, 49 skipped

$ pytest tests/unittests -q
15226 passed, 86 skipped, 27 xfailed, 2 xpassed in 346.47s (0:05:46)
# 1 unrelated failure, test_import_loading.py::...[agent], is an artifact of the
# git-worktree checkout I built this on; it fails there on an unmodified main
# and passes in a normal clone.

$ pre-commit run --files src/google/adk/a2a/converters/event_converter.py tests/unittests/a2a/converters/test_event_converter.py
# all hooks pass

Manual End-to-End (E2E) Tests:

Driven through the converter directly, since reaching it end-to-end needs a live remote A2A peer. Running the same three-artifact task through both converters:

before (main):   event_converter -> ['THIRD artifact']
                 to_adk_event    -> ['FIRST artifact', 'SECOND artifact', 'THIRD artifact']

after (this PR): event_converter -> ['FIRST artifact', 'SECOND artifact', 'THIRD artifact']
                 to_adk_event    -> ['FIRST artifact', 'SECOND artifact', 'THIRD artifact']

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules. (none)

Additional context

No public API change. Streaming chunk updates go through convert_a2a_message_to_event on the per-update artifact and carry an explicit comment about not re-emitting the accumulated task, so this does not introduce duplicate parts. The one behaviour change is that a client reconnecting to an already-accumulated task now sees every accumulated artifact rather than only the last.

🤖 Generated with Claude Code

convert_a2a_task_to_event built the event from a2a_task.artifacts[-1], so a
task carrying more than one artifact lost everything before the last one. A
remote agent returns one artifact per non-partial event it produced, and
ADK's own convert_event_to_a2a_events mints a fresh artifact_id per event,
so a multi-event remote agent hit this on every call.

This is the default client path: RemoteA2aAgent._handle_a2a_response uses it
for any peer that does not advertise the new integration extension. Flatten
the artifacts the way to_adk_event.convert_a2a_task_to_event already does.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants