diff --git a/core/cli/main.py b/core/cli/main.py index 0ea3cb42..ca1359bf 100644 --- a/core/cli/main.py +++ b/core/cli/main.py @@ -82,37 +82,36 @@ async def run_project(sm: StateManager, ui: UIBase, args) -> bool: telemetry.set("end_result", "interrupt") await sm.rollback() except APIError as err: - log.warning(f"LLM API error occurred: {err.message}") - capture_exception(err) - await ui.send_message( - f"Stopping Pythagora due to an error while calling the LLM API: {err.message}", - source=pythagora_source, - ) + log.warning(f"an LLM API error occurred: {err.message}") + await send_error(ui, "error while calling the LLM API", err) telemetry.set("end_result", "failure:api-error") await sm.rollback() except CustomAssertionError as err: - log.warning(f"Anthropic assertion error occurred: {str(err)}") - capture_exception(err) - await ui.send_message( - f"Stopping Pythagora due to an error inside Anthropic SDK. {str(err)}", - source=pythagora_source, - ) + log.warning(f"an Anthropic assertion error occurred: {str(err)}") + await send_error(ui, "error inside Anthropic SDK", err) telemetry.set("end_result", "failure:assertion-error") await sm.rollback() except Exception as err: log.error(f"Uncaught exception: {err}", exc_info=True) - capture_exception(err) + await send_error(ui, "an error", err) - stack_trace = telemetry.record_crash(err) + telemetry.record_crash(err) await sm.rollback() - await ui.send_message( - f"Stopping Pythagora due to error:\n\n{stack_trace}", - source=pythagora_source, - ) return success +async def send_error(ui: UIBase, error_source: str, err: Exception): + await ui.send_fatal_error( + f"Stopping Pythagora due to {error_source}:\n\n{err}", + source=pythagora_source, + extra_info={ + "fatal_error": True, + }, + ) + capture_exception(err) + + async def start_new_project(sm: StateManager, ui: UIBase, args: Namespace = None) -> bool: """ Start a new project. @@ -368,7 +367,7 @@ async def async_main( success = await run_pythagora_session(sm, ui, args) except Exception as err: log.error(f"Uncaught exception in main session: {err}", exc_info=True) - capture_exception(err) + await send_error(ui, "an error", err) raise finally: await cleanup(ui) diff --git a/core/ui/base.py b/core/ui/base.py index cdea2997..adcbaa2d 100644 --- a/core/ui/base.py +++ b/core/ui/base.py @@ -516,6 +516,15 @@ class UIBase: """ raise NotImplementedError() + async def send_fatal_error( + self, + message: str, + extra_info: Optional[dict] = None, + source: Optional[UISource] = None, + project_state_id: Optional[str] = None, + ): + pass + async def send_front_logs_headers( self, project_state_id: str, diff --git a/core/ui/console.py b/core/ui/console.py index 2d8c7804..5dc5a373 100644 --- a/core/ui/console.py +++ b/core/ui/console.py @@ -240,6 +240,15 @@ class PlainConsoleUI(UIBase): ): pass + async def send_fatal_error( + self, + message: str, + extra_info: Optional[dict] = None, + source: Optional[UISource] = None, + project_state_id: Optional[str] = None, + ): + pass + async def send_front_logs_headers( self, project_state_id: str, diff --git a/core/ui/ipc_client.py b/core/ui/ipc_client.py index 0fb4a66b..962f4fb9 100644 --- a/core/ui/ipc_client.py +++ b/core/ui/ipc_client.py @@ -68,6 +68,7 @@ class MessageType(str, Enum): LOAD_FRONT_LOGS = "loadFrontLogs" FRONT_LOGS_HEADERS = "frontLogsHeaders" CLEAR_MAIN_LOGS = "clearMainLogs" + FATAL_ERROR = "fatalError" class Message(BaseModel): @@ -621,6 +622,21 @@ class IPCClientUI(UIBase): await self._send(MessageType.BACK_LOGS, content={"items": items}) + async def send_fatal_error( + self, + message: str, + extra_info: Optional[dict] = None, + source: Optional[UISource] = None, + project_state_id: Optional[str] = None, + ): + await self._send( + MessageType.FATAL_ERROR, + content=message, + category=source.type_name if source else None, + project_state_id=project_state_id, + extra_info=extra_info, + ) + async def send_front_logs_headers( self, project_state_id: str, diff --git a/core/ui/virtual.py b/core/ui/virtual.py index 33243d36..ca94b241 100644 --- a/core/ui/virtual.py +++ b/core/ui/virtual.py @@ -228,6 +228,15 @@ class VirtualUI(UIBase): ): pass + async def send_fatal_error( + self, + message: str, + extra_info: Optional[dict] = None, + source: Optional[UISource] = None, + project_state_id: Optional[str] = None, + ): + pass + async def send_front_logs_headers( self, project_state_id: str, diff --git a/tests/cli/test_cli.py b/tests/cli/test_cli.py index 7d24b69a..a5096225 100644 --- a/tests/cli/test_cli.py +++ b/tests/cli/test_cli.py @@ -342,7 +342,7 @@ async def test_main_handles_crash(mock_Orchestrator, tmp_path): mock_response = MagicMock(text="test", cancelled=False) mock_response.button = "test_project_type" # Set a string value for project_type ui.ask_question = AsyncMock(return_value=mock_response) - ui.send_message = AsyncMock() + ui.send_fatal_error = AsyncMock() mock_orca = mock_Orchestrator.return_value mock_orca.run = AsyncMock(side_effect=RuntimeError("test error")) @@ -350,5 +350,5 @@ async def test_main_handles_crash(mock_Orchestrator, tmp_path): success = await async_main(ui, db, args) assert success is False - ui.send_message.assert_called_once() - assert "test error" in ui.send_message.call_args[0][0] + ui.send_fatal_error.assert_called_once() + assert "test error" in ui.send_fatal_error.call_args[0][0]