diff --git a/src/serena/agent.py b/src/serena/agent.py index 32daf0ef..83f9694b 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -12,6 +12,7 @@ from typing import TYPE_CHECKING, Optional, TypeVar from sensai.util import logging from sensai.util.logging import LogTime +from sensai.util.string import TextBuilder from interprompt.jinja_template import JinjaTemplate from serena import serena_version @@ -23,10 +24,11 @@ from serena.config.serena_config import ( NamedToolInclusionDefinition, RegisteredProject, SerenaConfig, + SerenaPaths, ToolInclusionDefinition, ) from serena.dashboard import SerenaDashboardAPI -from serena.ls_manager import LanguageServerManager +from serena.ls_manager import LanguageServerManager, LanguageServerManagerInitialisationError from serena.project import Project from serena.prompt_factory import SerenaPromptFactory from serena.task_executor import TaskExecutor @@ -252,6 +254,7 @@ class SerenaAgent: # project-specific instances, which will be initialized upon project activation self._active_project: Project | None = None + self._lsp_init_error: LanguageServerManagerInitialisationError | None = None # determine registered project to be activated (if any) registered_project_to_activate: RegisteredProject | None = ( @@ -473,13 +476,26 @@ class SerenaAgent: def get_language_server_manager_or_raise(self) -> LanguageServerManager: language_server_manager = self.get_language_server_manager() if language_server_manager is None: - raise Exception( - "The language server manager is not initialized, indicating a problem during project activation. " - "Inform the user, telling them to inspect Serena's logs in order to determine the issue. " - "IMPORTANT: Wait for further instructions before you continue!" + msg = TextBuilder("The language server manager is not initialized, indicating a problem during project initialisation.") + if self._lsp_init_error is not None: + msg.with_text(str(self._lsp_init_error)) + msg.with_text("For details, please check the logs. " + self.get_log_inspection_instructions()) + msg.with_text( + "IMPORTANT: Stop, do not attempt workarounds. Inform the user and wait for further instructions before you continue!" ) + raise Exception(msg.build()) return language_server_manager + def get_log_inspection_instructions(self) -> str: + if self.serena_config.web_dashboard: + return f"Live logs can be inspected via the dashboard at {self.get_dashboard_url()}" + else: + log_path = SerenaPaths().last_returned_log_file_path + if log_path is not None: + return f"Find the current log file here: f{log_path}" + else: + return "Unfortunately, logs are not available. We recommend enabling the web dashboard/logging in general." + def get_context(self) -> SerenaAgentContext: return self._context @@ -816,12 +832,17 @@ class SerenaAgent: ls_timeout = tool_timeout - 5 # the LS timeout is for a single call, it should be smaller than the tool timeout # instantiate and start the necessary language servers - self.get_active_project_or_raise().create_language_server_manager( - log_level=self.serena_config.log_level, - ls_timeout=ls_timeout, - trace_lsp_communication=self.serena_config.trace_lsp_communication, - ls_specific_settings=self.serena_config.ls_specific_settings, - ) + try: + self._lsp_init_error = None + self.get_active_project_or_raise().create_language_server_manager( + log_level=self.serena_config.log_level, + ls_timeout=ls_timeout, + trace_lsp_communication=self.serena_config.trace_lsp_communication, + ls_specific_settings=self.serena_config.ls_specific_settings, + ) + except LanguageServerManagerInitialisationError as e: + self._lsp_init_error = e + raise def add_language(self, language: Language) -> None: """ diff --git a/src/serena/config/serena_config.py b/src/serena/config/serena_config.py index b286c63b..432e8a65 100644 --- a/src/serena/config/serena_config.py +++ b/src/serena/config/serena_config.py @@ -85,6 +85,11 @@ class SerenaPaths: """ file containing the ID of the last read news snippet """ + self.last_returned_log_file_path: str | None = None + """ + the path to the last log file returned by `get_next_log_file_path`. If this is not None, the logs + are currently being written to this file + """ def get_next_log_file_path(self, prefix: str) -> str: """ @@ -93,7 +98,8 @@ class SerenaPaths: """ log_dir = os.path.join(self.serena_user_home_dir, "logs", datetime.now().strftime("%Y-%m-%d")) os.makedirs(log_dir, exist_ok=True) - return os.path.join(log_dir, prefix + "_" + datetime_tag() + ".txt") + self.last_returned_log_file_path = os.path.join(log_dir, prefix + "_" + datetime_tag() + ".txt") + return self.last_returned_log_file_path # TODO: Paths from constants.py should be moved here diff --git a/src/serena/ls_manager.py b/src/serena/ls_manager.py index 1cb8f3b5..dde1f428 100644 --- a/src/serena/ls_manager.py +++ b/src/serena/ls_manager.py @@ -14,6 +14,11 @@ from solidlsp.settings import SolidLSPSettings log = logging.getLogger(__name__) +class LanguageServerManagerInitialisationError(Exception): + def __init__(self, message: str): + super().__init__(message) + + class LanguageServerFactory: def __init__( self, @@ -122,14 +127,14 @@ class LanguageServerManager: # If any server failed to start up, raise an exception and stop all started language servers. # We intentionally fail fast here. The user's intention is to work with all the specified languages, - # so if any of them is not available, it is better to fail immediately and bring the failure to the + # so if any of them is not available, it is better to make symbolic tool calls fail, bringing the issue to the # user's attention instead of silently continuing with a subset of the language servers and potentially # causing suboptimal agent behaviour. if exceptions: for ls in language_servers.values(): ls.stop() failure_messages = "\n".join([f"{lang.value}: {e}" for lang, e in exceptions.items()]) - raise Exception(f"Failed to start language servers:\n{failure_messages}") + raise LanguageServerManagerInitialisationError(f"Failed to start {len(exceptions)} language server(s):\n{failure_messages}") return LanguageServerManager(language_servers, factory)