From 7f89900fb1de04af66d2af663e5c87d4ab8fd117 Mon Sep 17 00:00:00 2001 From: mdnaimul22 Date: Thu, 9 Apr 2026 19:24:47 +0000 Subject: [PATCH 1/2] Fix inconsistent tool validation and prevent system crashes on misformatted requests (Fixes #1349) --- agent.py | 95 ++++++++++++++++++++++++++++++++------------------------ 1 file changed, 54 insertions(+), 41 deletions(-) diff --git a/agent.py b/agent.py index db7626b0eb..c98d5b2151 100644 --- a/agent.py +++ b/agent.py @@ -592,43 +592,49 @@ async def prepare_prompt(self, loop_data: LoopData) -> list[BaseMessage]: @extension.extensible async def handle_exception(self, location: str, exception: Exception): - if exception: - raise exception # exception handling is done by extensions + if not exception: + return + + if isinstance(exception, errors.HandledException): + raise exception + elif isinstance(exception, asyncio.CancelledError): + PrintStyle(font_color="white", background_color="red", padding=True).print( + f"Context {self.context.id} terminated during message loop" + ) + raise errors.HandledException(exception) + elif isinstance(exception, errors.InterventionException): + # Intervention exceptions just skip the rest of the message loop iteration + return + elif isinstance(exception, errors.RepairableException): + # Repairable exceptions should be caught and added to chat history + # so the model can fix its mistake. + error_msg = str(exception) + PrintStyle(font_color="yellow", padding=True).print(f"Repairable Error: {error_msg}") + + # Create a log entry and warning message + wmsg = self.hist_add_warning(error_msg) + self.context.log.log( + type="warning", + content=f"{self.agent_name}: {error_msg}", + id=wmsg.id + ) + return + + else: + error_text = errors.error_text(exception) + error_message = errors.format_error(exception) + + # Mask secrets in error messages + PrintStyle(font_color="red", padding=True).print(error_message) + self.context.log.log( + type="error", + content=error_message, + ) + PrintStyle(font_color="red", padding=True).print( + f"{self.agent_name}: {error_text}" + ) - # exception_data = {"exception": exception} - # await self.call_extensions( - # "message_loop_exception", exception_data=exception_data - # ) - - # # If extensions cleared the exception, continue. - # if not exception_data.get("exception"): - # return - - # # Backwards-compatible fallback (should normally be handled by _90 extension). - # exception = exception_data["exception"] - # if isinstance(exception, HandledException): - # raise exception - # elif isinstance(exception, asyncio.CancelledError): - # PrintStyle(font_color="white", background_color="red", padding=True).print( - # f"Context {self.context.id} terminated during message loop" - # ) - # raise HandledException(exception) - - # else: - # error_text = errors.error_text(exception) - # error_message = errors.format_error(exception) - - # # Mask secrets in error messages - # PrintStyle(font_color="red", padding=True).print(error_message) - # self.context.log.log( - # type="error", - # content=error_message, - # ) - # PrintStyle(font_color="red", padding=True).print( - # f"{self.agent_name}: {error_text}" - # ) - - # raise HandledException(exception) # Re-raise the exception to kill the loop + raise errors.HandledException(exception) # Re-raise the exception to kill the loop @extension.extensible async def get_system_prompt(self, loop_data: LoopData) -> list[str]: @@ -975,11 +981,18 @@ async def process_tools(self, msg: str): @extension.extensible async def validate_tool_request(self, tool_request: Any): if not isinstance(tool_request, dict): - raise ValueError("Tool request must be a dictionary") - if not tool_request.get("tool_name") or not isinstance(tool_request.get("tool_name"), str): - raise ValueError("Tool request must have a tool_name (type string) field") - if not tool_request.get("tool_args") or not isinstance(tool_request.get("tool_args"), dict): - raise ValueError("Tool request must have a tool_args (type dictionary) field") + raise RepairableException("Tool request must be a dictionary") + + # Support aliases 'tool_name' and 'tool' + tool_name = tool_request.get("tool_name", tool_request.get("tool")) + if not tool_name or not isinstance(tool_name, str): + raise RepairableException("Tool request must have a 'tool_name' (type string) field") + + # Support aliases 'tool_args' and 'args' + tool_args = tool_request.get("tool_args", tool_request.get("args")) + # Allow missing args (None) or empty dict, but if present must be a dict + if tool_args is not None and not isinstance(tool_args, dict): + raise RepairableException("Tool request 'tool_args' field must be a dictionary") From 35bd59ed8c9b20a2b0d7acef43040962ee112533 Mon Sep 17 00:00:00 2001 From: mdnaimul22 Date: Thu, 9 Apr 2026 19:49:58 +0000 Subject: [PATCH 2/2] Refactor tool validation feedback to provide standardized, actionable instructions and usage examples --- agent.py | 24 +++++++++++++++++++++--- 1 file changed, 21 insertions(+), 3 deletions(-) diff --git a/agent.py b/agent.py index c98d5b2151..930aa70910 100644 --- a/agent.py +++ b/agent.py @@ -981,18 +981,36 @@ async def process_tools(self, msg: str): @extension.extensible async def validate_tool_request(self, tool_request: Any): if not isinstance(tool_request, dict): - raise RepairableException("Tool request must be a dictionary") + received_type = type(tool_request).__name__ + raise RepairableException( + f"Tool request must be a dictionary (JSON object). Received: {received_type}. " + "Please ensure your tool call is wrapped in a valid JSON object. " + "Example: {'tool_name': 'terminal', 'tool_args': {'command': 'ls'}}" + ) # Support aliases 'tool_name' and 'tool' tool_name = tool_request.get("tool_name", tool_request.get("tool")) if not tool_name or not isinstance(tool_name, str): - raise RepairableException("Tool request must have a 'tool_name' (type string) field") + received_keys = list(tool_request.keys()) + raise RepairableException( + f"Tool request is missing a 'tool_name' or 'tool' string field. " + "Both are supported, but one is required. " + f"Received keys: {received_keys}. " + "Correct format: {'tool_name': 'terminal', 'tool_args': {'command': 'ls'}} " + "OR {'tool': 'python', 'args': {'code': 'print(1)'}}" + ) # Support aliases 'tool_args' and 'args' tool_args = tool_request.get("tool_args", tool_request.get("args")) # Allow missing args (None) or empty dict, but if present must be a dict if tool_args is not None and not isinstance(tool_args, dict): - raise RepairableException("Tool request 'tool_args' field must be a dictionary") + received_type = type(tool_args).__name__ + raise RepairableException( + f"The 'tool_args' (or 'args') field must be a dictionary of parameters. " + f"Received: {received_type}. " + "Correct format: {'tool_name': 'terminal', 'tool_args': {'command': 'ls'}} " + "OR {'tool': 'python', 'args': {'code': 'print(1)'}}" + )