Fix stored tool conversation replay order#621
Open
gracjankubicki wants to merge 1 commit into
Open
Conversation
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.
Summary
This PR fixes the replay order for stored assistant messages that contain tool calls, tool results, and final assistant content.
DatabaseConversationStorestores an assistant response as a single database row containing:contenttool_callstool_resultsBefore this change, that stored aggregate row was rehydrated as:
tool_callsand finalcontentThat produces an invalid or misleading tool-calling history:
This PR rehydrates the stored row into the same logical order produced during live tool execution:
The database storage format stays unchanged. Only the in-memory replay order returned by
DatabaseConversationStore::getLatestConversationMessages()changes.Why this order matters
This is not specific to one provider. The issue is in the provider-agnostic conversation store, and the rehydrated messages are later mapped into each provider's API format.
The correct flow for tool calling is consistently:
OpenAI
Docs:
Package code:
src/Gateway/OpenAi/Concerns/MapsMessages.phpOpenAI Responses API represents tool calls as
function_callitems and tool outputs asfunction_call_outputitems linked bycall_id. Replaying final assistant text beforefunction_call_outputbreaks the logical Responses API sequence.Azure OpenAI
Docs:
Package code:
src/Gateway/AzureOpenAi/AzureOpenAiGateway.phpAzure OpenAI uses the same OpenAI message mapping concerns in this package, so it has the same replay-order requirement as OpenAI Responses-compatible flows.
xAI
Docs:
Package code:
src/Gateway/Xai/Concerns/MapsMessages.phpxAI Responses API also uses
function_call_outputlinked bycall_id. Tool output must be replayed before the final assistant text.Anthropic
Docs:
Package code:
src/Gateway/Anthropic/Concerns/MapsMessages.phpAnthropic maps tool requests as
tool_useblocks and tool outputs astool_resultblocks. The final assistant text should not be replayed in the same logical step as thetool_usewhen the correspondingtool_resulthas not yet appeared.Gemini
Docs:
Package code:
src/Gateway/Gemini/Concerns/MapsMessages.phpGemini uses
functionCalland function response parts. Rehydrating the final text as a separate model message after the function response better matches the function-calling turn sequence.Groq
Docs:
Package code:
src/Gateway/Groq/Concerns/MapsMessages.phpGroq follows a Chat Completions-like flow where an assistant message with
tool_callsis followed byrole: tool, and then by the final assistant response.Mistral
Docs:
Package code:
src/Gateway/Mistral/Concerns/MapsMessages.phpMistral documents the function-calling conversation flow as assistant function call, tool result, then assistant answer. This PR makes stored conversation replay match that order.
DeepSeek
Docs:
Package code:
src/Gateway/DeepSeek/Concerns/MapsMessages.phpDeepSeek uses a Chat Completions-like tool-calling format. Tool result messages should follow the assistant tool call and precede the final assistant answer.
OpenRouter
Docs:
Package code:
src/Gateway/OpenRouter/Concerns/MapsMessages.phpOpenRouter standardizes tool calling across models and providers using a Chat Completions-like interface. Replaying the stored history in tool-call order avoids passing a misleading message sequence to routed models.
Ollama
Docs:
Package code:
src/Gateway/Ollama/Concerns/MapsMessages.phpOllama's tool-calling loop appends the model message with tool calls, appends the tool result, and then continues the loop until the model produces the final answer. This PR makes database replay follow that same order.
Amazon Bedrock
Docs:
Package code:
src/Gateway/Bedrock/BedrockTextGateway.phpBedrock Converse API uses
toolUseandtoolResult, linked bytoolUseId. A tool result is the response to a previous model tool request, so the final assistant response should be replayed after thetoolResult.Scope
This change intentionally keeps the PR small:
ConversationStorecontract changesStored tool conversations now replay as:
Tests
Added and updated storage tests to assert that stored tool conversations replay in the logical tool-calling order.
Local verification:
composer test -- --filter DatabaseConversationStoreTestcomposer test:lintcomposer testgit diff --checkFull local suite result: 1392 passed, 182 skipped due to missing external provider API keys.