Repository navigation
Description
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:
- Which hook actually fires.
RemoteA2aAgentdispatches each response to either_handle_a2a_responseor_handle_a2a_response_v2depending 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_v2never fires against it at all. A workaround/fix that only patches_v2silently does nothing in that setup. - Streaming vs non-streaming shape. With
streaming=Truethe terminal state arrives as anA2ATaskStatusUpdateEvent(update.status.state), not on theTaskitself (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. TaskState.failedisn't version-safe. The proposed fix in Failed A2A task errors leak into conversation history as regular content #4309 compares againstTaskState.failed, which only exists on the pydantic-enum shape ofa2a-sdk(0.3.x). On the protobuf shape (1.x, e.g.a2a-sdk==1.1.2, bundled withgoogle-adk==2.6.2),TaskStatehas no.failedattribute at all — the value isTaskState.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
surajksharma07 commentedon Aug 13, 2026
@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.
ferponse commentedon Aug 14, 2026
Thanks for the fix, @surajksharma07! I applied this PR's diff on top of
google-adk/a2a-sdklocally and tested it against a task that fails inside our sandboxed code agent (viaRemoteA2aAgent). Both the streaming path (TaskStatusUpdateEvent) and the non-streaming one (Task) now correctly detectstatus.state == TASK_STATE_FAILEDand propagateerror_code/error_messageon the resultingEvent— exactly the behavior we needed. Would be great to see this merged 🙏surajksharma07 commentedon Aug 17, 2026
@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.
ferponse commentedon Oct 3, 2026
This is still not fixed. It reproduces on
google-adk2.11.0 witha2a-sdk1.2.1, and onmainat 63aed55: a remote task that ends inTASK_STATE_FAILEDstill reaches the caller as an ordinary answer, witherror_code=Noneanderror_message=None, streamed or not.#6713 no longer applies. Since a6a4052,
RemoteA2aAgentlives insrc/google/adk/a2a/agent/_remote_a2a_agent.py, andsrc/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_impldispatches each response, so it covers:_handle_a2a_responseand_handle_a2a_response_v2);TaskStatusUpdateEventwhen streamed, theTaskwhen not);a2a_*_converter.The event gets
error_code="A2A_TASK_FAILED"and the remote's text aserror_message.taskmode 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.surajksharma07 commentedon Oct 7, 2026
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.
ferponse commentedon Oct 8, 2026
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: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.pydoesn't look aterror_code. I kept the content on purpose. If the event has no content,_contains_empty_contentdrops 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.pypresents every error event, which goes beyond this PR.I've added both points to the PR description under "What does not change".
surajksharma07 commentedon Oct 8, 2026
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.
DeanChensj commentedon Oct 14, 2026
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.
fix: mark failed A2A tasks as error events