Skip to content

[Bug] Long Conversation Triggers "tool message without toolcalls" Error #412

@Clawiee

Description

@Clawiee

🐛 Bug: Long Conversation Triggers "tool message without toolcalls" Error

Description

When a conversation exceeds a certain length threshold, it triggers an LLM API error:

Messages with role 'tool' must be a response to a preceding message with 'toolcalls'

Starting a new conversation fixes the issue, confirming it's related to message history length.

Steps to Reproduce

  1. Have a conversation with multiple tool/function calls
  2. Continue the conversation past a certain message count or token threshold
  3. Attempt another tool call
  4. Error: invalidrequesterror - tool message without preceding toolcalls

Root Cause Analysis

The issue is likely caused by message history truncation in one of these scenarios:

Scenario 1: Truncation cuts at tool_calls boundary

[正常历史]
Assistant: has tool_calls: [search]
User (tool): "search results..."
Assistant: has tool_calls: [write_file]
User (tool): "file written..."

[截断后]
User (tool): "file written..."  ← 没有前面的 tool_calls,API 报错!

Scenario 2: Inconsistent message pairing
The message history has a role: 'tool' message that doesn't correspond to any preceding tool_calls message, possibly due to:

  • Incomplete truncation logic
  • Race condition in message processing
  • Message deduplication removing the tool_calls parent

Suggested Fix

Option 1: Smarter Truncation

# Before truncating, ensure we don't cut in the middle of a tool/tool_calls pair
def truncate_history(messages, max_tokens):
    # Find safe truncation points (before tool_calls messages)
    safe_points = [i for i, m in enumerate(messages) 
                   if m.get('role') == 'assistant' and 'tool_calls' in m]
    # Truncate before the oldest safe point that leaves room
    ...

Option 2: Validate Message Chain Before API Call

def validate_message_chain(messages):
    tool_calls_seen = set()
    for msg in messages:
        if msg.get('role') == 'tool':
            # Check if this tool message has a valid parent
            if msg.get('tool_call_id') not in tool_calls_seen:
                raise ValueError("Tool message without preceding tool_calls")
        if 'tool_calls' in msg:
            for tc in msg['tool_calls']:
                tool_calls_seen.add(tc.get('id'))

Option 3: Automatic Repair

def repair_message_history(messages):
    # Remove orphan tool messages
    valid_tool_ids = set()
    for msg in messages:
        if 'tool_calls' in msg:
            for tc in msg['tool_calls']:
                valid_tool_ids.add(tc.get('id'))
    
    return [m for m in messages 
            if m.get('role') != 'tool' or m.get('tool_call_id') in valid_tool_ids]

Affected Components

  • LLM caller / message history management
  • Conversation truncation logic

Labels

  • bug
  • llm
  • conversation-management

Priority

High - This blocks users from having longer conversations

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions