fix(a2a): render a historical function response as text - #6844
Open
Akshaay1 wants to merge 1 commit into
Open
Conversation
A completed task delegation is recorded as a function response authored
"user" (`_synthesize_task_fr_event`). Because the author is "user",
`_is_other_agent_reply` is False, so `_present_other_agent_message` --
which already renders another agent's function responses as text -- never
sees it. `_construct_message_parts_from_session` then re-serialized it as
a DataPart next to the text parts of the same history, and the receiving
runner rejects that combination:
Message cannot contain both function responses and text.
So the first delegation in a turn succeeded and every later delegation to
a remote peer in the same turn failed.
Only the final session event can be a resume payload, and that path is
handled by `_create_a2a_request_for_user_function_response` before the
history rebuild ever runs. A function response older than that is history
and the peer has no invocation to resume, so render it as text instead of
sending it as data. Task mode keeps its existing remote_fc_ids rules
unchanged.
Fixes google#6831
|
Thanks for your pull request! It looks like this may be your first contribution to a Google open source project. Before we can look at your pull request, you'll need to sign a Contributor License Agreement (CLA). View this failed invocation of the CLA check for more information. For the most up to date status, view the checks section at the bottom of the pull request. |
Author
|
@googlebot I signed it! |
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.
Link to Issue or Description of Change
Problem:
Under
mode='task'delegation, succeeding at one delegation breaks every later delegation to a remote peer in the same turn.When a task delegation completes, ADK synthesizes its function response as an event authored
"user"(workflow/_llm_agent_wrapper.py::_synthesize_task_fr_event).RemoteA2aAgent._construct_message_parts_from_sessionthen rebuilds the next peer's request from raw session history.That rebuild already renders another agent's events as text via
_present_other_agent_message— including their function responses. But the synthesized function response is authored"user", so_is_other_agent_replyisFalseand it is never routed through that path. It was re-serialized verbatim as a DataPart, next to the text parts of the same history.The receiving agent's runner rejects exactly that combination (
runners.py::_validate_new_message):So the first hop looks perfect and everything after it fails. The A2A task comes back
TASK_STATE_FAILEDwith that sentence as its status message, and the coordinator's model treats the error string as the peer's answer — from the user's side the second specialist "just didn't do anything".Note the asymmetry this produced: the coordinator's function call was rendered as text, and the function response to it was not.
Solution:
Only the final session event can be a resume payload, and that path is handled by
_create_a2a_request_for_user_function_responsebefore the history rebuild ever runs. Any function response older than that is history by definition — the peer starting this turn has no invocation to resume — so it is rendered as text rather than sent as data.The conversion reuses the text rendering already used for this purpose in task mode, so both function calls and function responses now cross the wire as text.
Deliberately scoped:
remote_fc_idsrules still decide conversion there, so a peer's own outstanding calls keep working.test_construct_message_parts_from_session_foreign_function_response_not_convertedpins that behaviour on purpose, and it still passes. Narrowing to "not the final event" fixes the reported failure without reopening a decision that was already made.Testing Plan
Unit Tests:
Added
test_construct_message_parts_from_session_historical_function_response_convertedtotests/unittests/agents/test_remote_a2a_agent.py, building the session shape ADK produces after one completed task delegation followed by the next delegation.Confirmed the test fails for the right reason without the source change — the function response reaches the part converter at all:
With the fix applied:
The 2 failures are
test_import_loading.py::test_entry_point_loads_only_allowlisted_packages[agent|runner], and they are pre-existing and unrelated — they reproduce identically on an unmodified checkout ofmainin the same environment:That is dependency drift (
httpx2arrives via the currentanthropicrelease;sitecustomizeis a venv artifact), not something this change touches — it adds no imports.pyink --checkreports both files unchanged.Manual End-to-End (E2E) Tests:
Reproduced with the script from the issue, extended to the shape of the live trace reported there — the follow-on
FC:mathevent is what makes the synthesized function response non-final, which is the real-world path:Before:
After:
The delegation's result still reaches the peer, now carried as text, and the message no longer mixes a function response with text.
Environment:
google-adk2.7.1 (main@ 775c1bd),a2a-sdk1.1.2, Python 3.12.13, macOS.