From e6e25d3f862e80fa9c250bed1b85f56fbf6bb02f Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Wed, 29 Jul 2026 17:23:35 +0200 Subject: [PATCH] Report project activation errors to the client in the system prompt SerenaAgent now tracks an initial/startup project activation error and appends it to the system prompt, instead of the failure being visible only in the log. This applies both to a failed activation of an explicitly given project and to a failed --project-from-cwd auto-detection. Also fix the --project-from-cwd help text, which falsely promised a fallback to the CWD Resolves #1773 --- CHANGELOG.md | 7 +++++++ src/serena/agent.py | 10 ++++++++++ src/serena/cli.py | 12 ++++++++++-- src/serena/mcp.py | 23 ++++++++++++++--------- test/serena/test_serena_agent.py | 31 +++++++++++++++++++++++++++++++ 5 files changed, 72 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f89ae818..105c4fff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,13 @@ Status of the `main` branch. Changes prior to the next official version change w - The `languages` key in project configurations was changed to `language_servers` to better reflect the actual semantics (configurations are automatically migrated) - Fix: glob matching bare `*` and `?` in non-`**` patterns matched across `/`, contradicting documented behaviour #1732 + - Project activation errors are now reported to the client in Serena's system prompt, instead of failures + being visible only in the log. This applies both to a failed activation of an explicitly given project and to a + failed `--project-from-cwd` auto-detection (#1773). + +* CLI: + - Fix: `start-mcp-server` help text for `--project-from-cwd` falsely promised a fallback to the CWD, which was + removed in v1.0.0 #1773 * Tools: - `find_symbol`: Change tool description to improve tool search results in clients that load tools dynamically diff --git a/src/serena/agent.py b/src/serena/agent.py index aed7dc65..5107b572 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -540,7 +540,9 @@ class SerenaAgent: def __init__( self, project: str | None = None, + *, project_activation_callback: Callable[[], None] | None = None, + project_activation_error: str | None = None, serena_config: SerenaConfig | None = None, context: SerenaAgentContext | None = None, modes: ModeSelectionDefinition | None = None, @@ -550,6 +552,8 @@ class SerenaAgent: :param project: the project to load immediately or None to not load any project; may be a path to the project or a name of an already registered project; :param project_activation_callback: a callback function to be called when a project is activated. + :param project_activation_error: an initial error to report back to the client/LLM pertaining to project determination/activation + in Serena's initial prompts. This is only applicable if `project` is None. :param serena_config: the Serena configuration or None to read the configuration from the default location. :param context: the context in which the agent is operating, None for default context. The context may adjust prompts, tool availability, and tool descriptions. @@ -559,6 +563,7 @@ class SerenaAgent: """ self._active_project: Project | None = None self._project_activation_callback = project_activation_callback + self._project_activation_error: str | None = project_activation_error self._gui_log_viewer: Optional["GuiLogViewer"] = None self._dashboard_manager: DashboardManager | None = None self._project_prompt_status = ProjectPromptProvisionStatus() @@ -668,6 +673,7 @@ class SerenaAgent: self.activate_project_from_path_or_name(project, update_active_modes=False, update_active_tools=False) except Exception as e: log.error(f"Error activating project '{project}' at startup: {e}", exc_info=e) + self._project_activation_error = str(e) self._update_active_modes() # determine the base toolset defining the set of exposed tools (which e.g. the MCP shall see), @@ -1003,6 +1009,8 @@ class SerenaAgent: # provide the project activation message if it hasn't yet been provided if self._active_project is not None and not self._project_prompt_status.is_project_activation_message_already_provided(session_id): system_prompt += "\n\n" + self.get_project_activation_message(session_id) + elif self._project_activation_error: + system_prompt += f"\n\nNo project is active ({self._project_activation_error})." return system_prompt @@ -1170,6 +1178,8 @@ class SerenaAgent: log.info(f"Activating {project.project_name} at {project.project_root}") + self._project_activation_error = None + # check if the project requires a different language backend than the one initialized at startup project_backend = project.project_config.language_backend if project_backend is not None and project_backend != self._language_backend: diff --git a/src/serena/cli.py b/src/serena/cli.py index c514fcb4..34a10620 100644 --- a/src/serena/cli.py +++ b/src/serena/cli.py @@ -37,6 +37,7 @@ from serena.constants import ( SERENAS_OWN_MODE_YAMLS_DIR, ) from serena.prompt_factory import SerenaPromptFactory +from serena.tools import ActivateProjectTool from serena.util.cli_util import AutoRegisteringGroup from serena.util.logging import MemoryLogHandler from solidlsp.ls_config import LanguageServerId @@ -312,7 +313,7 @@ class TopLevelCommands(AutoRegisteringGroup): "--project-from-cwd", is_flag=True, default=False, - help="Auto-detect project from current working directory (searches for .serena/project.yml or .git, falls back to CWD). Intended for CLI-based agents like Claude Code, Gemini and Codex.", + help="Auto-detect project from current working directory (nearest ancestor containing .serena/project.yml or .git). If none is found, no project is activated. Intended for CLI-based agents like Claude Code, Gemini and Codex.", ) def start_mcp_server( project: str | None, @@ -355,6 +356,7 @@ class TopLevelCommands(AutoRegisteringGroup): log.info("Storing logs in %s", log_path) # Handle --project-from-cwd flag + project_activation_error: str | None = None if project_from_cwd: if project is not None or project_file_arg is not None: raise click.UsageError("--project-from-cwd cannot be used with --project or positional project argument") @@ -362,7 +364,12 @@ class TopLevelCommands(AutoRegisteringGroup): if project is not None: log.info("Auto-detected project root: %s", project) else: - log.warning("No project root found from %s; not activating any project", os.getcwd()) + project_activation_error = ( + f"No project root found from cwd={os.getcwd()} (no .serena/project.yml or .git found); " + "no project activated. If the folder is a coding project folder to be worked on, " + f"activate the folder explicitly using the {ActivateProjectTool.get_name_from_cls()} tool." + ) + log.warning(project_activation_error) project_file = project_file_arg or project @@ -382,6 +389,7 @@ class TopLevelCommands(AutoRegisteringGroup): log_level=log_level, trace_lsp_communication=trace_lsp_communication, tool_timeout=tool_timeout, + project_activation_error=project_activation_error, ) if project_file_arg: log.warning( diff --git a/src/serena/mcp.py b/src/serena/mcp.py index 321c9b35..12aa3312 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -22,10 +22,9 @@ from sensai.util import logging from serena.agent import ( SerenaAgent, - SerenaConfig, ) from serena.config.context_mode import SerenaAgentContext -from serena.config.serena_config import LanguageBackend, ModeSelectionDefinition +from serena.config.serena_config import LanguageBackend, ModeSelectionDefinition, SerenaConfig from serena.constants import DEFAULT_CONTEXT, SERENA_LOG_FORMAT from serena.tools import Tool, ToolCallError from serena.util.exception import show_fatal_exception_safe @@ -307,14 +306,18 @@ class SerenaMCPFactory: mcp._tool_manager._tools[tool.get_name()] = mcp_tool log.info(f"Starting MCP server with {len(mcp._tool_manager._tools)} tools: {list(mcp._tool_manager._tools.keys())}") - def _create_serena_agent(self, serena_config: SerenaConfig, modes: ModeSelectionDefinition | None = None) -> SerenaAgent: + def _create_serena_agent( + self, serena_config: SerenaConfig, modes: ModeSelectionDefinition | None = None, project_activation_error: str | None = None + ) -> SerenaAgent: return SerenaAgent( - project=self.project, serena_config=serena_config, context=self.context, modes=modes, memory_log_handler=self.memory_log_handler + project=self.project, + serena_config=serena_config, + context=self.context, + modes=modes, + memory_log_handler=self.memory_log_handler, + project_activation_error=project_activation_error, ) - def _create_default_serena_config(self) -> SerenaConfig: - return SerenaConfig.from_config_file() - def create_mcp_server( self, host: str = "127.0.0.1", @@ -327,6 +330,7 @@ class SerenaMCPFactory: log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] | None = None, trace_lsp_communication: bool | None = None, tool_timeout: float | None = None, + project_activation_error: str | None = None, ) -> FastMCP: """ Create an MCP server with process-isolated SerenaAgent to prevent asyncio contamination. @@ -344,9 +348,10 @@ class SerenaMCPFactory: :param trace_lsp_communication: Whether to trace the communication between Serena and the language servers. This is useful for debugging language server issues. :param tool_timeout: Timeout in seconds for tool execution. If not specified, will take the value from the serena configuration. + :param project_activation_error: an initial project activation error to report back to the client """ try: - config = self._create_default_serena_config() + config = SerenaConfig.from_config_file() # update configuration with the provided parameters if enable_web_dashboard is not None: @@ -365,7 +370,7 @@ class SerenaMCPFactory: if language_backend is not None: config.language_backend = language_backend - self.agent = self._create_serena_agent(config, mode_selection_def) + self.agent = self._create_serena_agent(config, modes=mode_selection_def, project_activation_error=project_activation_error) except Exception as e: show_fatal_exception_safe(e) diff --git a/test/serena/test_serena_agent.py b/test/serena/test_serena_agent.py index 933c5568..cd7cbb6d 100644 --- a/test/serena/test_serena_agent.py +++ b/test/serena/test_serena_agent.py @@ -1452,3 +1452,34 @@ class TestPromptProvision: result2 = self._call_tool(serena_agent, ActivateProjectTool, project=project_name, session_id=session) self._assert_activation_message(result2, project_name, present=True) + + @pytest.mark.python + @pytest.mark.skipif(not language_server_tests_enabled(LanguageServerId.PYTHON), reason="python tests are disabled in this environment") + def test_initial_instructions_report_project_activation_error_until_project_is_activated(self, serena_config) -> None: + """ + Tests that a project activation error passed to the agent at startup is reported in the initial instructions + of every session for as long as no project is active, and that it is no longer reported once a project + has been activated. #1773 + """ + project_name = "test_repo_python" + activation_error = "No project root found from cwd" + + agent = SerenaAgent(project=None, serena_config=serena_config, project_activation_error=activation_error) + agent.execute_task(lambda: None) + try: + # the error must be reported in the initial instructions of any session while no project is active + result1 = self._call_tool(agent, InitialInstructionsTool, session_id="session1") + assert activation_error in result1, f"Expected activation error to be reported in result:\n{result1}" + result2 = self._call_tool(agent, InitialInstructionsTool, session_id="session2") + assert activation_error in result2, f"Expected activation error to be reported in result:\n{result2}" + + # activating a project must provide the activation message + result3 = self._call_tool(agent, ActivateProjectTool, project=project_name, session_id="session1") + self._assert_activation_message(result3, project_name, present=True) + + # after activation, the error must no longer be reported + result4 = self._call_tool(agent, InitialInstructionsTool, session_id="session2") + self._assert_activation_message(result4, project_name, present=True) + assert activation_error not in result4, f"Expected activation error to no longer be reported in result:\n{result4}" + finally: + agent.on_shutdown(timeout=5)