Skip to content

A2A client renders thought parts together with final user-facing response #4676

Closed

Description

@EliavShbt

🔴 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:

  1. Install google-adk and run an ADK server with A2A enabled.
  2. Run a local ADK client against that A2A server.
  3. Send a normal user request that produces a multi-part response.
  4. Inspect the returned message/task parts: one part has metadata.adk_thought: true, another part is the final answer.
  5. 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

  1. added
    a2a[Component] This issue is related a2a support inside ADK.
    on Mar 2, 2026
  2. added theissue type on Mar 2, 2026
  3. added a commit that references this issue on Mar 3, 2026
    5dbaa22
  4. weiguangli-io commented on Mar 3, 2026

    @weiguangli-io
    Contributor

    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 as thought=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.

  5. self-assigned this
    on Mar 4, 2026
  6. klateefa commented on Mar 4, 2026

    @klateefa

    @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.

  7. added
    answered[Status] This issue has been answered by the maintainer
    on Mar 4, 2026
  8. added a commit that references this issue on Mar 5, 2026
    e322381
  9. EliavShbt commented on Mar 5, 2026

    @EliavShbt
    Author

    @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.

    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 because p.thought is always None on 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 GenAI Part during deserialization.

    Root cause: convert_a2a_part_to_genai_part() in part_converter.py discards the metadata on inbound conversion:

    # Current — drops adk_thought metadata:
    if isinstance(part, a2a_types.TextPart):
        return genai_types.Part(text=part.text)

    The outbound direction (convert_genai_part_to_a2a_part) correctly serializes part.thought into adk_thought, but the inverse never reads it back. So the round-trip is broken and all parts arrive with thought=None.

    Fix needed in part_converter.py:

    if isinstance(part, a2a_types.TextPart):
        genai_part = genai_types.Part(text=part.text)
        if part.metadata and part.metadata.get(_get_adk_metadata_key('thought')):
            genai_part.thought = True
        return genai_part

    With this, thought is correctly restored from wire metadata, and the filter in PR #4686 then works as intended. Without it, #4686 only catches parts tagged by the submitted/working state logic — completed artifact parts with adk_thought in 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>" }
            ]
          }]
        }
      }
    }
  10. added a commit that references this issue on Mar 6, 2026
    002a010
  11. surajksharma07 commented on Mar 6, 2026

    @surajksharma07
    Collaborator

    @EliavShbt your diagnosis is correct.

    convert_a2a_part_to_genai_part() in part_converter.py was discarding the adk_thought metadata on inbound conversion, so p.thought was always None and 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 restores thought=True from 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.

  12. weiguangli-io commented on Mar 9, 2026

    @weiguangli-io
    Contributor

    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 for adk_thought metadata preservation would complement it. Happy to coordinate if you're working on the part_converter.py side.

  13. 7 remaining items

  14. added
    core[Component] This issue is related to the core interface and implementation
    on Mar 18, 2026
  15. surajksharma07 commented on Mar 26, 2026

    @surajksharma07
    Collaborator

    @seanzhou1023 Please have a look into it once.

  16. added
    needs review[Status] The PR/issue is awaiting review from the maintainer
    and removed
    answered[Status] This issue has been answered by the maintainer
    on Apr 16, 2026
  17. added 3 commits that reference this issue on Apr 29, 2026
    4aebbe9
    d9ec43f
    41ea0b3
  18. surajksharma07 commented on May 27, 2026

    @surajksharma07
    Collaborator

    @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!

  19. added a commit that references this issue on Aug 14, 2026
    8963a04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

a2a[Component] This issue is related a2a support inside ADK.core[Component] This issue is related to the core interface and implementationneeds review[Status] The PR/issue is awaiting review from the maintainer

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions