mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-12 18:16:43 +00:00
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:
1 parent
a61765ecc8
commit
e6e25d3f86
5 files changed
+72
-11
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
Reference in new issue
Block a user