Add configuration facade (cfg) with get_current_config and open_dashboard

Introduce ConfigApi, exposing Serena's configuration and session state: get_current_config and
open_dashboard (both non-editing). The corresponding tools delegate to the API via ConfigApiMixin.

As on the tool side, opening the dashboard is only offered if the dashboard is enabled and not
opened automatically; the condition is shared (_is_dashboard_openable) and applied to the API scope
via a NamedApiInclusionDefinition, the API-side counterpart of NamedToolInclusionDefinition.
This commit is contained in:
Dominik Jain authored and Dominik Jain committed 2026-09-15 12:50:44 +02:00
1 parent 6716193d71
commit 05603b2706
7 files changed
+118 -11

No files matched your search

+4 -2
View File
@@ -53,5 +53,7 @@ Code runs as a function body (`return` defines the result); a single expression
- Python code can always modify the system; the REPL tool is inherently fully privileged, regardless of
facade scope or the project's `read_only` setting (which only makes Serena's own API refuse edits).
A "read-only REPL" is not feasible and must not be promised.
- Session/project management (activate_project, initial_instructions, dashboard, ...) stays tool-only;
facades expose operations on the active project.
- Project activation (activate_project) and initial_instructions stay tool-only (activation rebuilds the REPL);
Serena's configuration/session state (config overview, dashboard; later e.g. modes) lives in the `cfg` facade.
Computed conditions (read-only project, dashboard not openable) are applied to the API scope in
`SerenaAgent.get_repl` via `exclude_editing()`/`NamedApiInclusionDefinition`, mirroring the tool side.
+15 -1
View File
@@ -36,6 +36,7 @@ from serena.config.serena_config import (
ModeSelectionDefinition,
ModeSelectionDefinitionWithAddedModes,
ModeSelectionDefinitionWithBaseModes,
NamedApiInclusionDefinition,
NamedToolInclusionDefinition,
RegisteredProject,
SerenaConfig,
@@ -48,6 +49,7 @@ from serena.ls_manager import LanguageServerManager
from serena.memories.memory_manager import MemoryManager
from serena.project import Project
from serena.prompt_factory import SerenaPromptFactory
from serena.repl.api.cfg_api import ConfigApi
from serena.repl.api.edit_api import EditApi
from serena.repl.api.fs_api import FsApi
from serena.repl.api.jb_api import JetBrainsApi
@@ -760,6 +762,15 @@ class SerenaAgent:
except Exception as e:
log.debug(f"Failed to send usage info: {e}")
@staticmethod
def _is_dashboard_openable(serena_config: SerenaConfig) -> bool:
"""
:param serena_config: the configuration
:return: whether the web dashboard is available and opening it is a meaningful operation
(i.e. it is enabled and not opened automatically)
"""
return serena_config.web_dashboard and not serena_config.web_dashboard_open_on_launch and not serena_config.gui_log_window
@classmethod
def _create_base_toolset(
cls,
@@ -789,7 +800,7 @@ class SerenaAgent:
# determine whether to include the OpenDashboardTool based on the Serena configuration
tool_inclusion_definitions: list[ToolInclusionDefinition] = []
if serena_config.web_dashboard and not serena_config.web_dashboard_open_on_launch and not serena_config.gui_log_window:
if cls._is_dashboard_openable(serena_config):
tool_inclusion_definitions.append(
NamedToolInclusionDefinition(name="OpenDashboard", included_optional_tools=[OpenDashboardTool.get_name_from_cls()])
)
@@ -1281,9 +1292,12 @@ class SerenaAgent:
api_scope.process(self._active_project.project_config)
if self._active_project.project_config.read_only:
api_scope.exclude_editing()
if not self._is_dashboard_openable(self.serena_config):
api_scope.process(NamedApiInclusionDefinition(name="Dashboard", excluded_apis=["cfg.open_dashboard"]))
# gather facades
facades = [
Facade.from_api(ConfigApi(self), api_scope),
Facade.from_api(FsApi(self), api_scope),
Facade.from_api(EditApi(self), api_scope),
Facade.from_api(MemoryApi(self), api_scope),
+8
View File
@@ -190,6 +190,14 @@ class ApiInclusionDefinition:
excluded_apis: Sequence[str] = ()
@dataclass
class NamedApiInclusionDefinition(ApiInclusionDefinition):
name: str | None = None
def __str__(self) -> str:
return f"ApiInclusionDefinition[{self.name}]"
@dataclass
class ModeSelectionDefinition:
default_modes: Sequence[str] | None = None
+40
View File
@@ -0,0 +1,40 @@
# SPDX-License-Identifier: GPL-3.0-or-later
"""
The implementation of operations concerning Serena's configuration and session state.
"""
from typing import TYPE_CHECKING
from serena.tools import GetCurrentConfigTool, OpenDashboardTool
from ..facade import FacadeApi, facade_method
if TYPE_CHECKING:
from serena.agent import SerenaAgent
class ConfigApi(FacadeApi):
def __init__(self, agent: "SerenaAgent") -> None:
super().__init__(agent, name="cfg", description="Serena's configuration and session state (incl. the dashboard)")
@facade_method(corresponding_tool=GetCurrentConfigTool)
def get_current_config(self) -> str:
"""
Provides the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
:return: the configuration overview
"""
return self._agent.get_current_config_overview()
@facade_method(corresponding_tool=OpenDashboardTool)
def open_dashboard(self) -> str:
"""
Opens the Serena web dashboard in the default web browser.
The dashboard provides logs, session information, and tool usage statistics.
:return: a message indicating whether the dashboard could be opened
"""
if self._agent.open_dashboard():
return f"Serena web dashboard has been opened in the user's default web browser: {self._agent.get_dashboard_url()}"
else:
return f"Serena web dashboard could not be opened automatically; tell the user to open it via {self._agent.get_dashboard_url()}"
+22 -7
View File
@@ -1,11 +1,29 @@
# SPDX-License-Identifier: GPL-3.0-or-later
from typing import TYPE_CHECKING, cast
from sensai.util.helper import mark_used
from serena.tools import Tool, ToolMarkerDoesNotRequireActiveProject, ToolMarkerOptional
if TYPE_CHECKING:
from serena.repl.api.cfg_api import ConfigApi
class OpenDashboardTool(Tool, ToolMarkerOptional, ToolMarkerDoesNotRequireActiveProject):
class ConfigApiMixin:
"""
Mixin for tools which delegate to the configuration API.
The API is imported locally, since the API module refers to the tools (as corresponding tools).
"""
def _api(self) -> "ConfigApi":
from serena.repl.api.cfg_api import ConfigApi
tool = cast(Tool, cast(object, self))
return ConfigApi(tool.agent)
class OpenDashboardTool(Tool, ToolMarkerOptional, ToolMarkerDoesNotRequireActiveProject, ConfigApiMixin):
"""
Opens the Serena web dashboard in the default web browser.
The dashboard provides logs, session information, and tool usage statistics.
@@ -15,10 +33,7 @@ class OpenDashboardTool(Tool, ToolMarkerOptional, ToolMarkerDoesNotRequireActive
"""
Opens the Serena web dashboard in the default web browser.
"""
if self.agent.open_dashboard():
return f"Serena web dashboard has been opened in the user's default web browser: {self.agent.get_dashboard_url()}"
else:
return f"Serena web dashboard could not be opened automatically; tell the user to open it via {self.agent.get_dashboard_url()}"
return self._api().open_dashboard()
class ActivateProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject):
@@ -56,7 +71,7 @@ class RemoveProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject, ToolMarkerO
return f"Successfully removed project '{project_name}' from configuration."
class GetCurrentConfigTool(Tool):
class GetCurrentConfigTool(Tool, ConfigApiMixin):
"""
Prints the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
"""
@@ -65,4 +80,4 @@ class GetCurrentConfigTool(Tool):
"""
Print the current configuration of the agent, including the active and available projects, tools, contexts, and modes.
"""
return self.agent.get_current_config_overview()
return self._api().get_current_config()
+4 -1
View File
@@ -268,7 +268,10 @@ class TestAgentInterface:
fixed_toolset = {"serena_repl", "initial_instructions", "activate_project"}
assert {t.get_name() for t in agent.get_exposed_tool_instances()} == fixed_toolset
assert set(agent.get_active_tool_names()) == fixed_toolset
assert "s.mem" in agent.get_repl().entrypoint.overview()
overview = agent.get_repl().entrypoint.overview()
assert "s.mem" in overview
# the dashboard is disabled in the test configuration, so opening it is not offered
assert "s.cfg" in overview and "open_dashboard" not in overview
# after activation: the toolset is unchanged, the REPL reflects the project's API exclusions
agent.activate_project_from_path_or_name(name)
+25
View File
@@ -0,0 +1,25 @@
"""
Tests for the configuration facade API.
"""
from unittest.mock import MagicMock
from serena.repl.api.cfg_api import ConfigApi
from serena.repl.facade import ApiScope, Facade
def test_facade_exposes_config_operations() -> None:
facade = Facade.from_api(ConfigApi(MagicMock()), ApiScope())
assert facade.name == "cfg"
assert set(facade.enabled_method_names) == {"get_current_config", "open_dashboard"}
assert not any(facade.get_method(name).info.can_edit for name in facade.enabled_method_names)
def test_operations_delegate_to_agent() -> None:
agent = MagicMock()
agent.get_current_config_overview.return_value = "overview"
agent.open_dashboard.return_value = False
agent.get_dashboard_url.return_value = "http://localhost:1"
api = ConfigApi(agent)
assert api.get_current_config() == "overview"
assert "http://localhost:1" in api.open_dashboard()