@@ -46,6 +46,19 @@ async def _arun(self, input_text: str = "") -> str:
4646 return f"Async wrapped mock result: { input_text } "
4747
4848
49+ class MockFailingTool (BaseTool ):
50+ """Mock tool that always fails for testing error handling."""
51+
52+ name : str = "mock_failing_tool"
53+ description : str = "A mock tool that fails for testing"
54+
55+ def _run (self , input_text : str = "" ) -> str :
56+ raise ValueError (f"Tool execution failed: { input_text } " )
57+
58+ async def _arun (self , input_text : str = "" ) -> str :
59+ raise ValueError (f"Async tool execution failed: { input_text } " )
60+
61+
4962class FilteredState (BaseModel ):
5063 """Mock filtered state model for testing wrappers."""
5164
@@ -283,6 +296,96 @@ def invalid_wrapper(
283296 AgentRuntimeErrorCode .TOOL_INVALID_WRAPPER_STATE
284297 )
285298
299+ def test_tool_error_propagates_when_handle_errors_false (self , mock_state ):
300+ """Test that tool errors propagate when handle_tool_errors=False."""
301+ failing_tool = MockFailingTool ()
302+ tool_call = {
303+ "name" : "mock_failing_tool" ,
304+ "args" : {"input_text" : "test input" },
305+ "id" : "test_call_id" ,
306+ }
307+ ai_message = AIMessage (content = "Using tool" , tool_calls = [tool_call ])
308+ state = MockState (messages = [ai_message ])
309+
310+ node = UiPathToolNode (failing_tool , handle_tool_errors = False )
311+
312+ with pytest .raises (ValueError ) as exc_info :
313+ node ._func (state ) # type: ignore[arg-type]
314+
315+ assert "Tool execution failed: test input" in str (exc_info .value )
316+
317+ async def test_async_tool_error_propagates_when_handle_errors_false (self ):
318+ """Test that async tool errors propagate when handle_tool_errors=False."""
319+ failing_tool = MockFailingTool ()
320+ tool_call = {
321+ "name" : "mock_failing_tool" ,
322+ "args" : {"input_text" : "test input" },
323+ "id" : "test_call_id" ,
324+ }
325+ ai_message = AIMessage (content = "Using tool" , tool_calls = [tool_call ])
326+ state = MockState (messages = [ai_message ])
327+
328+ node = UiPathToolNode (failing_tool , handle_tool_errors = False )
329+
330+ with pytest .raises (ValueError ) as exc_info :
331+ await node ._afunc (state ) # type: ignore[arg-type]
332+
333+ assert "Async tool execution failed: test input" in str (exc_info .value )
334+
335+ def test_tool_error_captured_when_handle_errors_true (self ):
336+ """Test that tool errors are captured as error ToolMessages when handle_tool_errors=True."""
337+ failing_tool = MockFailingTool ()
338+ tool_call = {
339+ "name" : "mock_failing_tool" ,
340+ "args" : {"input_text" : "test input" },
341+ "id" : "test_call_id" ,
342+ }
343+ ai_message = AIMessage (content = "Using tool" , tool_calls = [tool_call ])
344+ state = MockState (messages = [ai_message ])
345+
346+ node = UiPathToolNode (failing_tool , handle_tool_errors = True )
347+
348+ result = node ._func (state ) # type: ignore[arg-type]
349+
350+ assert result is not None
351+ assert isinstance (result , dict )
352+ assert "messages" in result
353+ assert len (result ["messages" ]) == 1
354+
355+ tool_message = result ["messages" ][0 ]
356+ assert isinstance (tool_message , ToolMessage )
357+ assert tool_message .name == "mock_failing_tool"
358+ assert tool_message .tool_call_id == "test_call_id"
359+ assert tool_message .status == "error"
360+ assert "Tool execution failed: test input" in tool_message .content
361+
362+ async def test_async_tool_error_captured_when_handle_errors_true (self ):
363+ """Test that async tool errors are captured as error ToolMessages when handle_tool_errors=True."""
364+ failing_tool = MockFailingTool ()
365+ tool_call = {
366+ "name" : "mock_failing_tool" ,
367+ "args" : {"input_text" : "test input" },
368+ "id" : "test_call_id" ,
369+ }
370+ ai_message = AIMessage (content = "Using tool" , tool_calls = [tool_call ])
371+ state = MockState (messages = [ai_message ])
372+
373+ node = UiPathToolNode (failing_tool , handle_tool_errors = True )
374+
375+ result = await node ._afunc (state ) # type: ignore[arg-type]
376+
377+ assert result is not None
378+ assert isinstance (result , dict )
379+ assert "messages" in result
380+ assert len (result ["messages" ]) == 1
381+
382+ tool_message = result ["messages" ][0 ]
383+ assert isinstance (tool_message , ToolMessage )
384+ assert tool_message .name == "mock_failing_tool"
385+ assert tool_message .tool_call_id == "test_call_id"
386+ assert tool_message .status == "error"
387+ assert "Async tool execution failed: test input" in tool_message .content
388+
286389
287390class TestToolWrapperMixin :
288391 """Test cases for ToolWrapperMixin class."""
@@ -354,3 +457,28 @@ def test_create_tool_node_empty_tools(self):
354457 result = create_tool_node ([])
355458
356459 assert result == {}
460+
461+ def test_create_tool_node_with_handle_errors_false (self ):
462+ """Test creating tool nodes with handle_tool_errors=False."""
463+ tools = [MockTool (name = "mock_tool_1" )]
464+
465+ result = create_tool_node (tools , handle_tool_errors = False )
466+
467+ assert len (result ) == 1
468+ assert "mock_tool_1" in result
469+ node = result ["mock_tool_1" ]
470+ assert isinstance (node , UiPathToolNode )
471+ assert node .handle_tool_errors is False
472+
473+ def test_create_tool_node_with_handle_errors_true (self ):
474+ """Test creating tool nodes with handle_tool_errors=True."""
475+ tools = [MockTool (name = "mock_tool_1" ), MockTool (name = "mock_tool_2" )]
476+
477+ result = create_tool_node (tools , handle_tool_errors = True )
478+
479+ assert len (result ) == 2
480+ for tool_name in ["mock_tool_1" , "mock_tool_2" ]:
481+ assert tool_name in result
482+ node = result [tool_name ]
483+ assert isinstance (node , UiPathToolNode )
484+ assert node .handle_tool_errors is True
0 commit comments