Skip to content

fix(flows): pair async function responses on (id, name) not id alone - #6764

Open
a-yeyang wants to merge 1 commit into
google:mainfrom
a-yeyang:fix/rearrange-history-function-call-id-collision
Open

fix(flows): pair async function responses on (id, name) not id alone#6764
a-yeyang wants to merge 1 commit into
google:mainfrom
a-yeyang:fix/rearrange-history-function-call-id-collision

Conversation

@a-yeyang

Copy link
Copy Markdown

Link to Issue or Description of Change

Problem:

_rearrange_events_for_async_function_responses_in_history pairs function responses to calls by function_response.id alone. Model-supplied ids are not guaranteed unique — Vertex Gemini mints call_<n> ids from a space small enough to repeat within a single session, and ADK preserves model-supplied ids (remove_client_function_call_id strips only adk--prefixed ids; populate_client_function_call_id assigns one only when the id is empty). So a repeated id survives into this rebuild.

Because the index is built in forward order, a repeated id keeps only the newest response. The second pass then appends that response directly after the oldest call carrying the same id, pairing a function_call for one tool with the function_response of another. On the next generateContent the model rejects the contents with a bare 400 INVALID_ARGUMENT. The reporter measured this at ~1 collision in 153 requests in a real session (gemini-3.5-flash, Vertex EU), where a single collision ended the run.

Solution:

Key the pairing on (id, name) instead of id. ADK always sets function_response.name to the tool name (Part.from_function_response(name=tool.name, ...)), and the matching function_call carries the same name, so:

  • for every non-colliding history the pair is unchanged (behavior-preserving);
  • a collision can no longer resolve across two different tools, because the tool name disambiguates it.

This is the narrowest change that fixes the reported, reproduced behavior; it touches only the pairing key in this one function.

Testing Plan

Unit Tests:

  • I have added a regression test (test_rearrange_async_function_responses_pairs_on_id_and_name) that reproduces the reported shape — two tools (site_security_posture, fleet_security_summary) both reusing id call_807, interleaved with their responses — and asserts each call is followed by the response for the same tool. It fails on the old id-only keying and passes with this change.
  • All unit tests in tests/unittests/flows/llm_flows/test_contents.py pass locally.
$ python -m pytest tests/unittests/flows/llm_flows/test_contents.py
42 passed

Manual End-to-End (E2E) Tests:

The failure is intermittent and content-dependent (it requires the model to reuse an id across two different tools within one session), so it is exercised through the unit test above rather than a manual E2E run. The reproduction in the issue captures the exact rejected-request contents.

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

Model-supplied function-call ids are not guaranteed unique. Vertex Gemini
mints `call_<n>` ids from a small space that can repeat within a single
session, and ADK preserves model-supplied ids (only `adk-` prefixed ids are
generated or stripped by the framework), so a repeated id survives into the
history rebuild.

`_rearrange_events_for_async_function_responses_in_history` indexed responses
by `function_response.id` alone, in forward order, so a repeated id kept only
the newest response. The second pass then appended that response after the
oldest call carrying the same id, pairing a `function_call` for one tool with
the `function_response` of another. The model rejects the resulting contents
with a bare `400 INVALID_ARGUMENT`.

Key the pairing on `(id, name)` instead. ADK always sets `function_response.name`
to the tool name and the matching `function_call` carries the same name, so this
is behavior-preserving for every non-colliding history while keeping a collision
from resolving across two different tools.

Adds a regression test that reproduces the reported shape (two tools reusing
`call_807`) and asserts each call is paired with its own tool's response.

Fixes google#6761
@google-cla

google-cla Bot commented Aug 17, 2026

Copy link
Copy Markdown

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.

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

Labels

None yet

Projects

None yet

2 participants