Repository navigation
fix(formatter): put parallel Gemini function responses in one user turn - #2631
Merged
qbc2016 merged 2 commits intoOct 21, 2026
Merged
Conversation
GeminiChatFormatter emitted one `user` content per ToolResultBlock, so a model turn with two function calls was answered by two separate functionResponse turns. Gemini requires the number of function response parts to match the number of function call parts in the preceding turn, and with a media-carrying result the promoted media turn was written between the two responses. The formatter now accumulates a contiguous run of tool results and flushes them as a single `user` content, with any promoted media in one turn after it, mirroring what AnthropicChatFormatter already does. A message with a single tool result formats exactly as before.
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.
AgentScope Version
2.0.8 (
main, base commit0727cbf)Description
Fixes #2628. That issue was split out of the review on #2613, and this change covers the Gemini side.
GeminiChatFormatter.formatgives every tool result its ownusercontent. The append is at src/agentscope/formatter/_gemini_formatter.py:232, inside theelif isinstance(block, ToolResultBlock):branch that opens at line 221. One result per turn is fine on its own, but parallel tool calls and their results all land in a single assistantMsg, becauseappend_contextin src/agentscope/state/_state.py extends the tail assistant message while thereply_idmatches. The formatter therefore sees twoToolCallBlocks followed by twoToolResultBlocks in one message and turns the two results into two separateusercontents.Gemini's parallel function calling contract counts the function response parts answering a model turn against the function call parts of that turn, and refuses a mismatch with
400 INVALID_ARGUMENT, "Please ensure that the number of function response parts is equal to the number of function call parts of the function call turn." What the formatter returns is handed togoogle-genaiverbatim ascontents(src/agentscope/model/_gemini/_model.py:320), so the shape goes out on the wire and the agent loop stops on the turn after any parallel tool call. I have not run this against the live API, so the evidence below is the request body the formatter builds, not a captured response.When one of the parallel results carries media the ordering is wrong as well. The promoted media turn is appended straight after the result it came from, so the model turn is followed by a functionResponse, then the media turn, then the second functionResponse.
GeminiMultiAgentFormatterhas the same problem, since its_format_tool_sequencecallsGeminiChatFormatter.formatat src/agentscope/formatter/_gemini_formatter.py:366, so the fix below covers it with no edit of its own.AnthropicChatFormattercarried this bug and it was fixed in #1894. The comment at src/agentscope/formatter/_anthropic_formatter.py:183-189 spells out why an extraToolResultBlockmust not open a new user message. This change does the same for Gemini, with the same scope: results accumulate within a singleMsg, so two results that arrive as two separateMsgobjects still produce two turns, exactly as on the Anthropic side.Repro on
main, one user message then an assistant message holding twoToolCallBlocks and their twoToolResultBlocks:After the change:
The same two calls with a PNG in the first result, on
main:After the change, which is the ordering #2613 proposes for the OpenAI family formatters:
Change
A contiguous run of
ToolResultBlocks now accumulates intotool_result_parts, and media promoted out of those results intopromoted_parts. A small_flush_tool_resultshelper writes the run out as oneusercontent followed by one media content, and it runs both when a block that is not a tool result interrupts the run and at the end of the message. Nothing else moves: thefunction_callformatting and its repair path, the thinking, hint and data branches, the role mapping, theconvert_tool_result_to_stringoutput and the promoted media parts themselves are untouched, and theif parts:flush that already opens the tool result branch stays where it is. A message with a single tool result, with or without media, formats identically to before, which is why nine of the ten existing tests pass unchanged.Tests
test_parallel_tool_results_share_one_user_turnin tests/formatter_gemini_test.py builds the assistant message with two calls and two results and asserts the whole formatted list, mirroring the sibling tests in that file.test_chat_formatter_parallel_tool_media_after_tool_msgscovers the ordering half: the first of the two results carries a base64 PNG, and the assertion pins the media turn after both function responses rather than between them. It takes the name and shape #2613 gives the same case in the other formatter test files.test_chat_formatter_complex_multi_stepassertedcall_1andcall_2in two separate user turns, which encoded the bug; correcting it is a five line deletion that merges the two turns. Run againstmain's formatter, those three fail and the other nine pass; on this branch all twelve pass.The formatter files are listed one by one in the second command because
pytest tests/ -k formatter -qaborts here with 18 collection errors: the vdb, workspace, storage, realtime and tui extras are not installed.macOS, Python 3.11.15, agentscope 2.0.8, black 23.3.0, flake8 6.1.0, mypy 1.7.0, pylint 3.0.2. The new tests import only
agentscope.formatterandagentscope.messageand make no network call, so they need nothing CI does not already install.Checklist
Please check the following items before code is ready to be reviewed.