Skip to main content
Glama

search_for_pattern

Search for arbitrary patterns in codebases using regular expressions, including non-code files. Restrict searches by file type, directory, or glob patterns to target specific content. Retrieve matched lines with optional surrounding context for detailed analysis.

Instructions

Offers a flexible search for arbitrary patterns in the codebase, including the possibility to search in non-code files. Generally, symbolic operations like find_symbol or find_referencing_symbols should be preferred if you know which symbols you are looking for.

Pattern Matching Logic: For each match, the returned result will contain the full lines where the substring pattern is found, as well as optionally some lines before and after it. The pattern will be compiled with DOTALL, meaning that the dot will match all characters including newlines. This also means that it never makes sense to have .* at the beginning or end of the pattern, but it may make sense to have it in the middle for complex patterns. If a pattern matches multiple lines, all those lines will be part of the match. Be careful to not use greedy quantifiers unnecessarily, it is usually better to use non-greedy quantifiers like .*? to avoid matching too much content.

File Selection Logic: The files in which the search is performed can be restricted very flexibly. Using restrict_search_to_code_files is useful if you are only interested in code symbols (i.e., those symbols that can be manipulated with symbolic tools like find_symbol). You can also restrict the search to a specific file or directory, and provide glob patterns to include or exclude certain files on top of that. The globs are matched against relative file paths from the project root (not to the relative_path parameter that is used to further restrict the search). Smartly combining the various restrictions allows you to perform very targeted searches. Returns A mapping of file paths to lists of matched consecutive lines.

Input Schema

TableJSON Schema
NameRequiredDescriptionDefault
context_lines_afterNoNumber of lines of context to include after each match.
context_lines_beforeNoNumber of lines of context to include before each match.
max_answer_charsNoIf the output is longer than this number of characters, no content will be returned. Don't adjust unless there is really no other way to get the content required for the task. Instead, if the output is too long, you should make a stricter query.
paths_exclude_globNoOptional glob pattern specifying files to exclude from the search. Matches against relative file paths from the project root (e.g., "*test*", "**/*_generated.py"). Takes precedence over paths_include_glob. Only matches files, not directories. If left empty, no files are excluded.
paths_include_globNoOptional glob pattern specifying files to include in the search. Matches against relative file paths from the project root (e.g., "*.py", "src/**/*.ts"). Only matches files, not directories. If left empty, all non-ignored files will be included.
relative_pathNoOnly subpaths of this path (relative to the repo root) will be analyzed. If a path to a single file is passed, only that will be searched. The path must exist, otherwise a `FileNotFoundError` is raised.
restrict_search_to_code_filesNoWhether to restrict the search to only those files where analyzed code symbols can be found. Otherwise, will search all non-ignored files. Set this to True if your search is only meant to discover code that can be manipulated with symbolic tools. For example, for finding classes or methods from a name pattern. Setting to False is a better choice if you also want to search in non-code files, like in html or yaml files, which is why it is the default.
substring_patternYesRegular expression for a substring pattern to search for.

Implementation Reference

  • SearchForPatternTool class: the main handler implementing the tool logic. Performs regex pattern searches across project files or subdirectories, with options for context lines, include/exclude globs, and restriction to code files. Returns JSON of file-to-match mappings.
    class SearchForPatternTool(Tool): """ Performs a search for a pattern in the project. """ def apply( self, substring_pattern: str, context_lines_before: int = 0, context_lines_after: int = 0, paths_include_glob: str = "", paths_exclude_glob: str = "", relative_path: str = "", restrict_search_to_code_files: bool = False, max_answer_chars: int = -1, ) -> str: """ Offers a flexible search for arbitrary patterns in the codebase, including the possibility to search in non-code files. Generally, symbolic operations like find_symbol or find_referencing_symbols should be preferred if you know which symbols you are looking for. Pattern Matching Logic: For each match, the returned result will contain the full lines where the substring pattern is found, as well as optionally some lines before and after it. The pattern will be compiled with DOTALL, meaning that the dot will match all characters including newlines. This also means that it never makes sense to have .* at the beginning or end of the pattern, but it may make sense to have it in the middle for complex patterns. If a pattern matches multiple lines, all those lines will be part of the match. Be careful to not use greedy quantifiers unnecessarily, it is usually better to use non-greedy quantifiers like .*? to avoid matching too much content. File Selection Logic: The files in which the search is performed can be restricted very flexibly. Using `restrict_search_to_code_files` is useful if you are only interested in code symbols (i.e., those symbols that can be manipulated with symbolic tools like find_symbol). You can also restrict the search to a specific file or directory, and provide glob patterns to include or exclude certain files on top of that. The globs are matched against relative file paths from the project root (not to the `relative_path` parameter that is used to further restrict the search). Smartly combining the various restrictions allows you to perform very targeted searches. :param substring_pattern: Regular expression for a substring pattern to search for :param context_lines_before: Number of lines of context to include before each match :param context_lines_after: Number of lines of context to include after each match :param paths_include_glob: optional glob pattern specifying files to include in the search. Matches against relative file paths from the project root (e.g., "*.py", "src/**/*.ts"). Supports standard glob patterns (*, ?, [seq], **, etc.) and brace expansion {a,b,c}. Only matches files, not directories. If left empty, all non-ignored files will be included. :param paths_exclude_glob: optional glob pattern specifying files to exclude from the search. Matches against relative file paths from the project root (e.g., "*test*", "**/*_generated.py"). Supports standard glob patterns (*, ?, [seq], **, etc.) and brace expansion {a,b,c}. Takes precedence over paths_include_glob. Only matches files, not directories. If left empty, no files are excluded. :param relative_path: only subpaths of this path (relative to the repo root) will be analyzed. If a path to a single file is passed, only that will be searched. The path must exist, otherwise a `FileNotFoundError` is raised. :param max_answer_chars: if the output is longer than this number of characters, no content will be returned. -1 means the default value from the config will be used. Don't adjust unless there is really no other way to get the content required for the task. Instead, if the output is too long, you should make a stricter query. :param restrict_search_to_code_files: whether to restrict the search to only those files where analyzed code symbols can be found. Otherwise, will search all non-ignored files. Set this to True if your search is only meant to discover code that can be manipulated with symbolic tools. For example, for finding classes or methods from a name pattern. Setting to False is a better choice if you also want to search in non-code files, like in html or yaml files, which is why it is the default. :return: A mapping of file paths to lists of matched consecutive lines. """ abs_path = os.path.join(self.get_project_root(), relative_path) if not os.path.exists(abs_path): raise FileNotFoundError(f"Relative path {relative_path} does not exist.") if restrict_search_to_code_files: matches = self.project.search_source_files_for_pattern( pattern=substring_pattern, relative_path=relative_path, context_lines_before=context_lines_before, context_lines_after=context_lines_after, paths_include_glob=paths_include_glob.strip(), paths_exclude_glob=paths_exclude_glob.strip(), ) else: if os.path.isfile(abs_path): rel_paths_to_search = [relative_path] else: _dirs, rel_paths_to_search = scan_directory( path=abs_path, recursive=True, is_ignored_dir=self.project.is_ignored_path, is_ignored_file=self.project.is_ignored_path, relative_to=self.get_project_root(), ) # TODO (maybe): not super efficient to walk through the files again and filter if glob patterns are provided # but it probably never matters and this version required no further refactoring matches = search_files( rel_paths_to_search, substring_pattern, file_reader=self.project.read_file, root_path=self.get_project_root(), paths_include_glob=paths_include_glob, paths_exclude_glob=paths_exclude_glob, ) # group matches by file file_to_matches: dict[str, list[str]] = defaultdict(list) for match in matches: assert match.source_file_path is not None file_to_matches[match.source_file_path].append(match.to_display_string()) result = self._to_json(file_to_matches) return self._limit_length(result, max_answer_chars)
  • ToolRegistry singleton: automatically discovers all Tool subclasses in serena.tools.* modules via iter_subclasses(Tool), derives snake_case tool name (e.g., SearchForPatternTool -> search_for_pattern), and registers them with is_optional based on ToolMarkerOptional.
    @singleton class ToolRegistry: def __init__(self) -> None: self._tool_dict: dict[str, RegisteredTool] = {} for cls in iter_subclasses(Tool): if not cls.__module__.startswith("serena.tools"): continue is_optional = issubclass(cls, ToolMarkerOptional) name = cls.get_name_from_cls() if name in self._tool_dict: raise ValueError(f"Duplicate tool name found: {name}. Tool classes must have unique names.") self._tool_dict[name] = RegisteredTool(tool_class=cls, is_optional=is_optional, tool_name=name) def get_tool_class_by_name(self, tool_name: str) -> type[Tool]: return self._tool_dict[tool_name].tool_class def get_all_tool_classes(self) -> list[type[Tool]]: return list(t.tool_class for t in self._tool_dict.values()) def get_tool_classes_default_enabled(self) -> list[type[Tool]]: """ :return: the list of tool classes that are enabled by default (i.e. non-optional tools). """ return [t.tool_class for t in self._tool_dict.values() if not t.is_optional] def get_tool_classes_optional(self) -> list[type[Tool]]: """ :return: the list of tool classes that are optional (i.e. disabled by default). """ return [t.tool_class for t in self._tool_dict.values() if t.is_optional] def get_tool_names_default_enabled(self) -> list[str]: """ :return: the list of tool names that are enabled by default (i.e. non-optional tools). """ return [t.tool_name for t in self._tool_dict.values() if not t.is_optional] def get_tool_names_optional(self) -> list[str]: """ :return: the list of tool names that are optional (i.e. disabled by default). """ return [t.tool_name for t in self._tool_dict.values() if t.is_optional] def get_tool_names(self) -> list[str]: """ :return: the list of all tool names. """ return list(self._tool_dict.keys()) def print_tool_overview( self, tools: Iterable[type[Tool] | Tool] | None = None, include_optional: bool = False, only_optional: bool = False ) -> None: """ Print a summary of the tools. If no tools are passed, a summary of the selection of tools (all, default or only optional) is printed. """ if tools is None: if only_optional: tools = self.get_tool_classes_optional() elif include_optional: tools = self.get_all_tool_classes() else: tools = self.get_tool_classes_default_enabled() tool_dict: dict[str, type[Tool] | Tool] = {} for tool_class in tools: tool_dict[tool_class.get_name_from_cls()] = tool_class for tool_name in sorted(tool_dict.keys()): tool_class = tool_dict[tool_name] print(f" * `{tool_name}`: {tool_class.get_tool_description().strip()}") def is_valid_tool_name(self, tool_name: str) -> bool: return tool_name in self._tool_dict
  • SerenaAgent initializes all tools from ToolRegistry.get_all_tool_classes() into self._all_tools dict (class -> instance), provides get_tool(tool_class) and get_tool_by_name(tool_name) for access.
    # instantiate all tool classes self._all_tools: dict[type[Tool], Tool] = {tool_class: tool_class(self) for tool_class in ToolRegistry().get_all_tool_classes()} tool_names = [tool.get_name_from_cls() for tool in self._all_tools.values()] # If GUI log window is enabled, set the tool names for highlighting if self._gui_log_viewer is not None: self._gui_log_viewer.set_tool_names(tool_names) token_count_estimator = RegisteredTokenCountEstimator[self.serena_config.token_count_estimator] log.info(f"Will record tool usage statistics with token count estimator: {token_count_estimator.name}.") self._tool_usage_stats = ToolUsageStats(token_count_estimator) # log fundamental information log.info(f"Starting Serena server (version={serena_version()}, process id={os.getpid()}, parent process id={os.getppid()})") log.info("Configuration file: %s", self.serena_config.config_file_path) log.info("Available projects: {}".format(", ".join(self.serena_config.project_names))) log.info(f"Loaded tools ({len(self._all_tools)}): {', '.join([tool.get_name_from_cls() for tool in self._all_tools.values()])}") self._check_shell_settings() # determine the base toolset defining the set of exposed tools (which e.g. the MCP shall see), # limited by the Serena config, the context (which is fixed for the session) and JetBrains mode tool_inclusion_definitions: list[ToolInclusionDefinition] = [self.serena_config, self._context] if self._context.single_project: tool_inclusion_definitions.extend(self._single_project_context_tool_inclusion_definitions(project)) if self.serena_config.jetbrains: tool_inclusion_definitions.append(SerenaAgentMode.from_name_internal("jetbrains")) self._base_tool_set = ToolSet.default().apply(*tool_inclusion_definitions) self._exposed_tools = AvailableTools([t for t in self._all_tools.values() if self._base_tool_set.includes_name(t.get_name())]) log.info(f"Number of exposed tools: {len(self._exposed_tools)}") # create executor for starting the language server and running tools in another thread # This executor is used to achieve linear task execution self._task_executor = TaskExecutor("SerenaAgentTaskExecutor") # Initialize the prompt factory self.prompt_factory = SerenaPromptFactory() self._project_activation_callback = project_activation_callback # set the active modes if modes is None: modes = SerenaAgentMode.load_default_modes() self._modes = modes self._active_tools: dict[type[Tool], Tool] = {} self._update_active_tools() # activate a project configuration (if provided or if there is only a single project available) if project is not None: try: self.activate_project_from_path_or_name(project) except Exception as e: log.error(f"Error activating project '{project}' at startup: {e}", exc_info=e) # start the dashboard (web frontend), registering its log handler # should be the last thing to happen in the initialization since the dashboard # may access various parts of the agent if self.serena_config.web_dashboard: self._dashboard_thread, port = SerenaDashboardAPI( get_memory_log_handler(), tool_names, agent=self, tool_usage_stats=self._tool_usage_stats ).run_in_thread() dashboard_url = f"http://127.0.0.1:{port}/dashboard/index.html" log.info("Serena web dashboard started at %s", dashboard_url) if self.serena_config.web_dashboard_open_on_launch: # open the dashboard URL in the default web browser (using a separate process to control # output redirection) process = multiprocessing.Process(target=self._open_dashboard, args=(dashboard_url,)) process.start() process.join(timeout=1) # inform the GUI window (if any) if self._gui_log_viewer is not None: self._gui_log_viewer.set_dashboard_url(dashboard_url) def get_current_tasks(self) -> list[TaskExecutor.TaskInfo]: """ Gets the list of tasks currently running or queued for execution. The function returns a list of thread-safe TaskInfo objects (specifically created for the caller). :return: the list of tasks in the execution order (running task first) """ return self._task_executor.get_current_tasks() def get_last_executed_task(self) -> TaskExecutor.TaskInfo | None: """ Gets the last executed task. :return: the last executed task info or None if no task has been executed yet """ return self._task_executor.get_last_executed_task() def get_language_server_manager(self) -> LanguageServerManager | None: if self._active_project is not None: return self._active_project.language_server_manager return None 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!" ) return language_server_manager def get_context(self) -> SerenaAgentContext: return self._context def get_tool_description_override(self, tool_name: str) -> str | None: return self._context.tool_description_overrides.get(tool_name, None) def _check_shell_settings(self) -> None: # On Windows, Claude Code sets COMSPEC to Git-Bash (often even with a path containing spaces), # which causes all sorts of trouble, preventing language servers from being launched correctly. # So we make sure that COMSPEC is unset if it has been set to bash specifically. if platform.system() == "Windows": comspec = os.environ.get("COMSPEC", "") if "bash" in comspec: os.environ["COMSPEC"] = "" # force use of default shell log.info("Adjusting COMSPEC environment variable to use the default shell instead of '%s'", comspec) def _single_project_context_tool_inclusion_definitions(self, project_root_or_name: str | None) -> list[ToolInclusionDefinition]: """ In the IDE assistant context, the agent is assumed to work on a single project, and we thus want to apply that project's tool exclusions/inclusions from the get-go, limiting the set of tools that will be exposed to the client. Furthermore, we disable tools that are only relevant for project activation. So if the project exists, we apply all the aforementioned exclusions. :param project_root_or_name: the project root path or project name :return: """ tool_inclusion_definitions = [] if project_root_or_name is not None: # Note: Auto-generation is disabled, because the result must be returned instantaneously # (project generation could take too much time), so as not to delay MCP server startup # and provide responses to the client immediately. project = self.load_project_from_path_or_name(project_root_or_name, autogenerate=False) if project is not None: log.info( "Applying tool inclusion/exclusion definitions for single-project context based on project '%s'", project.project_name ) tool_inclusion_definitions.append( ToolInclusionDefinition( excluded_tools=[ActivateProjectTool.get_name_from_cls(), GetCurrentConfigTool.get_name_from_cls()] ) ) tool_inclusion_definitions.append(project.project_config) return tool_inclusion_definitions def record_tool_usage(self, input_kwargs: dict, tool_result: str | dict, tool: Tool) -> None: """ Record the usage of a tool with the given input and output strings if tool usage statistics recording is enabled. """ tool_name = tool.get_name() input_str = str(input_kwargs) output_str = str(tool_result) log.debug(f"Recording tool usage for tool '{tool_name}'") self._tool_usage_stats.record_tool_usage(tool_name, input_str, output_str) @staticmethod def _open_dashboard(url: str) -> None: # Redirect stdout and stderr file descriptors to /dev/null, # making sure that nothing can be written to stdout/stderr, even by subprocesses null_fd = os.open(os.devnull, os.O_WRONLY) os.dup2(null_fd, sys.stdout.fileno()) os.dup2(null_fd, sys.stderr.fileno()) os.close(null_fd) # open the dashboard URL in the default web browser webbrowser.open(url) def get_project_root(self) -> str: """ :return: the root directory of the active project (if any); raises a ValueError if there is no active project """ project = self.get_active_project() if project is None: raise ValueError("Cannot get project root if no project is active.") return project.project_root def get_exposed_tool_instances(self) -> list["Tool"]: """ :return: the tool instances which are exposed (e.g. to the MCP client). Note that the set of exposed tools is fixed for the session, as clients don't react to changes in the set of tools, so this is the superset of tools that can be offered during the session. If a client should attempt to use a tool that is dynamically disabled (e.g. because a project is activated that disables it), it will receive an error. """ return list(self._exposed_tools.tools) def get_active_project(self) -> Project | None: """ :return: the active project or None if no project is active """ return self._active_project def get_active_project_or_raise(self) -> Project: """ :return: the active project or raises an exception if no project is active """ project = self.get_active_project() if project is None: raise ValueError("No active project. Please activate a project first.") return project def set_modes(self, modes: list[SerenaAgentMode]) -> None: """ Set the current mode configurations. :param modes: List of mode names or paths to use """ 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 _format_prompt(self, prompt_template: str) -> str: template = JinjaTemplate(prompt_template) return template.render(available_tools=self._exposed_tools.tool_names, available_markers=self._exposed_tools.tool_marker_names) def create_system_prompt(self) -> str: available_markers = self._exposed_tools.tool_marker_names log.info("Generating system prompt with available_tools=(see exposed tools), available_markers=%s", available_markers) system_prompt = self.prompt_factory.create_system_prompt( context_system_prompt=self._format_prompt(self._context.prompt), mode_system_prompts=[self._format_prompt(mode.prompt) for mode in self._modes], available_tools=self._exposed_tools.tool_names, available_markers=available_markers, ) # If a project is active at startup, append its activation message if self._active_project is not None: system_prompt += "\n\n" + self._active_project.get_activation_message() log.info("System prompt:\n%s", system_prompt) return system_prompt def _update_active_tools(self) -> None: """ Update the active tools based on enabled modes and the active project. The base tool set already takes the Serena configuration and the context into account (as well as any internal modes that are not handled dynamically, such as JetBrains mode). """ tool_set = self._base_tool_set.apply(*self._modes) if self._active_project is not None: tool_set = tool_set.apply(self._active_project.project_config) if self._active_project.project_config.read_only: tool_set = tool_set.without_editing_tools() self._active_tools = { tool_class: tool_instance for tool_class, tool_instance in self._all_tools.items() if tool_set.includes_name(tool_instance.get_name()) } log.info(f"Active tools ({len(self._active_tools)}): {', '.join(self.get_active_tool_names())}") def issue_task( self, task: Callable[[], T], name: str | None = None, logged: bool = True, timeout: float | None = None ) -> TaskExecutor.Task[T]: """ Issue a task to the executor for asynchronous execution. It is ensured that tasks are executed in the order they are issued, one after another. :param task: the task to execute :param name: the name of the task for logging purposes; if None, use the task function's name :param logged: whether to log management of the task; if False, only errors will be logged :param timeout: the maximum time to wait for task completion in seconds, or None to wait indefinitely :return: the task object, through which the task's future result can be accessed """ return self._task_executor.issue_task(task, name=name, logged=logged, timeout=timeout) def execute_task(self, task: Callable[[], T], name: str | None = None, logged: bool = True, timeout: float | None = None) -> T: """ Executes the given task synchronously via the agent's task executor. This is useful for tasks that need to be executed immediately and whose results are needed right away. :param task: the task to execute :param name: the name of the task for logging purposes; if None, use the task function's name :param logged: whether to log management of the task; if False, only errors will be logged :param timeout: the maximum time to wait for task completion in seconds, or None to wait indefinitely :return: the result of the task execution """ return self._task_executor.execute_task(task, name=name, logged=logged, timeout=timeout) def is_using_language_server(self) -> bool: """ :return: whether this agent uses language server-based code analysis """ return not self.serena_config.jetbrains def _activate_project(self, project: Project) -> None: log.info(f"Activating {project.project_name} at {project.project_root}") self._active_project = project self._update_active_tools() def init_language_server_manager() -> None: # start the language server with LogTime("Language server initialization", logger=log): self.reset_language_server_manager() # initialize the language server in the background (if in language server mode) if self.is_using_language_server(): self.issue_task(init_language_server_manager) if self._project_activation_callback is not None: self._project_activation_callback() def load_project_from_path_or_name(self, project_root_or_name: str, autogenerate: bool) -> Project | None: """ Get a project instance from a path or a name. :param project_root_or_name: the path to the project root or the name of the project :param autogenerate: whether to autogenerate the project for the case where first argument is a directory which does not yet contain a Serena project configuration file :return: the project instance if it was found/could be created, None otherwise """ project_instance: Project | None = self.serena_config.get_project(project_root_or_name) if project_instance is not None: log.info(f"Found registered project '{project_instance.project_name}' at path {project_instance.project_root}") elif autogenerate and os.path.isdir(project_root_or_name): project_instance = self.serena_config.add_project_from_path(project_root_or_name) log.info(f"Added new project {project_instance.project_name} for path {project_instance.project_root}") return project_instance def activate_project_from_path_or_name(self, project_root_or_name: str) -> Project: """ Activate a project from a path or a name. If the project was already registered, it will just be activated. If the argument is a path at which no Serena project previously existed, the project will be created beforehand. Raises ProjectNotFoundError if the project could neither be found nor created. :return: a tuple of the project instance and a Boolean indicating whether the project was newly created """ project_instance: Project | None = self.load_project_from_path_or_name(project_root_or_name, autogenerate=True) if project_instance is None: raise ProjectNotFoundError( f"Project '{project_root_or_name}' not found: Not a valid project name or directory. " f"Existing project names: {self.serena_config.project_names}" ) self._activate_project(project_instance) return project_instance def get_active_tool_classes(self) -> list[type["Tool"]]: """ :return: the list of active tool classes for the current project """ return list(self._active_tools.keys()) def get_active_tool_names(self) -> list[str]: """ :return: the list of names of the active tools for the current project """ return sorted([tool.get_name_from_cls() for tool in self.get_active_tool_classes()]) def tool_is_active(self, tool_class: type["Tool"] | str) -> bool: """ :param tool_class: the class or name of the tool to check :return: True if the tool is active, False otherwise """ if isinstance(tool_class, str): return tool_class in self.get_active_tool_names() else: return tool_class in self.get_active_tool_classes() def get_current_config_overview(self) -> str: """ :return: a string overview of the current configuration, including the active and available configuration options """ result_str = "Current configuration:\n" result_str += f"Serena version: {serena_version()}\n" result_str += f"Loglevel: {self.serena_config.log_level}, trace_lsp_communication={self.serena_config.trace_lsp_communication}\n" if self._active_project is not None: result_str += f"Active project: {self._active_project.project_name}\n" else: result_str += "No active project\n" result_str += "Available projects:\n" + "\n".join(list(self.serena_config.project_names)) + "\n" result_str += f"Active context: {self._context.name}\n" # Active modes active_mode_names = [mode.name for mode in self.get_active_modes()] result_str += "Active modes: {}\n".format(", ".join(active_mode_names)) + "\n" # Available but not active modes all_available_modes = SerenaAgentMode.list_registered_mode_names() inactive_modes = [mode for mode in all_available_modes if mode not in active_mode_names] if inactive_modes: result_str += "Available but not active modes: {}\n".format(", ".join(inactive_modes)) + "\n" # Active tools 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" # Available but not active tools all_tool_names = sorted([tool.get_name_from_cls() for tool in self._all_tools.values()]) inactive_tool_names = [tool for tool in all_tool_names if tool not in active_tool_names] if inactive_tool_names: result_str += "Available but not active tools:\n" for i in range(0, len(inactive_tool_names), chunk_size): chunk = inactive_tool_names[i : i + chunk_size] result_str += " " + ", ".join(chunk) + "\n" return result_str def reset_language_server_manager(self) -> None: """ Starts/resets the language server manager for the current project """ tool_timeout = self.serena_config.tool_timeout if tool_timeout is None or tool_timeout < 0: ls_timeout = None else: if tool_timeout < 10: raise ValueError(f"Tool timeout must be at least 10 seconds, but is {tool_timeout} seconds") 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, ) def add_language(self, language: Language) -> None: """ Adds a new language to the active project, spawning the respective language server and updating the project configuration. The addition is scheduled via the agent's task executor and executed synchronously, i.e. the method returns when the addition is complete. :param language: the language to add """ self.execute_task(lambda: self.get_active_project_or_raise().add_language(language), name=f"AddLanguage:{language.value}") def remove_language(self, language: Language) -> None: """ Removes a language from the active project, shutting down the respective language server and updating the project configuration. The removal is scheduled via the agent's task executor and executed asynchronously. :param language: the language to remove """ self.issue_task(lambda: self.get_active_project_or_raise().remove_language(language), name=f"RemoveLanguage:{language.value}") def get_tool(self, tool_class: type[TTool]) -> TTool: return self._all_tools[tool_class] # type: ignore def print_tool_overview(self) -> None: ToolRegistry().print_tool_overview(self._active_tools.values()) def __del__(self) -> None: """ Destructor to clean up the language server instance and GUI logger """ if not hasattr(self, "_is_initialized"): return log.info("SerenaAgent is shutting down ...") if self._active_project is not None: self._active_project.shutdown() self._active_project = None if self._gui_log_viewer: log.info("Stopping the GUI log window ...") self._gui_log_viewer.stop() def get_tool_by_name(self, tool_name: str) -> Tool: tool_class = ToolRegistry().get_tool_class_by_name(tool_name)
  • Tool.get_name_from_cls(): classmethod deriving the tool name in snake_case from CamelCase class name by stripping 'Tool' suffix and inserting underscores before uppercase letters.
    def get_name_from_cls(cls) -> str: name = cls.__name__ if name.endswith("Tool"): name = name[:-4] # convert to snake_case name = "".join(["_" + c.lower() if c.isupper() else c for c in name]).lstrip("_") return name

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/oraios/serena'

If you have feedback or need assistance with the MCP directory API, please join our Discord server