diff --git a/src/askui/tools/askui/askui_ui_controller_grpc/desktop_agent_os_error.py b/src/askui/tools/askui/askui_ui_controller_grpc/desktop_agent_os_error.py index 14a66aba..5b482aa0 100644 --- a/src/askui/tools/askui/askui_ui_controller_grpc/desktop_agent_os_error.py +++ b/src/askui/tools/askui/askui_ui_controller_grpc/desktop_agent_os_error.py @@ -1,5 +1,9 @@ -class DesktopAgentOsError(BaseException): +class DesktopAgentOsError(Exception): """Base class for Desktop Agent OS errors. This error is raised when an error occurs in the Desktop Agent OS. + + Inherits from `Exception` (not `BaseException`) so that the standard + `except Exception` handlers in the tool-calling loop can catch it and + surface it to the agent as a tool error result instead of crashing. """ diff --git a/tests/unit/models/shared/test_tool_error_handling.py b/tests/unit/models/shared/test_tool_error_handling.py new file mode 100644 index 00000000..e02cd71b --- /dev/null +++ b/tests/unit/models/shared/test_tool_error_handling.py @@ -0,0 +1,60 @@ +"""Tests that tool failures are surfaced to the agent instead of crashing. + +When a tool raises, the tool-calling loop is expected to catch the error and +return a `ToolResultBlockParam` with `is_error=True` so the agent can react to +it. This only works if the raised exception derives from `Exception`; a +`BaseException` subclass would slip past the `except Exception` handler and +crash the run instead. `DesktopAgentOsError` (raised e.g. when reading a remote +file/directory that does not exist) must therefore behave like a regular +`Exception`. +""" + +from askui.models.shared.agent_message_param import ( + ToolResultBlockParam, + ToolUseBlockParam, +) +from askui.models.shared.tools import Tool, ToolCollection +from askui.tools.askui.askui_ui_controller_grpc.desktop_agent_os_error import ( + DesktopAgentOsError, +) + + +class _RaisingTool(Tool): + """A tool whose `__call__` always raises a `DesktopAgentOsError`.""" + + def __init__(self) -> None: + super().__init__( + name="raising_tool", + description="Always raises a DesktopAgentOsError.", + ) + + def __call__(self) -> str: + raise DesktopAgentOsError(self._error_message) + + _error_message = ( + "directory_iterator::directory_iterator: The system cannot find the " + 'path specified.: "FrontEnd\\Traces"' + ) + + +class TestDesktopAgentOsErrorHandling: + def test_desktop_agent_os_error_is_an_exception(self) -> None: + assert issubclass(DesktopAgentOsError, Exception) + + def test_raising_tool_returns_error_result_instead_of_crashing(self) -> None: + tool = _RaisingTool() + collection = ToolCollection(tools=[tool]) + tool_use = ToolUseBlockParam( + id="tool_use_1", + input={}, + name=tool.name, + ) + + results = collection.run([tool_use]) + + assert len(results) == 1 + result = results[0] + assert isinstance(result, ToolResultBlockParam) + assert result.is_error is True + assert result.tool_use_id == "tool_use_1" + assert "FrontEnd\\Traces" in str(result.content)