fix(flows): pair every function call with a result in request contents - #6914
Open
QuentinBisson wants to merge 2 commits into
Open
fix(flows): pair every function call with a result in request contents#6914QuentinBisson wants to merge 2 commits into
QuentinBisson wants to merge 2 commits into
Conversation
A function call and its result are two separate session events. When a turn ends between them (a restart, an OOM kill, a disconnect, a cancellation), the session keeps a function_call that no function_response answers. That history is replayed on every later turn, and a provider that requires strict pairing rejects the whole conversation. Anthropic answers "tool_use ids were found without tool_result blocks immediately after", and the session stays unusable until it is deleted. Give every unanswered call a placeholder result in the immediately following content, while the request contents are assembled. The stored events are left untouched, so recorded history stays intact and a session that is already broken heals on its next turn without a migration. It sits above the session service, so it covers every store and every provider. A call the framework is holding open (a long-running tool, an approval, a request for user input) is described as awaiting a response, not as having returned nothing: told a tool returned nothing, the model reissues the call or proceeds without it; told the call is still awaiting a response, it can wait. Those ids are read from long_running_tool_ids, which lives on the event and does not survive the conversion to contents.
Anthropic requires every tool result to precede any other block in the message that carries it. Appending the placeholder to the end of a partially answered turn put it after a trailing text part, which is rejected for the same reason the missing result was. Insert the placeholder into the leading run of responses instead, and warn only for a call with no recorded response, so a call that is merely awaiting an answer stays quiet across the turns it spans.
QuentinBisson
marked this pull request as ready for review
August 26, 2026 13:57
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
A function call and its result are two separate session events. When a turn ends between them (a restart, an OOM kill, a client disconnect, a cancellation), the session keeps a
function_callthat nofunction_responseanswers.That history is replayed on every later turn, so the session does not recover on its own. A provider that requires strict pairing rejects the whole conversation: Anthropic answers
tool_use ids were found without tool_result blocks immediately after, and the session stays unusable until it is deleted._drop_orphaned_function_responsesalready covers the mirror case, a response with no call. The unanswered call has no owner.Change
Pair every function call with a result while the request contents are assembled, in
_get_contents. A call the immediately following content does not answer gets a placeholder result there.adk-id was stripped is answered by a response with the same stripped id.A call the framework holds open (a long-running tool, an approval, a request for user input) is described as awaiting a response, not as having returned nothing. The difference changes what the model does next: told a tool returned nothing, it reissues the call or proceeds without it; told the call is still awaiting a response, it can wait. Those ids come from
long_running_tool_ids, which lives on the event and does not survive the conversion to contents. Only a call with no recorded response is logged, so a call that is merely awaiting an answer stays quiet across the turns it spans.A conversation whose calls are all answered is returned unchanged.
The mirror change for adk-go is google/adk-go#1405.
Testing plan
uv run pytest tests/unittests— 13067 passed, 2 pre-existing failures intest_import_loading.pythat are present onmainas well.Seven new cases in
tests/unittests/flows/llm_flows/test_contents_function.py: the interrupted turn at the end of the history and in the middle, the partially answered parallel turn, the same turn with a trailing text part, the call held open in both directions, and calls without an id. Two more convert the repaired contents throughanthropic_llm.content_to_message_paramand assert both halves of the provider invariant, that everytool_useblock is answered by the next message and that the results precede any other block in it; a guard test asserts the same check rejects unrepaired input.Two assertions in
tests/unittests/apps/test_compaction.pyare updated, because a pending call now reaches the model with a placeholder.