From dcc517b187fb8c9ee5ba2584e281e41bf86e23b4 Mon Sep 17 00:00:00 2001 From: Zhou Jing <124344487+starchou6@users.noreply.github.com> Date: Wed, 10 Sep 2025 00:25:52 +0900 Subject: [PATCH] fix(core): ensure `InjectedToolCallId` always overrides LLM-generated values (#32766) --- libs/core/langchain_core/tools/base.py | 10 ++-------- libs/core/tests/unit_tests/test_tools.py | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 8 deletions(-) diff --git a/libs/core/langchain_core/tools/base.py b/libs/core/langchain_core/tools/base.py index 5f09665c798..81ba71a891a 100644 --- a/libs/core/langchain_core/tools/base.py +++ b/libs/core/langchain_core/tools/base.py @@ -659,10 +659,7 @@ class ChildTool(BaseTool): return tool_input if issubclass(input_args, BaseModel): for k, v in get_all_basemodel_annotations(input_args).items(): - if ( - _is_injected_arg_type(v, injected_type=InjectedToolCallId) - and k not in tool_input - ): + if _is_injected_arg_type(v, injected_type=InjectedToolCallId): if tool_call_id is None: msg = ( "When tool includes an InjectedToolCallId " @@ -677,10 +674,7 @@ class ChildTool(BaseTool): result_dict = result.model_dump() elif issubclass(input_args, BaseModelV1): for k, v in get_all_basemodel_annotations(input_args).items(): - if ( - _is_injected_arg_type(v, injected_type=InjectedToolCallId) - and k not in tool_input - ): + if _is_injected_arg_type(v, injected_type=InjectedToolCallId): if tool_call_id is None: msg = ( "When tool includes an InjectedToolCallId " diff --git a/libs/core/tests/unit_tests/test_tools.py b/libs/core/tests/unit_tests/test_tools.py index b296608120a..63d9c30e46d 100644 --- a/libs/core/tests/unit_tests/test_tools.py +++ b/libs/core/tests/unit_tests/test_tools.py @@ -2349,6 +2349,28 @@ def test_tool_injected_tool_call_id() -> None: ) == ToolMessage(0, tool_call_id="bar") # type: ignore[arg-type] +def test_tool_injected_tool_call_id_override_llm_generated() -> None: + """Test that InjectedToolCallId overrides LLM-generated values.""" + + @tool + def foo(x: int, tool_call_id: Annotated[str, InjectedToolCallId]) -> ToolMessage: + """Foo.""" + return ToolMessage(x, tool_call_id=tool_call_id) # type: ignore[arg-type] + + # Test that when LLM generates the tool_call_id, it gets overridden + result = foo.invoke( + { + "type": "tool_call", + "args": {"x": 0, "tool_call_id": "fake_llm_id"}, # LLM generated this + "name": "foo", + "id": "real_tool_call_id", # This should be used instead + } + ) + + # The tool should receive the real tool call ID, not the LLM-generated one + assert result == ToolMessage(0, tool_call_id="real_tool_call_id") # type: ignore[arg-type] + + def test_tool_uninjected_tool_call_id() -> None: @tool def foo(x: int, tool_call_id: str) -> ToolMessage: