-
Notifications
You must be signed in to change notification settings - Fork 130
Description
Description
I'm encountering an issue where tool messages get duplicated during summarization when they immediately follow an AI message with tool calls. This duplication causes a validation error from the LLM provider because the duplicated tool messages don't have a corresponding AI message with tool calls immediately before them.
Error Message
{'error_message': 'Chat completion failed', 'cause': "BadRequestError: AzureException BadRequestError - Invalid parameter: messages with role 'tool' must be a response to a preceeding message with 'tool_calls'."}
Expected Behavior
Tool messages should only appear once in the summarized output, maintaining the natural conversation flow and proper message sequence validation.
Reproduction
Here's a minimal example of the message structure that triggers this:
messages = [
HumanMessage(content="Analyze metrics for asset XYZ"),
AIMessage(content="", tool_calls=[
{"id": "call_1", "name": "query_errors", "args": {...}},
{"id": "call_2", "name": "query_latency", "args": {...}},
]),
ToolMessage(content="error_results", tool_call_id="call_1"),
ToolMessage(content="latency_results", tool_call_id="call_2"),
]After summarization, this becomes:
[
HumanMessage(...),
AIMessage(tool_calls=[...]), # ← AI message with tool calls
ToolMessage(tool_call_id="call_1"), # ← Valid tool response
ToolMessage(tool_call_id="call_2"), # ← Valid tool response
ToolMessage(tool_call_id="call_1"), # ← DUPLICATE - no preceding AI message!
ToolMessage(tool_call_id="call_2"), # ← DUPLICATE - no preceding AI message!
]The duplicated tool messages at the end violate the chat completion API requirements because they don't immediately follow an AI message with the corresponding tool calls.
Likely Problem
Looking at the _preprocess_messages function around line 197-214, the issue is in this logic:
# If the last message is an AI message with tool calls,
# include subsequent corresponding tool messages in the summary as well,
# to avoid issues w/ the LLM provider
if (
messages_to_summarize
and isinstance(messages_to_summarize[-1], AIMessage)
and (tool_calls := messages_to_summarize[-1].tool_calls)
):
# Add any matching tool messages from our dictionary
for tool_call in tool_calls:
if tool_call["id"] in tool_call_id_to_tool_message:
tool_message = tool_call_id_to_tool_message[tool_call["id"]]
messages_to_summarize.append(tool_message)The function builds tool_call_id_to_tool_message from ALL messages, then when it finds an AI message with tool calls at the boundary, it appends the corresponding tool messages - even if they're already part of messages_to_summarize. This creates invalid message sequences that LLM providers reject.
Suggested Fix
Check if tool messages are already included before adding them:
if (
messages_to_summarize
and isinstance(messages_to_summarize[-1], AIMessage)
and (tool_calls := messages_to_summarize[-1].tool_calls)
):
# Get IDs of tool messages already in messages_to_summarize
existing_tool_call_ids = {
msg.tool_call_id for msg in messages_to_summarize
if isinstance(msg, ToolMessage) and msg.tool_call_id
}
# Only add tool messages that aren't already included
for tool_call in tool_calls:
tool_call_id = tool_call["id"]
if (tool_call_id in tool_call_id_to_tool_message and
tool_call_id not in existing_tool_call_ids):
tool_message = tool_call_id_to_tool_message[tool_call_id]
messages_to_summarize.append(tool_message)Environment
- LangMem version: 0.0.27
- Using with LangGraph ReAct agents
- Azure OpenAI as the LLM provider
- Standard tool call/response pattern
Please let me know if this is a valid issue - I'd be happy to contribute.