fix(a2a): mark terminal A2A tasks as final responses - #6872
Open
Akshaay1 wants to merge 1 commit into
Open
Conversation
`convert_a2a_task_to_event` never reflected the task's terminal state onto the event it produced. When a peer does not advertise streaming, the a2a-sdk falls back to `message/send` and the whole turn arrives as a single task, so the converted event carries the peer's narration, its function calls, its function responses and its closing text together. `Event.is_final_response()` returns False for any event holding function calls or responses, so that event — the last one of the invocation — was never recognised as the end of the turn. A consumer that closes the turn on `is_final_response()`, which the helper's own docstring recommends, never closed it. This is the default path for an ADK-served peer: `to_a2a()` builds its card through `AgentCardBuilder` with `capabilities.streaming` left at False. Set `skip_summarization` when the task state is completed, failed or canceled. It is the existing signal for "final despite carrying tool activity", and it is already trusted from a peer via `_PEER_SETTABLE_ACTION_FIELDS`, so this does not widen what an A2A response can influence. `Event.is_final_response()` is left untouched. `input_required` and `auth_required` are deliberately excluded: they pause the turn rather than end it, and already reach `is_final_response()` through the mock function call built for them. Applied to both the v2 converter and the legacy one that `RemoteA2aAgent` imports. Fixes google#6584
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #6584.
Problem
convert_a2a_task_to_eventnever reflected the A2A task's terminal state onto the event it produced.When a peer does not advertise streaming, the a2a-sdk falls back to
message/sendand the entire turn arrives as a single task. The converted event therefore carries the peer's narration, itsfunction_calls, itsfunction_responses and its closing text all together.Event.is_final_response()returnsFalsefor any event holding function calls or responses:So ADK produced a completed task as the last event of the invocation, and its own public helper could not recognise it as the end of the turn. A consumer that closes the turn on
is_final_response()— which that helper's docstring explicitly recommends ("Application and UI layers can rely on this helper to detect a complete, user-facing response instead of replicating its logic") — never closed it.This is the default path for an ADK-served peer, not an exotic one:
to_a2a(...)builds its card throughAgentCardBuilderwithcapabilities.streamingleft atFalse.Fix
Set
event.actions.skip_summarizationwhen the task state iscompleted,failedorcanceled.Reasoning for that choice:
skip_summarizationis already the established signal for "this event is final despite carrying tool activity" — it is the first thingis_final_response()checks._PEER_SETTABLE_ACTION_FIELDSallowlist, whose docstring classifies it as inert, in contrast to the fields that "mutate the caller's own session ... or drive the caller's control flow and persistence". Setting it here therefore does not widen what an A2A response can influence.event.actions.skip_summarizationhas exactly one behavioural consumer (Event.is_final_response()); the only other reference is persistence invertex_ai_session_service. Thefunctions.pyconsumer readstool_context.actions, which is a different object on the local tool-execution path.Event.is_final_response()itself is left untouched, so the generic contract does not change.input_requiredandauth_requiredare deliberately excluded. They pause the turn to ask the caller for something rather than ending it, and they already reachis_final_response()through the mock function call_create_mock_function_call_for_required_user_inputbuilds for them. This was the edge case raised in review on the issue, and it is covered by a test that asserts those states are left exactly as they were.Applied to both converters — the v2
to_adk_event.pyand the legacyevent_converter.pythatRemoteA2aAgentimports — since the issue reports both paths behave the same way.In the legacy converter the state is read through a small
_is_terminal_task()helper usinggetattr, because 1.x tasks carry a protobufTaskStatuswhose fields are not always reachable on stand-in objects; an unreadable state simply means we cannot claim the turn is over.Before / after
Reproduced deterministically — no API key and no model call. A completed task carrying one function call and its response:
is_final_response()beforecompletedFalse❌True✅failedFalse❌True✅canceledFalse❌True✅workingFalse✅False✅ (unchanged)input_requiredTrue✅True✅ (unchanged)Tests
Added to both converter test modules:
test_convert_a2a_task_to_event_terminal_task_is_final_response— parametrized overcompleted/failed/canceled; asserts the tool activity is still carried on the event and that the turn now reads as closed.test_convert_a2a_task_to_event_non_terminal_task_is_not_final/..._working_task_is_not_final— an in-flight task must keep reporting the turn is open.test_convert_a2a_task_to_event_pause_state_left_untouched—input_required/auth_requiredkeep resolving through the mock function call, withskip_summarizationuntouched.Verified the new tests fail without the source change (
git stashon the two source files only): the 6 terminal-state cases fail, and the guard tests pass either way, as regression guards should.Full unit suite:
9095 passed, 56 skipped, 19 xfailed.The only failures are the two pre-existing
test_import_loading.py::test_entry_point_loads_only_allowlisted_packages[agent|runner]cases, which reproduce identically on a clean checkout ofmainwith no changes applied (environment-related, unrelated to this change).pyinkand the pre-commit hooks are clean.