Skip to content

Conversation

@haseryo0403
Copy link

@haseryo0403 haseryo0403 commented Dec 8, 2025

Link to Issue or Description of Change

1. Link to an existing issue (if applicable):

2. Or, if no issue exists, describe the change:

N/A - Issue exists.

Problem:

When using Gemini 3 models with thinking enabled
(ThinkingConfig(include_thoughts=True)), multiple
sequential tool calls fail with the error:

Unable to submit request because function call
google_search in the 5. content block is missing a
thought_signature

This occurs because ADK creates an interleaved
function call/response history format:
model(fc1) → user(fr1) → model(fc2) → user(fr2) →
model(fc3) → user(fr3)

However, Gemini 3 models with thinking enabled
expect a grouped format:
model([fc1, fc2, fc3]) → user([fr1, fr2, fr3])

Solution:

Added _merge_interleaved_function_call_contents()
function in contents.py that transforms
interleaved function call/response patterns into the
grouped format before sending to the model.

Key design decisions:

  • Single function call/response pairs are NOT merged
    (no issue with single calls)
  • Only pure function call/response contents are
    merged (mixed content with text is preserved as-is)
  • The merge happens at the end of _get_contents()
    after all other processing

Testing Plan

Unit Tests:

  • I have added or updated unit tests for my
    change.
  • All unit tests pass locally.

$ uv run pytest tests/unittests/flows/llm_flows/test
_contents_function.py -v
========================= 10 passed in 0.51s =========================

$ uv run pytest tests/unittests/flows/llm_flows/ -v
========================= 295 passed, 160 warnings
in 1.08s =========================

New test cases added:

  • test_interleaved_function_calls_are_merged -
    Tests 2 interleaved tool calls are merged
  • test_three_interleaved_function_calls_are_merged
  • Tests 3+ interleaved tool calls
  • test_interleaved_merge_with_text_after - Tests
    merge works when followed by text content

Manual End-to-End (E2E) Tests:

Tested with production-like setup using:

  • Main agent: gemini-3-pro-preview with
    BuiltInPlanner(thinking_config=ThinkingConfig(inclu de_thoughts=True))
  • Sub-agent: AgentTool wrapping google_search
    with gemini-flash-latest
  • Multiple sequential google_search calls

Before fix:
ERROR: 400 Bad Request
"Unable to submit request because function call
default_api:google_search in the 5. content block is
missing a thought_signature"

After fix:
[1] FC: google_search
[1] FR: google_search
[2] FC: google_search
[2] FR: google_search
[3] FC: google_search
[3] FR: google_search
[4] FC: google_search
[4] FR: google_search

SUCCESS! No thought_signature error occurred.

Checklist

  • I have read the CONTRIBUTING.md document.
  • I have performed a self-review of my own code.
  • I have commented my code, particularly in hard-to-understand areas.
  • I have added tests that prove my fix is effective or that my feature works.
  • New and existing unit tests pass locally with my changes.
  • I have manually tested my changes end-to-end.
  • Any dependent changes have been merged and published in downstream modules.

Additional context

Add any other context or screenshots about the feature request here.

@gemini-code-assist
Copy link
Contributor

Summary of Changes

Hello @haseryo0403, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request addresses a critical compatibility issue with Gemini 3 models when ThinkingConfig(include_thoughts=True) is enabled. Previously, the system's interleaved handling of function calls and responses led to thought_signature errors. The changes introduce a new content merging mechanism that transforms this interleaved history into the grouped format expected by Gemini 3 models, ensuring seamless execution of sequential tool calls and enhancing the reliability of agent interactions.

Highlights

  • Gemini 3 Compatibility Fix: Resolved a "thought_signature" error in Gemini 3 models with thinking enabled by reformatting interleaved function call/response histories into a grouped format.
  • New Content Merging Logic: Introduced _merge_interleaved_function_call_contents() in contents.py to transform [model(fc1), user(fr1), model(fc2), user(fr2)] into [model([fc1, fc2]), user([fr1, fr2])].
  • Selective Merging: The new merging logic only applies to pure function call/response contents and does not merge single function call/response pairs, which are already in the correct format.
  • Comprehensive Testing: Added new unit tests (test_interleaved_function_calls_are_merged, test_three_interleaved_function_calls_are_merged, test_interleaved_merge_with_text_after) and performed manual end-to-end testing to validate the fix.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@adk-bot adk-bot added the core [Component] This issue is related to the core interface and implementation label Dec 8, 2025
Copy link
Contributor

@gemini-code-assist gemini-code-assist bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces a fix for an issue with Gemini 3 models where interleaved function calls and responses lead to thought_signature errors. The solution involves a new function, _merge_interleaved_function_call_contents, which correctly identifies and merges these interleaved calls into the required grouped format. The implementation is robust and well-supported by a comprehensive set of new unit tests that cover various scenarios, including multiple interleaved calls and mixed content types. Additionally, an existing test has been updated to align with this new behavior. The overall approach is sound, though I have a suggestion to refactor the new function for improved readability and maintainability by extracting some repeated logic into helper functions.

Comment on lines 370 to 431
if (
current.role == 'model'
and current.parts
and all(_is_function_call_part(p) for p in current.parts)
):
# Start collecting consecutive function call/response pairs
function_call_parts: list[types.Part] = list(current.parts)
function_response_parts: list[types.Part] = []
j = i + 1
pairs_count = 0 # Track how many fc/fr pairs we've seen

# Look ahead for interleaved pattern: fr1 -> fc2 -> fr2 -> fc3 -> ...
while j + 1 < len(contents):
next_content = contents[j]
next_next_content = contents[j + 1]

# Check if next is user content with only function responses
is_response = (
next_content.role == 'user'
and next_content.parts
and all(_is_function_response_part(p) for p in next_content.parts)
)
# Check if the one after is model content with only function calls
is_next_call = (
next_next_content.role == 'model'
and next_next_content.parts
and all(_is_function_call_part(p) for p in next_next_content.parts)
)

if is_response and is_next_call:
# This is an interleaved pattern, collect the parts
function_response_parts.extend(next_content.parts)
function_call_parts.extend(next_next_content.parts)
pairs_count += 1
j += 2
else:
break

# Only merge if we found at least one interleaved pair (fc1->fr1->fc2)
# A single fc->fr pair should not be merged
if pairs_count > 0:
# Check if there's a trailing function response for the last fc
if j < len(contents):
trailing = contents[j]
if (
trailing.role == 'user'
and trailing.parts
and all(_is_function_response_part(p) for p in trailing.parts)
):
function_response_parts.extend(trailing.parts)
j += 1

# Create merged model content with all function calls
merged_model = types.Content(role='model', parts=function_call_parts)
result.append(merged_model)

# Create merged user content with all function responses
merged_user = types.Content(role='user', parts=function_response_parts)
result.append(merged_user)

i = j
continue
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The logic for identifying pure function call and function response contents is repeated multiple times within this block. To improve readability and maintainability, you can use helper functions for these checks. This will make the main logic of _merge_interleaved_function_call_contents cleaner and easier to follow. I've added another comment suggesting the new helper functions.

    if _is_pure_function_call_content(current):
      # Start collecting consecutive function call/response pairs
      function_call_parts: list[types.Part] = list(current.parts)
      function_response_parts: list[types.Part] = []
      j = i + 1
      pairs_count = 0  # Track how many fc/fr pairs we've seen

      # Look ahead for interleaved pattern: fr1 -> fc2 -> fr2 -> fc3 -> ...
      while j + 1 < len(contents):
        next_content = contents[j]
        next_next_content = contents[j + 1]

        if _is_pure_function_response_content(
            next_content
        ) and _is_pure_function_call_content(next_next_content):
          # This is an interleaved pattern, collect the parts
          function_response_parts.extend(next_content.parts)
          function_call_parts.extend(next_next_content.parts)
          pairs_count += 1
          j += 2
        else:
          break

      # Only merge if we found at least one interleaved pair (fc1->fr1->fc2)
      # A single fc->fr pair should not be merged
      if pairs_count > 0:
        # Check if there's a trailing function response for the last fc
        if j < len(contents) and _is_pure_function_response_content(
            contents[j]
        ):
          function_response_parts.extend(contents[j].parts)
          j += 1

        # Create merged model content with all function calls
        merged_model = types.Content(role='model', parts=function_call_parts)
        result.append(merged_model)

        # Create merged user content with all function responses
        merged_user = types.Content(
            role='user', parts=function_response_parts
        )
        result.append(merged_user)

        i = j
        continue

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

and part.file_data is None
and part.function_call is None
)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To improve readability and reduce code duplication in _merge_interleaved_function_call_contents, it would be beneficial to add helper functions that check if a Content object is purely a function call or a function response.

def _is_pure_function_call_content(content: types.Content) -> bool:
  """Checks if a content object contains only function calls."""
  return (
      content.role == 'model'
      and content.parts
      and all(_is_function_call_part(p) for p in content.parts)
  )


def _is_pure_function_response_content(content: types.Content) -> bool:
  """Checks if a content object contains only function responses."""
  return (
      content.role == 'user'
      and content.parts
      and all(_is_function_response_part(p) for p in content.parts)
  )

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@haseryo0403 haseryo0403 changed the title feat: merge interleaved function call/response contents to avoid thought_signature errors fix: merge interleaved function call/response contents to avoid thought_signature errors Dec 8, 2025
@haseryo0403 haseryo0403 marked this pull request as ready for review December 8, 2025 08:49
@ryanaiagent ryanaiagent self-assigned this Dec 9, 2025
@ryanaiagent ryanaiagent added models [Component] Issues related to model support request clarification [Status] The maintainer need clarification or more information from the author labels Dec 11, 2025
@ryanaiagent
Copy link
Collaborator

Hi @haseryo0403 , Thank you for your work on this pull request. We appreciate the effort you've invested.
Can you please fix the failing unit tests before we can continue with the review.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

core [Component] This issue is related to the core interface and implementation models [Component] Issues related to model support request clarification [Status] The maintainer need clarification or more information from the author

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Gemini 3 function calls fail with missing thought_signature error after multiple tool uses

3 participants