Skip to content

handle_tool_errors=True does not catch asyncio.CancelledError, causing INVALID_CHAT_HISTORY #6726

@FelGel

Description

@FelGel

Description

When a tool execution is cancelled via asyncio.CancelledError, the ToolNode does not create an error ToolMessage even when handle_tool_errors=True. This leaves the message history in an invalid state where an AIMessage has tool_calls without corresponding ToolMessages.

Root Cause

asyncio.CancelledError inherits from BaseException, not Exception. The error handling in ToolNode._arun_one() uses except Exception as e:, which doesn't catch CancelledError:

# langgraph/prebuilt/tool_node.py line ~951
except GraphBubbleUp:
    raise
except Exception as e:  # CancelledError bypasses this!
    # ... handle_tool_errors logic ...

Reproduction

import asyncio
from langchain_core.messages import AIMessage, HumanMessage, ToolMessage
from langchain_core.tools import tool
from langgraph.prebuilt import ToolNode


@tool
async def slow_tool(query: str) -> str:
    """A tool that takes time to complete."""
    await asyncio.sleep(10)
    return f"Result: {query}"


async def test_cancelled_error():
    tool_node = ToolNode(tools=[slow_tool], handle_tool_errors=True)

    ai_message = AIMessage(
        content="",
        tool_calls=[{"id": "call_1", "name": "slow_tool", "args": {"query": "test"}}]
    )
    state = {"messages": [HumanMessage(content="test"), ai_message]}
    config = {"configurable": {"thread_id": "test"}}

    task = asyncio.create_task(tool_node.ainvoke(state, config))
    await asyncio.sleep(0.1)
    task.cancel()

    try:
        await task
    except asyncio.CancelledError:
        print("CancelledError raised - no ToolMessage created!")
        # State now has AIMessage with tool_calls but no ToolMessage
        # This causes INVALID_CHAT_HISTORY on next LLM call


asyncio.run(test_cancelled_error())

Output:

CancelledError raised - no ToolMessage created!

Expected Behavior

When handle_tool_errors=True and a tool is cancelled, ToolNode should catch CancelledError and return a ToolMessage with an error status, maintaining valid chat history.

Suggested Fix

Either:

  1. Catch BaseException and re-raise after creating error ToolMessage for CancelledError
  2. Explicitly catch asyncio.CancelledError before the except Exception block
except asyncio.CancelledError:
    if self._handle_tool_errors:
        return ToolMessage(
            content="Tool execution was cancelled",
            name=call["name"],
            tool_call_id=call["id"],
            status="error",
        )
    raise
except Exception as e:
    # existing handling...

Environment

  • langgraph version: 1.0.7 (also confirmed in 0.5.4)
  • Python version: 3.11

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