diff --git a/config/modes/one-shot.yml b/config/modes/one-shot.yml index 27d3eb6..b408669 100644 --- a/config/modes/one-shot.yml +++ b/config/modes/one-shot.yml @@ -12,4 +12,4 @@ prompt: | excluded_tools: - get_current_config - activate_project - - activate_modes + - switch_modes diff --git a/src/serena/agent.py b/src/serena/agent.py index 80d57a0..ed16a06 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -321,8 +321,8 @@ class SerenaAgent: context = SerenaAgentContext.load_default() if modes is None: modes = SerenaAgentMode.load_default_modes() - self.context = context - self.modes = modes + self._context = context + self._modes = modes self._update_active_tools() log.info(f"Loaded tools ({len(self._all_tools)}): {', '.join([tool.get_name() for tool in self._all_tools.values()])}") @@ -368,15 +368,21 @@ class SerenaAgent: :param modes: List of mode names or paths to use """ - self.current_modes = modes + self._modes = modes self._update_active_tools() log.info(f"Set modes to {[mode.name for mode in modes]}") + def get_active_modes(self) -> list[SerenaAgentMode]: + """ + :return: the list of active modes + """ + return list(self._modes) + def create_system_prompt(self) -> str: return self.prompt_factory.create_system_prompt( - context_system_prompt=self.context.prompt, - mode_system_prompts=[mode.prompt for mode in self.current_modes], + context_system_prompt=self._context.prompt, + mode_system_prompts=[mode.prompt for mode in self._modes], ) def _update_active_tools(self) -> None: @@ -390,9 +396,9 @@ class SerenaAgent: """ # Collect all excluded tools with the desired priority mode < context < project excluded_tool_classes: set[type[Tool]] = set() - for mode in self.current_modes: + for mode in self._modes: excluded_tool_classes.update(mode.get_excluded_tool_classes()) - excluded_tool_classes.update(self.context.get_excluded_tool_classes()) + excluded_tool_classes.update(self._context.get_excluded_tool_classes()) if self.project_config is not None: excluded_tool_classes.update(self.project_config.get_excluded_tool_classes()) @@ -440,6 +446,24 @@ class SerenaAgent: """ return sorted([tool.get_name() for tool in self._active_tools.values()]) + def get_current_config_overview(self) -> str: + """ + :return: a string overview of the current configuration, including the active project, context, modes, and tools + """ + result_str = "Current configuration:\n" + if self.project_config is not None: + result_str += f"Active project: {self.project_config.project_name}\n" + result_str += f"Active context: {self._context.name}\n" + result_str += "Active modes: {}\n".format(", ".join([mode.name for mode in self.get_active_modes()])) + result_str += "Active tools (after all exclusions from the project, context, and modes):\n" + active_tool_names = self.get_active_tool_names() + # print the tool names in chunks + chunk_size = 4 + for i in range(0, len(active_tool_names), chunk_size): + chunk = active_tool_names[i : i + chunk_size] + result_str += " " + ", ".join(chunk) + "\n" + return result_str + def is_language_server_running(self) -> bool: return self.language_server is not None and self.language_server.is_running() @@ -1528,14 +1552,7 @@ class GetCurrentConfigTool(Tool): """ Print the current configuration of the agent, including the active modes, tools, and context. """ - result_str = "Current configuration:\n" - if self.agent.project_config is not None: - result_str += f"Active project: {self.agent.project_config.project_name}\n" - result_str += f"Active context: {self.agent.context.name}\n" - result_str += "Active modes: {}\n".format(", ".join([mode.name for mode in self.agent.current_modes])) - result_str += "Active tools (exclusions from the project, context, and modes):\n" - result_str += "\n".join(self.agent.get_active_tool_names()) - return result_str + return self.agent.get_current_config_overview() class InitialInstructionsTool(Tool): diff --git a/src/serena/config.py b/src/serena/config.py index 44de286..57b7a68 100644 --- a/src/serena/config.py +++ b/src/serena/config.py @@ -10,7 +10,7 @@ from typing import TYPE_CHECKING, Self import yaml from sensai.util import logging -from serena.constants import CONTEXT_YAMLS_DIR, MODE_YAMLS_DIR +from serena.constants import CONTEXT_YAMLS_DIR, DEFAULT_CONTEXT, DEFAULT_MODES, MODE_YAMLS_DIR if TYPE_CHECKING: from serena.agent import Tool @@ -69,7 +69,7 @@ class SerenaAgentMode: @classmethod def load_default_modes(cls) -> list[Self]: """Load the default modes (interactive and editing).""" - return [cls.from_name("interactive"), cls.from_name("editing")] + return [cls.from_name(mode) for mode in DEFAULT_MODES] @classmethod def load(cls, name_or_path: str | Path) -> Self: @@ -111,8 +111,8 @@ class SerenaAgentContext: yaml_path = os.path.join(CONTEXT_YAMLS_DIR, f"{name}.yml") if not os.path.exists(yaml_path): raise FileNotFoundError( - f"Context {Path(yaml_path).stem} not found in {CONTEXT_YAMLS_DIR}. You can load a custom context by using from_yaml() instead. " - f"Available contexts: {cls.list_registered_context_names()}" + f"Context {Path(yaml_path).stem} not found in {CONTEXT_YAMLS_DIR}. You can load a custom context by using from_yaml() instead.\n" + f"Available contexts:\n{cls.list_registered_context_names()}" ) return cls.from_yaml(yaml_path) @@ -131,7 +131,7 @@ class SerenaAgentContext: @classmethod def load_default(cls) -> Self: """Load the default context.""" - return cls.from_name("default") + return cls.from_name(DEFAULT_CONTEXT) def print_overview(self) -> None: """Print an overview of the mode.""" diff --git a/src/serena/constants.py b/src/serena/constants.py index 7ae6254..5e3165e 100644 --- a/src/serena/constants.py +++ b/src/serena/constants.py @@ -6,3 +6,6 @@ REPO_ROOT = str(_repo_root_path) PROMPT_TEMPLATES_DIR = str(_repo_root_path / "config" / "prompt_templates") CONTEXT_YAMLS_DIR = str(_repo_root_path / "config" / "contexts") MODE_YAMLS_DIR = str(_repo_root_path / "config" / "modes") + +DEFAULT_CONTEXT = "desktop-app" +DEFAULT_MODES = ("interactive", "editing") diff --git a/src/serena/mcp.py b/src/serena/mcp.py index 8f5773c..c8fed88 100644 --- a/src/serena/mcp.py +++ b/src/serena/mcp.py @@ -20,6 +20,7 @@ from sensai.util.helper import mark_used from serena.agent import SerenaAgent, Tool, show_fatal_exception_safe from serena.config import SerenaAgentContext, SerenaAgentMode +from serena.constants import DEFAULT_CONTEXT, DEFAULT_MODES log = logging.getLogger(__name__) LOG_FORMAT = "%(levelname)-5s %(asctime)-15s %(name)s:%(funcName)s:%(lineno)d - %(message)s" @@ -94,9 +95,13 @@ def make_tool( ) -def create_mcp_server( - project_file_path: str | None, host: str = "0.0.0.0", port: int = 8000, context: str = "default", modes: Sequence[str] = ("default",) -) -> FastMCP: +def create_mcp_server_and_agent( + project_file_path: str | None, + host: str = "0.0.0.0", + port: int = 8000, + context: str = DEFAULT_CONTEXT, + modes: Sequence[str] = DEFAULT_MODES, +) -> tuple[FastMCP, SerenaAgent]: """ Create an MCP server. @@ -147,7 +152,7 @@ def create_mcp_server( update_tools() - return mcp + return mcp, agent @click.command() @@ -169,7 +174,8 @@ def create_mcp_server( @click.option( "--context", type=str, - default="desktop-app", + show_default=True, + default=DEFAULT_CONTEXT, help="Context to use. This can be a name of a built-in context ('desktop-app', 'agent', 'ide-assistant') " "or a path to a custom context YAML file.", ) @@ -178,7 +184,8 @@ def create_mcp_server( "modes", type=str, multiple=True, - default=["editing", "interactive"], + default=DEFAULT_MODES, + show_default=True, help="Mode(s) to use. This can be names of built-in modes ('planning', 'editing', 'one-shot', 'interactive') " "or paths to custom mode YAML files. Can be specified multiple times to combine modes.", ) @@ -192,22 +199,22 @@ def create_mcp_server( @click.option( "--host", type=str, - default="0.0.0.0", show_default=True, + default="0.0.0.0", help="Host to bind to (for SSE transport).", ) @click.option( "--port", type=int, - default=8000, show_default=True, + default=8000, help="Port to bind to (for SSE transport).", ) def start_mcp_server( project_file_opt: str | None, project_file_arg: str | None, - context: str, - modes: tuple[str, ...], + context: str = DEFAULT_CONTEXT, + modes: tuple[str, ...] = DEFAULT_MODES, transport: Literal["stdio", "sse"] = "stdio", host: str = "0.0.0.0", port: int = 8000, @@ -222,7 +229,7 @@ def start_mcp_server( # This is for backward compatibility with the old CLI, should be removed in the future! project_file = project_file_arg if project_file_arg is not None else project_file_opt - mcp_server = create_mcp_server(project_file_path=project_file, host=host, port=port, context=context, modes=modes) + mcp_server, agent = create_mcp_server_and_agent(project_file_path=project_file, host=host, port=port, context=context, modes=modes) # log after server creation such that the log appears in the GUI if project_file_arg is not None: @@ -232,10 +239,6 @@ def start_mcp_server( f"Used path: {project_file}" ) - # Log selected context and modes - if context: - log.info(f"Using context: {context}") - if modes: - log.info(f"Using modes: {', '.join(modes)}") + log.info(f"Starting serena agent in MCP server with config:\n{agent.get_current_config_overview()}") mcp_server.run(transport=transport) diff --git a/src/serena/util/general.py b/src/serena/util/general.py new file mode 100644 index 0000000..e69de29