Repository navigation
A2A client renders thought parts together with final user-facing response #4676
Description
🔴 Required Information
Describe the Bug:
In A2A mode, the local ADK client displays both internal thought text and the final user-facing answer in one combined response.
The response payload contains multiple text parts, including one with metadata.adk_thought: true, but the client renders both instead of filtering thought content.
Steps to Reproduce:
- Install
google-adkand run an ADK server with A2A enabled. - Run a local ADK client against that A2A server.
- Send a normal user request that produces a multi-part response.
- Inspect the returned message/task parts: one part has
metadata.adk_thought: true, another part is the final answer. - Observe that the client output shows both parts concatenated in user-visible output.
Expected Behavior:
Only non-thought/final content should be shown to the user.
Parts marked with adk_thought: true should be hidden from user-facing rendering.
Observed Behavior:
Thought/internal text is rendered to the user together with final answer text.
Environment Details:
- ADK Library Version (pip show google-adk):
<fill> - Desktop OS: Linux
- Python Version (python -V):
<fill>
Model Information:
- Are you using LiteLLM:
<Yes/No> - Which model is being used:
<fill>
🟡 Optional Information
Regression:
Unknown / not yet verified.
Logs:
Sanitized payload shape observed:
parts[0]: kind=text, metadata.adk_thought=true, text=<internal reasoning>
parts[1]: kind=text, text=<final user answer>
Rendered output combines both parts.
Additional Context:
This appears to be a client-side rendering/filtering issue for thought parts in A2A responses.
Minimal Reproduction Code:
# Pseudocode (sanitized)
response = client.send_message("test request")
for i, p in enumerate(response.parts):
print(i, p.kind, getattr(p, "metadata", None), getattr(p, "text", None))
# Observed: part with adk_thought=true is still rendered in final UI output.How often has this issue occurred?:
Always (100%)
Activity
fix: filter thought parts from A2A client user-facing response
weiguangli-io commentedon Mar 3, 2026
I've submitted a fix in #4686.
The root cause is in
RemoteA2aAgent._handle_a2a_response(): while intermediate (working/submitted) events correctly mark all parts asthought=True, completed responses containing both thought and non-thought parts yield them all to consumers without filtering.The fix adds thought part filtering before returning the event — only non-thought parts are kept for completed responses, while intermediate events remain untouched.
klateefa commentedon Mar 4, 2026
@EliavShbt Thanks for reporting!
This looks like a missing filter step in
_handle_a2a_response(remote_a2a_agent.py) ,when a completed task artifact is processed,convert_a2a_task_to_event()returns all parts including thought-flagged ones without stripping them before surfacing to the consumer.As a workaround, filtering after the conversion with
event.content.parts = [p for p in event.content.parts if not p.thought]should stop the thought text from leaking into the user facing response, could you try that and let us know if it clears things up on your end?There's also a community PR (#4686) already targeting this exact gap worth validating the workaround against your setup before going that route separately.
Thank you.
fix: filter thought parts from A2A client user-facing response
EliavShbt commentedon Mar 5, 2026
Thanks for the suggestion — I tested the workaround and it doesn't fix my case, and I also traced the root cause.
The filter
[p for p in event.content.parts if not p.thought]has no effect becausep.thoughtis alwaysNoneon those parts. The thought flag is present on the wire (customMetadata → a2a:response → artifacts → parts → metadata.adk_thought: true), but it never gets restored onto the GenAIPartduring deserialization.Root cause:
convert_a2a_part_to_genai_part()inpart_converter.pydiscards the metadata on inbound conversion:The outbound direction (
convert_genai_part_to_a2a_part) correctly serializespart.thoughtintoadk_thought, but the inverse never reads it back. So the round-trip is broken and all parts arrive withthought=None.Fix needed in
part_converter.py:With this,
thoughtis correctly restored from wire metadata, and the filter in PR #4686 then works as intended. Without it, #4686 only catches parts tagged by thesubmitted/workingstate logic — completed artifact parts withadk_thoughtin metadata still leak through.Minimal payload demonstrating the issue:
{ "content": { "parts": [ { "text": "<internal reasoning text>" }, { "text": "<final user-facing answer>" } ], "role": "model" }, "customMetadata": { "a2a:response": { "artifacts": [{ "parts": [ { "kind": "text", "metadata": { "adk_thought": true }, "text": "<internal reasoning text>" }, { "kind": "text", "text": "<final user-facing answer>" } ] }] } } }fix: filter thought parts from A2A client user-facing response
surajksharma07 commentedon Mar 6, 2026
@EliavShbt your diagnosis is correct.
convert_a2a_part_to_genai_part()inpart_converter.pywas discarding theadk_thoughtmetadata on inbound conversion, sop.thoughtwas alwaysNoneand the filter in #4686 had no effect on completed artifact parts.This has already been fixed in main via commit
e59929e1("fix: Propagate thought from A2A TextPart metadata to GenAI Part", 2026-02-25), which restoresthought=Truefrom wire metadata during deserialization — after which the filtering in #4686 works end-to-end.The fix isn't in a released version yet, but you can pip-install directly from main or apply both changes locally to unblock yourself.
weiguangli-io commentedon Mar 9, 2026
Thanks @surajksharma07 for confirming the root cause in
convert_a2a_part_to_genai_part(). My PR #4686 addresses the filtering side in_handle_a2a_response, and the round-trip fix foradk_thoughtmetadata preservation would complement it. Happy to coordinate if you're working on thepart_converter.pyside.test: add tests for adk_thought metadata round-trip in part_converter
fix: filter thought parts from A2A client user-facing response
7 remaining items
surajksharma07 commentedon Mar 26, 2026
@seanzhou1023 Please have a look into it once.
fix: filter thought parts from A2A client user-facing response
fix: filter thought parts from A2A client user-facing response
test: add tests for adk_thought metadata round-trip in part_converter
surajksharma07 commentedon May 27, 2026
@EliavShbt — the root cause you traced (convert_a2a_part_to_genai_part discarding adk_thought on inbound conversion) was fixed in v1.27.0, so upgrading to v1.27.0 or later should fully resolve the thought-part leakage you were seeing.
PR #4686 from @weiguangli-io adds complementary filtering in _handle_a2a_response and is still under review for additional coverage.
Since we haven't heard back in a while, going ahead and closing this out — feel free to reopen if you're still hitting it after upgrading!
fix: filter thought parts from A2A client user-facing response