diff --git a/docs/07_tools.md b/docs/07_tools.md index c87b5419..ffb8552c 100644 --- a/docs/07_tools.md +++ b/docs/07_tools.md @@ -251,6 +251,46 @@ class GreetingTool(Tool): return f"{base_greeting}, {name}! How are you today?" ``` +### Error Handling in Tools + +When a tool raises an exception, the agent distinguishes between **fixable** and **unfixable** errors: + +- **Fixable errors** (regular exceptions): The error message is returned to the model, which can auto-correct and retry with different parameters. This is the default behavior for any `Exception` raised inside `__call__`. +- **Unfixable errors** (`AutomationError`): The error propagates immediately to the caller, terminating the agent's execution. Use this for errors where retrying cannot help (e.g., missing credentials, unreachable services, invalid environment state). + +```python +from askui import AutomationError +from askui.models.shared.tools import Tool + + +class DatabaseQueryTool(Tool): + """Queries a database.""" + + def __init__(self): + super().__init__( + name="database_query", + description="Executes a read-only SQL query", + input_schema={ + "type": "object", + "properties": { + "query": {"type": "string", "description": "SQL query to execute"} + }, + "required": ["query"], + }, + ) + + def __call__(self, query: str) -> str: + if not self._is_connected(): + # Unfixable: no amount of retrying will help + raise AutomationError("Database connection is not available") + + if "DROP" in query.upper(): + # Fixable: the agent can rephrase the query + raise ValueError("Only SELECT queries are allowed") + + return self._execute(query) +``` + To use this tool with the ComputerAgent, you can run ```python from askui import ComputerAgent diff --git a/src/askui/__init__.py b/src/askui/__init__.py index 1f498108..c5349466 100644 --- a/src/askui/__init__.py +++ b/src/askui/__init__.py @@ -31,6 +31,7 @@ ToolUseBlockParam, UrlImageSourceParam, ) +from .models.exceptions import AutomationError from .models.shared.settings import ( DEFAULT_GET_RESOLUTION, DEFAULT_LOCATE_RESOLUTION, @@ -69,6 +70,7 @@ __all__ = [ "Agent", + "AutomationError", "ComputerAgent", "VisionAgent", "AgentSettings", diff --git a/src/askui/agent_base.py b/src/askui/agent_base.py index e3554f28..5775a9c0 100644 --- a/src/askui/agent_base.py +++ b/src/askui/agent_base.py @@ -159,6 +159,8 @@ def act( None Raises: + AutomationError: If a tool raises an unfixable error that cannot be + auto-corrected by the agent. MaxTokensExceededError: If the model reaches the maximum token limit defined in the agent settings. ModelRefusalError: If the model refuses to process the request. diff --git a/src/askui/models/shared/conversation.py b/src/askui/models/shared/conversation.py index c5277282..04f35371 100644 --- a/src/askui/models/shared/conversation.py +++ b/src/askui/models/shared/conversation.py @@ -177,10 +177,11 @@ def execute_conversation( self._setup_control_loop(messages, tools, settings, reporters) self._on_conversation_start() - self._execute_control_loop() - self._on_conversation_end() - - self._teardown_control_loop() + try: + self._execute_control_loop() + finally: + self._on_conversation_end() + self._teardown_control_loop() @tracer.start_as_current_span("_setup_control_loop") def _setup_control_loop( diff --git a/src/askui/models/shared/tools.py b/src/askui/models/shared/tools.py index 6a837ae0..74912911 100644 --- a/src/askui/models/shared/tools.py +++ b/src/askui/models/shared/tools.py @@ -20,6 +20,7 @@ from pydantic import BaseModel, ConfigDict, Field, PrivateAttr from typing_extensions import Self +from askui.models.exceptions import AutomationError from askui.models.shared.agent_message_param import ( Base64ImageSourceParam, CacheControlEphemeralParam, @@ -384,10 +385,8 @@ def is_agent_os_initialized(self) -> bool: return self._agent_os is not None -class AgentException(Exception): - """ - Exception raised by the agent. - """ +class AgentError(Exception): + """Unfixable error raised by the agent that terminates execution immediately.""" def __init__(self, message: str): self.message = message @@ -647,7 +646,7 @@ def _run_regular_tool( content=_convert_to_content(tool_result), tool_use_id=tool_use_block_param.id, ) - except AgentException: + except (AgentError, AutomationError): raise except Exception as e: # noqa: BLE001 error_message = getattr(e, "message", str(e)) @@ -691,6 +690,8 @@ def _run_mcp_tool( content=_convert_to_content(result), tool_use_id=tool_use_block_param.id, ) + except AutomationError: + raise except Exception as e: # noqa: BLE001 logger.warning( "MCP tool failed", diff --git a/src/askui/tools/exception_tool.py b/src/askui/tools/exception_tool.py index 2cdb7c43..96bb10c0 100644 --- a/src/askui/tools/exception_tool.py +++ b/src/askui/tools/exception_tool.py @@ -1,4 +1,4 @@ -from askui.models.shared.tools import AgentException, Tool +from askui.models.shared.tools import AgentError, Tool class ExceptionTool(Tool): @@ -28,4 +28,4 @@ def __init__(self) -> None: ) def __call__(self, text: str) -> None: - raise AgentException(text) + raise AgentError(text)