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
This commit is contained in:
Dominik Jain authored and Dominik Jain committed 2026-07-29 17:39:06 +02:00
1 parent a61765ecc8
commit e6e25d3f86
5 files changed
+72 -11

No files matched your search

+7
View File
@@ -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
+10
View File
@@ -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:
+10 -2
View File
@@ -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(
+14 -9
View File
@@ -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)
+31
View File
@@ -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)