Skip to content

Follow-up to #4309: fix for failed A2A tasks needs both _handle_a2a_response/_v2, _compat.TS_FAILED, and error_code — still reproducible in 2.6.2 #6708

Closed

Description

@ferponse

Follow-up to #4309 ("Failed A2A task errors leak into conversation history as regular content"), closed for lack of follow-up rather than a fix. Filing separately since we hit this independently on a newer version and found a few things the original report and its proposed workaround don't cover — happy to have this folded into #4309 instead if that's preferred.

Still present in google-adk==2.6.2 (Python 3.12). convert_a2a_task_to_event() (google/adk/a2a/converters/event_converter.py) never reads a2a_task.status.state; it only extracts message content and returns a plain Event. Same root cause as reported for 1.1.0 → 1.24.1.

This is a round-trip asymmetry, not just a missing check. convert_event_to_a2a_events() (used when ADK is the A2A server) already does the reverse correctly: if event.error_code is set, _create_error_status_event() builds a TaskStatus(state=TASK_STATE_FAILED, ...). So ADK already models "failed task" as a first-class A2A state on the way out — it just never reads that same state back in on the way in. Two ADK instances talking A2A to each other already lose this, not only ADK-vs-third-party.

Three things worth folding into whatever fix lands:

  1. Which hook actually fires. RemoteA2aAgent dispatches each response to either _handle_a2a_response or _handle_a2a_response_v2 depending on whether the task carries ADK's own integration-extension marker. A third-party (non-ADK) A2A server never sets that marker, so every response goes through _handle_a2a_response — _handle_a2a_response_v2 never fires against it at all. A workaround/fix that only patches _v2 silently does nothing in that setup.
  2. Streaming vs non-streaming shape. With streaming=True the terminal state arrives as an A2ATaskStatusUpdateEvent (update.status.state), not on the Task itself (task.status.state) — already flagged in Failed A2A task errors leak into conversation history as regular content #4309, restating because both hooks from point 1 need to handle both shapes.
  3. TaskState.failed isn't version-safe. The proposed fix in Failed A2A task errors leak into conversation history as regular content #4309 compares against TaskState.failed, which only exists on the pydantic-enum shape of a2a-sdk (0.3.x). On the protobuf shape (1.x, e.g. a2a-sdk==1.1.2, bundled with google-adk==2.6.2), TaskState has no .failed attribute at all — the value is TaskState.Value("TASK_STATE_FAILED"). ADK already has a shim for exactly this split: google.adk.a2a._compat.TS_FAILED. Any fix should compare against that instead of a hardcoded enum member.

Also: the proposed fix sets error_message but not error_code. That stops the leak into history, but callers that want to tell "remote task failed" apart from other RemoteA2aAgent error paths generally key off error_code (which exists on Event for exactly that). Worth setting a stable code (e.g. "A2A_TASK_FAILED") alongside the message.

Sketch (untested against internals of every version, but shows the shape):

from google.adk.a2a import _compat

if task and task.status and task.status.state == _compat.TS_FAILED:
    event = Event(
        author=self.name,
        error_code="A2A_TASK_FAILED",
        error_message=extracted_text_from(event) or "Remote agent task failed",
        invocation_id=ctx.invocation_id,
        branch=ctx.branch,
    )

applied in both _handle_a2a_response and _handle_a2a_response_v2, checking both the Task and TaskStatusUpdateEvent shapes.

Environment: google-adk 2.6.2, a2a-sdk 1.1.2, Python 3.12.

Happy to close as a duplicate of #4309 if you'd rather track everything there.

Activity

  1. surajksharma07 commented on Aug 13, 2026

    @surajksharma07
    Collaborator

    @ferponse _handle_a2a_response never checks task.status.state so a failed task's message rides into content with error_message/error_code both unset exactly as you traced.

    #6713 looks like it lands the same _compat.TS_FAILED + error_code shape you sketched and it held up against your repro.
    Could you try it against your original failure case and confirm it behaves as expected?
    If it checks out that PR looks like the right vehicle rather than opening a separate one.

  2. added
    request clarification[Status] The maintainer need clarification or more information from the author
    a2a[Component] This issue is related a2a support inside ADK.
    on Aug 13, 2026
  3. ferponse commented on Aug 14, 2026

    @ferponse
    ContributorAuthor

    Thanks for the fix, @surajksharma07! I applied this PR's diff on top of google-adk/a2a-sdk locally and tested it against a task that fails inside our sandboxed code agent (via RemoteA2aAgent). Both the streaming path (TaskStatusUpdateEvent) and the non-streaming one (Task) now correctly detect status.state == TASK_STATE_FAILED and propagate error_code/error_message on the resulting Event — exactly the behavior we needed. Would be great to see this merged 🙏

  4. surajksharma07 commented on Aug 17, 2026

    @surajksharma07
    Collaborator

    @ferponse That's exactly the confirmation needed, good to have both the streaming and non-streaming paths checked not just the synthetic repro. Nothing left on your end at this point.

    Team is looking more into it.

  5. removed
    request clarification[Status] The maintainer need clarification or more information from the author
    on Aug 17, 2026
  6. ferponse commented on Oct 3, 2026

    @ferponse
    ContributorAuthor

    This is still not fixed. It reproduces on google-adk 2.11.0 with a2a-sdk 1.2.1, and on main at 63aed55: a remote task that ends in TASK_STATE_FAILED still reaches the caller as an ordinary answer, with error_code=None and error_message=None, streamed or not.

    #6713 no longer applies. Since a6a4052, RemoteA2aAgent lives in src/google/adk/a2a/agent/_remote_a2a_agent.py, and src/google/adk/agents/remote_a2a_agent.py, the file #6713 edits, only re-exports it.

    I opened #7398, which redoes the fix on the current layout. It marks the failure in one place, where _run_async_impl dispatches each response, so it covers:

    • both handlers (_handle_a2a_response and _handle_a2a_response_v2);
    • both shapes (a TaskStatusUpdateEvent when streamed, the Task when not);
    • any custom a2a_*_converter.

    The event gets error_code="A2A_TASK_FAILED" and the remote's text as error_message. task mode and the other terminal states are left as they are. The PR includes Runner-level tests and a standalone repro against a plain a2a-sdk server, with output before and after the change.

  7. surajksharma07 commented on Oct 7, 2026

    @surajksharma07
    Collaborator

    Confirmed it's still on current main (8c513c9) @ferponse and #6713 is stale since the a6a4052 move so #7398 is the right place for this now. It also still merges clean on top of the later changes to _remote_a2a_agent.py.

    Marking the event once at the dispatch point after either handler runs is a better fit than patching both handlers and 78aad40 fixes the earlier-progress-text-as-error_message issue @astrogilda raised.

    Two things from reading it: the self.mode != "task" skip means the case @lamberttraccard described still goes out as plain text because the plain yield event runs before the task-mode TS_FAILED block. Also the content is kept and _contents.py doesn't filter on error_code so the failure text still reaches the coordinator's next LLM turn.

    Is task mode meant to stay out of scope here (left to #7251/#7385)? If so it'd help to say that in the PR description before a maintainer picks it up.

  8. ferponse commented on Oct 8, 2026

    @ferponse
    ContributorAuthor

    Thanks for reading it closely, @surajksharma07. You're right on both counts. I checked them on the PR branch (78aad40):

    Task mode. Yes, it's meant to stay out of scope. A failed task in mode="task" yields, in this order:

    text='claude exited with code 1'  error_code=None  error_message=None
    text=''                           error_code=None  error_message='Remote A2A task failed: claude exited with code 1'
    text=''                           error_code=None  error_message='Remote A2A task failed: …'  finish_task: {'result': 'Task failed.'}
    text=''                           (end_of_agent)
    

    So the failure is reported, but the remote's text still goes out first as a plain answer. #7251 reworks that same block: it computes the terminal state separately and sets the failure events' output. Marking it in this PR would just conflict with it. It can follow once #7251 lands.

    Content. Also right. After the marked failure, the coordinator's next request quotes it as the remote agent's message ([currency] said: <<<…>>> claude exited with code 1 <<<…>>>), since _contents.py doesn't look at error_code. I kept the content on purpose. If the event has no content, _contains_empty_content drops it, and the coordinator's next request has no trace of the failure at all. Telling the model that it was an error is a change to how _contents.py presents every error event, which goes beyond this PR.

    I've added both points to the PR description under "What does not change".

  9. surajksharma07 commented on Oct 8, 2026

    @surajksharma07
    Collaborator

    Thanks @ferponse. The "What does not change" section covers both points so scoping task mode to #7251 and leaving the _contents.py presentation for a separate change makes sense.

    Merged #7398 onto current main (7d56ef8) and it's still clean: test_remote_a2a_agent.py passes 343/343 including the 20 new failure cases.

    One small nit: after 4245327 main compares against AgentMode.TASK everywhere in _remote_a2a_agent.py. Your self.mode != "task" still works because AgentMode is a str enum but switching to AgentMode.TASK would match the rest of the file.

    Nothing else needed from your side. This is ready for maintainer review.

  10. DeanChensj commented on Oct 14, 2026

    @DeanChensj
    Collaborator

    Hi @ferponse, thank you for reporting this issue!

    This has been resolved by PR #6772 and merged into the main branch in commit f7967c3.

    We are closing this issue as resolved. If you find that the issue persists on the latest ADK release or if this was closed in error, please feel free to reopen it or leave a comment! Thank you again for your contribution.

  11. added a commit that references this issue on Oct 24, 2026
    25d1917
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

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