Skip to content

Tool messages are duplicated when they immediately follow AI message with tool calls #98

@yash025

Description

@yash025

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions