From 323e855625fe32aa17aafae5797c034cf3d120fa Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Wed, 9 Sep 2026 12:24:28 +0200 Subject: [PATCH] Allow language backend changes upon project activation with the REPL interface * _activate_project: a project's backend override switches the backend in REPL mode (background modes, facades, prompt parameters and backend initialisation are recomputed upon activation) * the tool interface keeps rejecting the mismatch (fixed toolset); the error now mentions the REPL as a workaround --- .serena/memories/repl.md | 3 +++ src/serena/agent.py | 24 ++++++++++++++------ test/serena/config/test_serena_config.py | 29 ++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 7 deletions(-) diff --git a/.serena/memories/repl.md b/.serena/memories/repl.md index 86c8afb4..83403e4d 100644 --- a/.serena/memories/repl.md +++ b/.serena/memories/repl.md @@ -76,6 +76,9 @@ expression is the result. No `return` (a top-level `return` yields a SyntaxError (`serena_repl`, `initial_instructions`, `activate_project` unless single-project); tool inclusion/exclusion definitions do not apply — each interface has its own configuration vocabulary (tool definitions ↔ tools, API definitions ↔ REPL). Contexts do not influence the interface. + In REPL mode, the language backend may change upon project activation (a project's backend override is + applied; background modes, facades, prompt params and backend initialisation are recomputed), whereas the + tool interface forbids this (the toolset depends on the backend and is fixed). Idea (not implemented, considered over-engineered for now): contexts could declare *supported* interfaces (a capability constraint, e.g. clients that handle the REPL badly), with the user's preference choosing among them. - `included_apis`/`excluded_apis` (references `facade` or `facade.method`) in global config, context, modes, diff --git a/src/serena/agent.py b/src/serena/agent.py index 7e4bc490..c0927467 100644 --- a/src/serena/agent.py +++ b/src/serena/agent.py @@ -663,7 +663,8 @@ class SerenaAgent: # determine the effective language backend for this session. # If a startup project is provided and has a per-project override, use it; otherwise use the global config. - # Since we don't want to change the toolset after startup, the language backend cannot be changed within a running Serena session + # With the tool interface, the backend cannot change within a session (the toolset depends on it and is fixed); + # with the REPL interface, it may change upon project activation (see _activate_project). self._language_backend = self.serena_config.determine_language_backend( project_config=registered_project_to_activate.project_config if registered_project_to_activate is not None else None, log_choice=True, @@ -1420,15 +1421,24 @@ class SerenaAgent: self._project_activation_error = None - # check if the project requires a different language backend than the one initialized at startup + # handle the case where the project requires a different language backend than the current one. + # With the tool interface, the backend cannot change, since the set of exposed tools depends on it and is fixed + # for the session. With the REPL interface, the backend can be switched, as all backend-dependent state + # (background modes, REPL facades, prompt parameters, the project's language backend initialisation) is + # recomputed upon activation. project_backend = project.project_config.language_backend if project_backend is not None and project_backend != self._language_backend: - raise ValueError( - f"Cannot activate project '{project.project_name}': it requires the {project_backend.value} backend, " - f"but this session was initialized with {self._language_backend.value}. " - f"Workarounds: (1) Use project activation at startup via the --project flag, " - f"(2) Configure one MCP server per backend in your client." + if self._agent_interface.is_tools(): + raise ValueError( + f"Cannot activate project '{project.project_name}': it requires the {project_backend.value} backend, " + f"but this session was initialized with {self._language_backend.value}. " + f"Workarounds: (1) Use project activation at startup via the --project flag, " + f"(2) Configure one MCP server per backend in your client, (3) use the REPL interface." + ) + log.info( + f"Switching language backend from {self._language_backend.value} to {project_backend.value} for project '{project.project_name}'" ) + self._language_backend = project_backend # shut down the previously active project to release its language server processes if self._active_project is not None: diff --git a/test/serena/config/test_serena_config.py b/test/serena/config/test_serena_config.py index 33fdb60a..64b7a852 100644 --- a/test/serena/config/test_serena_config.py +++ b/test/serena/config/test_serena_config.py @@ -363,6 +363,35 @@ class TestEffectiveLanguageBackend: finally: agent.on_shutdown(timeout=5) + def test_activate_project_switches_backend_with_repl_interface(self): + """With the REPL interface, post-init activation of a project with a different backend switches the backend.""" + config, name = _make_config_with_project("lsp_proj", language_backend=None, global_backend=LanguageBackend.LSP) + config.agent_interface = AgentInterface.REPL + jb_project = Project( + project_root=str(Path(__file__).parent.parent / "resources" / "repos" / "java" / "test_repo"), + project_config=ProjectConfig( + project_name="jb_proj", + language_servers=[LanguageServerId.JAVA], + language_backend=LanguageBackend.JETBRAINS, + ), + serena_config=config, + ) + config.projects.append(RegisteredProject.from_project_instance(jb_project)) + + agent = SerenaAgent(project=name, serena_config=config) + try: + assert agent.get_language_backend() == LanguageBackend.LSP + assert "s.lsp" in agent.get_repl().entrypoint.overview() + + # the backend and everything depending on it follow the activated project + agent.activate_project_from_path_or_name("jb_proj") + assert agent.get_language_backend() == LanguageBackend.JETBRAINS + overview = agent.get_repl().entrypoint.overview() + assert "s.jb" in overview and "s.lsp" not in overview + assert "jetbrains" in [m.name for m in agent.get_active_modes().get_modes(include_background_base_modes=True)] + finally: + agent.on_shutdown(timeout=5) + def test_activate_project_allows_matching_backend(self): """Post-init activation of a project with matching backend succeeds.""" config, name = _make_config_with_project("lsp_proj", language_backend=None, global_backend=LanguageBackend.LSP)