From dc9ea7a1c8528bf3475268575fb38768a53256a1 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Wed, 10 Jun 2026 15:57:13 +0200 Subject: [PATCH] Fix: JetBrains mode prompt was not provided to agents The mode is now treated as a (background) base mode in `ActiveModes` which reduces the surface for issues pertaining to custom handling of modes. --- CHANGELOG.md | 2 ++ src/serena/agent.py | 66 ++++++++++++++++++++++++----------------- src/serena/dashboard.py | 2 +- 3 files changed, 42 insertions(+), 28 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01c8e759..df802ac9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,8 @@ Status of the `main` branch. Changes prior to the next official version change w incorrect file access if a corresponding local file existed (e.g. `./antigravity` binary); file access is now guarded with path detection (file ending or path separator must be present) - Allow `query_project` tool to access read-only tools that are not enabled in the current configuration + - Fix: JetBrains mode prompt was not provided to agents; The mode is now treated as a (background) base mode + in `ActiveModes` which reduces the surface for issues pertaining to custom handling of modes. * Language Servers: - `typescript_vts`: Add `initialization_options` setting in `ls_specific_settings.typescript_vts`. diff --git a/src/serena/agent.py b/src/serena/agent.py index 55e06180..5a151377 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -210,7 +210,12 @@ class ToolSet: class ActiveModes: _mode_instances: dict[str, SerenaAgentMode] = {} - def __init__(self) -> None: + def __init__(self, background_base_modes: Sequence[SerenaAgentMode] | None = None) -> None: + """ + :param background_base_modes: base modes that are always active in the background and will not be removed + by applications of mode selection definitions via method `apply` + """ + self._background_base_modes = background_base_modes or [] self._configured_base_modes: Sequence[str] | None = None self._configured_default_modes: Sequence[str] | None = None self._added_modes: set[str] = set() @@ -220,7 +225,7 @@ class ActiveModes: """ self._active_mode_names: Sequence[str] = [] """ - the full list of active mode names + the full list of active mode names (not including background base modes) """ def apply(self, mode_selection: ModeSelectionDefinition) -> None: @@ -245,6 +250,9 @@ class ActiveModes: self._active_mode_names = sorted(set(self._configured_base_modes or []) | self._dynamically_activated_mode_names) def get_mode_names(self) -> Sequence[str]: + """ + :return: the ordered list of active mode names (not including the non-user-facing background base modes) + """ return self._active_mode_names @classmethod @@ -253,14 +261,22 @@ class ActiveModes: cls._mode_instances[mode_name] = SerenaAgentMode.load(mode_name) return cls._mode_instances[mode_name] - def get_modes(self) -> Sequence[SerenaAgentMode]: - return [self.get_mode_instance(mode_name) for mode_name in self._active_mode_names] + def get_modes(self, include_background_base_modes: bool = False) -> Sequence[SerenaAgentMode]: + result: list[SerenaAgentMode] = [] + if include_background_base_modes: + result.extend(self._background_base_modes) + result.extend([self.get_mode_instance(mode_name) for mode_name in self._active_mode_names]) + return result def get_dynamically_activated_modes(self) -> Sequence[SerenaAgentMode]: return [self.get_mode_instance(mode_name) for mode_name in self._dynamically_activated_mode_names] - def get_base_modes(self) -> Sequence[SerenaAgentMode]: - return [self.get_mode_instance(mode_name) for mode_name in self._configured_base_modes or []] + def get_base_modes(self, include_background_base_modes: bool = False) -> Sequence[SerenaAgentMode]: + result: list[SerenaAgentMode] = [] + if include_background_base_modes: + result.extend(self._background_base_modes) + result.extend([self.get_mode_instance(mode_name) for mode_name in self._configured_base_modes or []]) + return result class ProjectPromptProvisionStatus: @@ -555,10 +571,6 @@ class SerenaAgent: # propagate configuration to other components self.serena_config.propagate_settings() - # initialise active modes (baseline modes prior to project activation) - self._active_modes: ActiveModes - self._update_active_modes(log_message=False) - # determine registered project to be activated (if any) registered_project_to_activate: RegisteredProject | None = ( self.serena_config.get_registered_project(project, autoregister=True) if project is not None else None @@ -642,6 +654,10 @@ class SerenaAgent: # Initialize the prompt factory self.prompt_factory = SerenaPromptFactory() + # initialise active modes (baseline modes prior to project activation, allowing newly activated modes to be tracked) + self._active_modes: ActiveModes + self._update_active_modes(log_message=False) + # activate the given project (if any), also updating the active modes # Note: We cannot update the active tools yet, because the base toolset has not been computed yet # (and its computation depends on the active project) @@ -653,9 +669,7 @@ class SerenaAgent: self._update_active_modes() # determine the base toolset defining the set of exposed tools (which e.g. the MCP shall see), - self._base_toolset = self._create_base_toolset( - self.serena_config, self._language_backend, self._context, self._active_modes, self._active_project - ) + self._base_toolset = self._create_base_toolset(self.serena_config, self._context, self._active_modes, self._active_project) self._exposed_tools = self._base_toolset.to_available_tools(self._all_tools) log.info(f"Number of exposed tools: {len(self._exposed_tools)}. Exposed tools: {self._exposed_tools.tool_names}") @@ -719,7 +733,6 @@ class SerenaAgent: def _create_base_toolset( cls, serena_config: SerenaConfig, - language_backend: LanguageBackend, context: SerenaAgentContext, modes: ActiveModes, project: Project | None, @@ -730,9 +743,9 @@ class SerenaAgent: * dashboard availability/opening on launch * Serena config * the context (which is fixed for the session) - * the optional tools enabled by initial modes + * the base modes (including background base modes like JetBrains mode) + * the optional tools enabled by initial dynamic modes * single-project mode reductions (if applicable) - * JetBrains mode """ # determine whether to include the OpenDashboardTool based on the Serena configuration tool_inclusion_definitions: list[ToolInclusionDefinition] = [] @@ -751,7 +764,7 @@ class SerenaAgent: # consider modes # * base modes: These cannot be changed, so they are fully applied - for base_mode in modes.get_base_modes(): + for base_mode in modes.get_base_modes(include_background_base_modes=True): tool_inclusion_definitions.append(base_mode) # * dynamically activated modes: # - When not in a single-project context, these modes can later be turned off, @@ -788,10 +801,6 @@ class SerenaAgent: ) tool_inclusion_definitions.append(project.project_config) - # enabled the internal 'jetbrains' mode for the JetBrains backend - if language_backend == LanguageBackend.JETBRAINS: - tool_inclusion_definitions.append(SerenaAgentMode.from_name_internal("jetbrains")) - # compute the resulting tool set base_toolset = ToolSet.default().apply(*tool_inclusion_definitions) log.info(f"Number of exposed tools: {len(base_toolset)}") @@ -912,11 +921,11 @@ class SerenaAgent: raise ValueError("No active project. Please activate a project first.") return project - def get_active_modes(self) -> list[SerenaAgentMode]: + def get_active_modes(self) -> ActiveModes: """ - :return: the list of active modes + :return: the active modes """ - return list(self._active_modes.get_modes()) + return self._active_modes def _format_prompt(self, prompt_template: str) -> str: template = JinjaTemplate(prompt_template) @@ -948,7 +957,7 @@ class SerenaAgent: # determine modes for which prompts must (still) be provided, excluding modes that were already provided in a # previously provided project activation message (if any) relevant_modes = [] - for mode in self.get_active_modes(): + for mode in self.get_active_modes().get_modes(include_background_base_modes=True): if mode.has_prompt(): if not self._project_prompt_status.is_mode_prompt_already_provided(mode.name, session_id): relevant_modes.append(mode) @@ -1027,7 +1036,10 @@ class SerenaAgent: Updates the active modes based on the Serena configuration, the active project configuration (if any), and mode overrides (if any). """ - self._active_modes = ActiveModes() + background_base_modes = [] + if self._language_backend.is_jetbrains(): + background_base_modes.append(SerenaAgentMode.from_name_internal("jetbrains")) + self._active_modes = ActiveModes(background_base_modes=background_base_modes) self._active_modes.apply(self.serena_config) if self._active_project: self._active_modes.apply(self._active_project.project_config) @@ -1260,7 +1272,7 @@ class SerenaAgent: result_str += f"Active context: {self._context.name}\n" # Active modes - active_mode_names = [mode.name for mode in self.get_active_modes()] + active_mode_names = self.get_active_modes().get_mode_names() result_str += "Active modes: {}\n".format(", ".join(active_mode_names)) + "\n" # Available but not active modes diff --git a/src/serena/dashboard.py b/src/serena/dashboard.py index cac54bc5..f424ca04 100644 --- a/src/serena/dashboard.py +++ b/src/serena/dashboard.py @@ -502,7 +502,7 @@ class SerenaDashboardAPI: } # Get active modes - modes = self._agent.get_active_modes() + modes = self._agent.get_active_modes().get_modes(include_background_base_modes=False) modes_info = [ {"name": mode.name, "description": mode.description, "path": SerenaAgentMode.get_path(mode.name, instance=mode)} for mode in modes