`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
Fixes #6584.
Problem
convert_a2a_task_to_event never 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/send and the entire turn arrives as a single task. The converted event therefore carries the peer's narration, its function_calls, its function_responses and its closing text all together.
Event.is_final_response() returns False for 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 through AgentCardBuilder with capabilities.streaming left at False.
Fix
Set event.actions.skip_summarization when the task state is completed, failed or canceled.
Reasoning for that choice:
input_required and auth_required are deliberately excluded. They pause the turn to ask the caller for something rather than ending it, and they already reach is_final_response() through the mock function call _create_mock_function_call_for_required_user_input builds 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.py and the legacy event_converter.py that RemoteA2aAgent imports — 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 using getattr, because 1.x tasks carry a protobuf TaskStatus whose 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:
Tests
Added to both converter test modules:
Verified the new tests fail without the source change (git stash on 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 of main with no changes applied (environment-related, unrelated to this change).
pyink and the pre-commit hooks are clean.