From a920479fc9d5aa8797e9cf548f9322652fea1f29 Mon Sep 17 00:00:00 2001 From: David Billiot Date: Fri, 30 Oct 2026 16:06:05 -0500 Subject: [PATCH 1/2] fix(react_agent): raise on truncated LLM output (finish_reason='length') Fixes #2518 Signed-off-by: David Billiot --- .../nat/plugins/langchain/agent/react_agent/agent.py | 5 +++++ .../nvidia_nat_langchain/tests/agent/test_react.py | 12 ++++++++++++ 2 files changed, 17 insertions(+) diff --git a/packages/nvidia_nat_langchain/src/nat/plugins/langchain/agent/react_agent/agent.py b/packages/nvidia_nat_langchain/src/nat/plugins/langchain/agent/react_agent/agent.py index 9375ca45c4..14f809956a 100644 --- a/packages/nvidia_nat_langchain/src/nat/plugins/langchain/agent/react_agent/agent.py +++ b/packages/nvidia_nat_langchain/src/nat/plugins/langchain/agent/react_agent/agent.py @@ -38,6 +38,7 @@ from pydantic import BaseModel from pydantic import Field +from nat.llm.utils.constants import LLMFinishReason from nat.plugins.langchain.agent.base import AGENT_CALL_LOG_MESSAGE from nat.plugins.langchain.agent.base import AGENT_LOG_PREFIX from nat.plugins.langchain.agent.base import INPUT_SCHEMA_MESSAGE @@ -309,6 +310,10 @@ async def agent_node(self, state: ReActGraphState, config: RunnableConfig | None logger.debug("%s The agent's scratchpad (with tool result) was:\n%s", AGENT_LOG_PREFIX, agent_scratchpad) + # A truncated reply would be parsed as a complete answer or tool input (#2518) + if getattr(output_message, "response_metadata", {}).get("finish_reason") == LLMFinishReason.LENGTH: + raise RuntimeError("LLM output truncated (finish_reason='length'). " + f"Truncated output: {str(output_message.content)[:500]!r}") try: # check if the agent has the final answer yet logger.debug("%s Successfully obtained agent response. Parsing agent's response", AGENT_LOG_PREFIX) diff --git a/packages/nvidia_nat_langchain/tests/agent/test_react.py b/packages/nvidia_nat_langchain/tests/agent/test_react.py index 9fca473fc4..d3c2422f16 100644 --- a/packages/nvidia_nat_langchain/tests/agent/test_react.py +++ b/packages/nvidia_nat_langchain/tests/agent/test_react.py @@ -2442,3 +2442,15 @@ async def on_llm_end(self, *args: typing.Any, **kwargs: typing.Any) -> None: config={"callbacks": [CountLLMEnds()]}) assert CountLLMEnds.calls == 2 assert response.usage.total_tokens == 300 + + +async def test_agent_node_raises_on_truncated_output(mock_react_agent_no_raise): + from unittest.mock import AsyncMock + from unittest.mock import patch + + truncated = AIMessage(content="Thought: I know this.\nFinal Answer: LangSmith is a platform for", + response_metadata={"finish_reason": "length"}) + with patch.object(mock_react_agent_no_raise, '_stream_llm', new_callable=AsyncMock) as mock_stream_llm: + mock_stream_llm.return_value = truncated + with pytest.raises(RuntimeError, match="finish_reason='length'"): + await mock_react_agent_no_raise.agent_node(ReActGraphState(messages=[HumanMessage(content="hi")])) From 5c23c4ea2ae1c71721b09edc1beab20ee21d6fb5 Mon Sep 17 00:00:00 2001 From: David Billiot Date: Fri, 30 Oct 2026 16:15:17 -0500 Subject: [PATCH 2/2] test(react): cover truncation in the agentic cycle Signed-off-by: David Billiot --- .../tests/agent/test_react.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/packages/nvidia_nat_langchain/tests/agent/test_react.py b/packages/nvidia_nat_langchain/tests/agent/test_react.py index d3c2422f16..52d6921b41 100644 --- a/packages/nvidia_nat_langchain/tests/agent/test_react.py +++ b/packages/nvidia_nat_langchain/tests/agent/test_react.py @@ -2454,3 +2454,19 @@ async def test_agent_node_raises_on_truncated_output(mock_react_agent_no_raise): mock_stream_llm.return_value = truncated with pytest.raises(RuntimeError, match="finish_reason='length'"): await mock_react_agent_no_raise.agent_node(ReActGraphState(messages=[HumanMessage(content="hi")])) + + +async def test_agent_node_raises_on_truncated_output_in_agentic_cycle(mock_react_agent_no_raise): + from unittest.mock import AsyncMock + from unittest.mock import patch + + truncated = AIMessage(content="Thought: search more.\nAction: Tool A\nAction Input: {\"query\": \"Lang", + response_metadata={"finish_reason": "length"}) + state = ReActGraphState(messages=[HumanMessage(content="hi")], + agent_scratchpad=[AgentAction(tool="Tool A", tool_input="hello", log="test")], + tool_responses=[ToolMessage(name="Tool A", tool_call_id="Tool A", content="result")]) + with patch.object(mock_react_agent_no_raise, '_stream_llm', new_callable=AsyncMock) as mock_stream_llm: + mock_stream_llm.return_value = truncated + with pytest.raises(RuntimeError, match="finish_reason='length'"): + await mock_react_agent_no_raise.agent_node(state) + assert "agent_scratchpad" in mock_stream_llm.call_args.args[1]