mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-13 18:16:10 +00:00
Add agent_interface setting selecting the tool or REPL interface
Introduce AgentInterface (tools/REPL) as a setting in the global configuration, overridable per project (resolution: project > global > default "tools") and via the CLI option --agent-interface. The interface is fixed for the session, like the language backend. In REPL mode, the set of exposed tools is fixed: the REPL tool, initial_instructions and (unless in a single-project session) activate_project. Tool inclusion/exclusion definitions do not apply in REPL mode, as they pertain to the tool interface; the REPL is configured via API inclusions/exclusions. The REPL tool is optional in tool mode. SerenaAgent.is_single_project records whether the session is a single-project session.
This commit is contained in:
1 parent
e7aa6a74e1
commit
337d545c4b
10 files changed
+244
-20
No files matched your search
@@ -34,6 +34,13 @@ Code runs as a function body (`return` defines the result); a single expression
|
||||
|
||||
## Configuration
|
||||
|
||||
- `agent_interface: tools | REPL` (`AgentInterface`; global config, overridable per project; CLI `--agent-interface`).
|
||||
`None` = Serena's default (`tools`). Fixed for the session. In REPL mode the toolset is *fixed*
|
||||
(`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.
|
||||
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,
|
||||
project config; applied in that order via `ApiScope` (exclusions first, then inclusions; later definitions win).
|
||||
Optional methods and all methods of an excluded facade must be included explicitly.
|
||||
|
||||
+54
-15
@@ -31,6 +31,7 @@ from serena import serena_version
|
||||
from serena.analytics import RegisteredTokenCountEstimator, ToolUsageStats
|
||||
from serena.config.context_mode import SerenaAgentContext, SerenaAgentMode
|
||||
from serena.config.serena_config import (
|
||||
AgentInterface,
|
||||
LanguageBackend,
|
||||
ModeSelectionDefinition,
|
||||
ModeSelectionDefinitionWithAddedModes,
|
||||
@@ -59,10 +60,12 @@ from serena.task_executor import TaskExecutor
|
||||
from serena.tools import (
|
||||
ActivateProjectTool,
|
||||
GetCurrentConfigTool,
|
||||
InitialInstructionsTool,
|
||||
OnboardingTool,
|
||||
OpenDashboardTool,
|
||||
ReadMemoryTool,
|
||||
ReplaceContentTool,
|
||||
SerenaReplTool,
|
||||
Tool,
|
||||
ToolMarker,
|
||||
ToolRegistry,
|
||||
@@ -660,6 +663,13 @@ class SerenaAgent:
|
||||
log_choice=True,
|
||||
)
|
||||
|
||||
# determine the effective agent interface for this session (project configuration > global configuration).
|
||||
# Like the language backend, it is fixed for the session, since the set of exposed tools cannot change after startup.
|
||||
self._agent_interface = self.serena_config.determine_agent_interface(
|
||||
project_config=registered_project_to_activate.project_config if registered_project_to_activate is not None else None,
|
||||
log_choice=True,
|
||||
)
|
||||
|
||||
# create the tool names mapping for prompts
|
||||
self._prompt_tool_names_mapping = self._create_prompt_tool_names_mapping(self._language_backend)
|
||||
|
||||
@@ -685,8 +695,14 @@ class SerenaAgent:
|
||||
self._project_activation_error = str(e)
|
||||
self._update_active_modes()
|
||||
|
||||
# determine whether we are operating in a single-project session, i.e. the project that was activated at startup
|
||||
# (if any) is the only project that will be worked with throughout the session (no project switching)
|
||||
self._is_single_project = self._context.single_project and self._active_project is not None
|
||||
|
||||
# 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._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._agent_interface, self._is_single_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}")
|
||||
|
||||
@@ -753,10 +769,12 @@ class SerenaAgent:
|
||||
context: SerenaAgentContext,
|
||||
modes: ActiveModes,
|
||||
project: Project | None,
|
||||
agent_interface: AgentInterface,
|
||||
is_single_project: bool,
|
||||
) -> ToolSet:
|
||||
"""
|
||||
Determines the base toolset defining the set of exposed tools (which e.g. the MCP shall see).
|
||||
It depends on ...
|
||||
In REPL mode, the toolset is fixed. Otherwise, it depends on ...
|
||||
* dashboard availability/opening on launch
|
||||
* Serena config
|
||||
* the context (which is fixed for the session)
|
||||
@@ -764,6 +782,13 @@ class SerenaAgent:
|
||||
* the optional tools enabled by initial dynamic modes
|
||||
* single-project mode reductions (if applicable)
|
||||
"""
|
||||
# when in REPL mode, the toolset is fixed and does not depend on the configuration, context, modes or project
|
||||
if agent_interface.is_repl():
|
||||
tool_classes: list[type[Tool]] = [SerenaReplTool, InitialInstructionsTool]
|
||||
if not is_single_project:
|
||||
tool_classes.append(ActivateProjectTool)
|
||||
return ToolSet({tool_class.get_name_from_cls() for tool_class in tool_classes})
|
||||
|
||||
# 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:
|
||||
@@ -775,10 +800,6 @@ class SerenaAgent:
|
||||
tool_inclusion_definitions.append(serena_config)
|
||||
tool_inclusion_definitions.append(context)
|
||||
|
||||
# determine whether we are operating in a single-project context
|
||||
# (i.e. the project that is activated at startup is the only project that will be worked with throughout the session)
|
||||
is_single_project = context.single_project and project is not None
|
||||
|
||||
# consider modes
|
||||
# * base modes: These cannot be changed, so they are fully applied
|
||||
for base_mode in modes.get_base_modes(include_background_base_modes=True):
|
||||
@@ -830,6 +851,17 @@ class SerenaAgent:
|
||||
def get_language_backend(self) -> LanguageBackend:
|
||||
return self._language_backend
|
||||
|
||||
def is_single_project(self) -> bool:
|
||||
"""
|
||||
:return: whether this is a single-project session, i.e. the project activated at startup is the only project
|
||||
that will be worked with throughout the session (no project switching); requires a single-project context
|
||||
and a project at startup
|
||||
"""
|
||||
return self._is_single_project
|
||||
|
||||
def get_agent_interface(self) -> AgentInterface:
|
||||
return self._agent_interface
|
||||
|
||||
def get_current_tasks(self) -> list[TaskExecutor.TaskInfo]:
|
||||
"""
|
||||
Gets the list of tasks currently running or queued for execution.
|
||||
@@ -1142,23 +1174,29 @@ class SerenaAgent:
|
||||
|
||||
def _update_active_tools(self) -> None:
|
||||
"""
|
||||
Updates the active tools based on the active modes and the active project.
|
||||
Updates the active tools (and the REPL, which depends on the same configuration) based on the active modes
|
||||
and the active project. Must be called whenever the active modes or the active project change.
|
||||
The base tool set already takes the Serena configuration and the context into account
|
||||
(as well as many other aspects, such as JetBrains mode).
|
||||
"""
|
||||
# apply modes
|
||||
tool_set = self._base_toolset.apply(*self._active_modes.get_modes())
|
||||
if self._agent_interface.is_repl():
|
||||
# the REPL toolset is fixed; tool inclusion/exclusion definitions do not apply
|
||||
tool_set = self._base_toolset
|
||||
else:
|
||||
# apply modes
|
||||
tool_set = self._base_toolset.apply(*self._active_modes.get_modes())
|
||||
|
||||
# apply active project configuration (if any)
|
||||
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()
|
||||
# apply active project configuration (if any)
|
||||
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_set.to_available_tools(self._all_tools)
|
||||
log.info(f"Active tools ({len(self._active_tools)}): {', '.join(self._active_tools.tool_names)}")
|
||||
|
||||
# reset the REPL, which depends on the same configuration (it is re-created on demand)
|
||||
# reset the REPL, whose facades/API scope depend on the active modes and project (it is re-created on demand).
|
||||
# NOTE: This must happen irrespective of the agent interface, since the REPL tool may be active in tool mode as well.
|
||||
self._repl = None
|
||||
|
||||
# check if a tool was activated that is not in the exposed tool set and issue a warning if so
|
||||
@@ -1435,6 +1473,7 @@ class SerenaAgent:
|
||||
result_str += f"Active project: {self._active_project.project_name}\n"
|
||||
else:
|
||||
result_str += "No active project\n"
|
||||
result_str += f"Agent interface: {self._agent_interface.value}\n"
|
||||
result_str += f"Language backend: {self._language_backend.value}"
|
||||
if self._active_project and self._active_project.project_config.language_backend is not None:
|
||||
result_str += " (project override)"
|
||||
|
||||
@@ -23,6 +23,7 @@ from serena import serena_version
|
||||
from serena.config.client_setup import client_setup_handlers
|
||||
from serena.config.context_mode import SerenaAgentContext, SerenaAgentMode
|
||||
from serena.config.serena_config import (
|
||||
AgentInterface,
|
||||
LanguageBackend,
|
||||
ModeSelectionDefinition,
|
||||
ModeSelectionDefinitionWithAddedModes,
|
||||
@@ -263,6 +264,13 @@ class TopLevelCommands(AutoRegisteringGroup):
|
||||
default=None,
|
||||
help="Override the configured language backend.",
|
||||
)
|
||||
@click.option(
|
||||
"--agent-interface",
|
||||
type=click.Choice([i.value for i in AgentInterface], case_sensitive=False),
|
||||
default=None,
|
||||
help="Override the configured agent interface: 'tools' (one tool per operation) or "
|
||||
"'REPL' (Python code execution via the serena_repl tool, with a fixed set of tools).",
|
||||
)
|
||||
@click.option(
|
||||
"--transport",
|
||||
type=click.Choice(["stdio", "sse", "streamable-http"]),
|
||||
@@ -325,6 +333,7 @@ class TopLevelCommands(AutoRegisteringGroup):
|
||||
default_modes: Sequence[str],
|
||||
added_modes: Sequence[str],
|
||||
language_backend: str | None,
|
||||
agent_interface: str | None,
|
||||
transport: Literal["stdio", "sse", "streamable-http"],
|
||||
host: str,
|
||||
port: int,
|
||||
@@ -385,6 +394,7 @@ class TopLevelCommands(AutoRegisteringGroup):
|
||||
port=port,
|
||||
mode_selection_def=mode_selection_def,
|
||||
language_backend=LanguageBackend.from_str(language_backend) if language_backend else None,
|
||||
agent_interface=AgentInterface.from_str(agent_interface) if agent_interface else None,
|
||||
enable_web_dashboard=enable_web_dashboard,
|
||||
open_web_dashboard=open_web_dashboard,
|
||||
enable_gui_log_window=enable_gui_log_window,
|
||||
|
||||
@@ -208,6 +208,37 @@ class ModeSelectionDefinitionWithAddedModes(ModeSelectionDefinition):
|
||||
added_modes: Sequence[str] | None = None
|
||||
|
||||
|
||||
class AgentInterface(Enum):
|
||||
"""
|
||||
The interface through which the agent (LLM) accesses Serena's functionality.
|
||||
"""
|
||||
|
||||
TOOLS = "tools"
|
||||
"""
|
||||
The classic tool interface: each operation is a separate tool, and the set of tools is configurable
|
||||
(via tool inclusions/exclusions in the configuration, context, modes and project).
|
||||
"""
|
||||
REPL = "REPL"
|
||||
"""
|
||||
The REPL interface: operations are accessed programmatically via the serena_repl tool, which executes Python code.
|
||||
The set of tools is fixed (the REPL tool and the tools required for session management) and tool inclusions/exclusions
|
||||
do not apply; the operations available in the REPL are configured via API inclusions/exclusions instead.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
def from_str(interface_str: str) -> "AgentInterface":
|
||||
for interface in AgentInterface:
|
||||
if interface.value.lower() == interface_str.lower():
|
||||
return interface
|
||||
raise ValueError(f"Unknown agent interface '{interface_str}': valid values are {[i.value for i in AgentInterface]}")
|
||||
|
||||
def is_tools(self) -> bool:
|
||||
return self == AgentInterface.TOOLS
|
||||
|
||||
def is_repl(self) -> bool:
|
||||
return self == AgentInterface.REPL
|
||||
|
||||
|
||||
class LanguageBackend(Enum):
|
||||
LSP = "LSP"
|
||||
"""
|
||||
@@ -294,6 +325,7 @@ class SharedConfig(ToolInclusionDefinition, ApiInclusionDefinition, ToStringMixi
|
||||
|
||||
symbol_info_budget: float | None = None
|
||||
language_backend: LanguageBackend | None = None
|
||||
agent_interface: AgentInterface | None = None
|
||||
line_ending: LineEnding | None = None
|
||||
read_only_memory_patterns: list[str] = field(default_factory=list)
|
||||
ignored_memory_patterns: list[str] = field(default_factory=list)
|
||||
@@ -624,6 +656,8 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
|
||||
|
||||
language_backend_value = data.get("language_backend")
|
||||
language_backend = LanguageBackend.from_str(language_backend_value) if language_backend_value else None
|
||||
agent_interface_value = data.get("agent_interface")
|
||||
agent_interface = AgentInterface.from_str(agent_interface_value) if agent_interface_value else None
|
||||
|
||||
line_ending_value = data.get("line_ending")
|
||||
line_ending = LineEnding.from_str(line_ending_value) if line_ending_value else None
|
||||
@@ -659,6 +693,7 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
|
||||
encoding=data["encoding"],
|
||||
line_ending=line_ending,
|
||||
language_backend=language_backend,
|
||||
agent_interface=agent_interface,
|
||||
added_modes=data["added_modes"],
|
||||
default_modes=data["default_modes"],
|
||||
symbol_info_budget=symbol_info_budget,
|
||||
@@ -683,6 +718,7 @@ class ProjectConfig(SharedConfig, ModeSelectionDefinitionWithAddedModes):
|
||||
# map fields using non-primitive types to a YAML-compatible representation
|
||||
d["language_servers"] = [lang.get_key() for lang in self.language_servers]
|
||||
d["language_backend"] = self.language_backend.value if self.language_backend is not None else None
|
||||
d["agent_interface"] = self.agent_interface.value if self.agent_interface is not None else None
|
||||
d["line_ending"] = self.line_ending.value if self.line_ending is not None else None
|
||||
|
||||
return d
|
||||
@@ -976,7 +1012,7 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes):
|
||||
# *** static members ***
|
||||
|
||||
CONFIG_FILE = "serena_config.yml"
|
||||
CONFIG_FIELDS_WITH_TYPE_CONVERSION = {"projects", "language_backend", "line_ending"}
|
||||
CONFIG_FIELDS_WITH_TYPE_CONVERSION = {"projects", "language_backend", "agent_interface", "line_ending"}
|
||||
|
||||
# *** methods ***
|
||||
@classmethod
|
||||
@@ -1126,6 +1162,15 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes):
|
||||
del loaded_commented_yaml["jetbrains"]
|
||||
instance.language_backend = language_backend
|
||||
|
||||
# determine agent interface
|
||||
agent_interface_value = loaded_commented_yaml.get("agent_interface")
|
||||
agent_interface: AgentInterface | None = None
|
||||
if "agent_interface" in loaded_commented_yaml:
|
||||
agent_interface = AgentInterface.from_str(agent_interface_value) if agent_interface_value else None
|
||||
else:
|
||||
num_migrations += 1
|
||||
instance.agent_interface = agent_interface
|
||||
|
||||
# determine line ending
|
||||
line_ending_value = loaded_commented_yaml.get("line_ending")
|
||||
if line_ending_value:
|
||||
@@ -1369,6 +1414,9 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes):
|
||||
# convert language backend to string
|
||||
commented_yaml["language_backend"] = self.language_backend.value
|
||||
|
||||
# convert agent interface to string (None if not configured)
|
||||
commented_yaml["agent_interface"] = self.agent_interface.value if self.agent_interface is not None else None
|
||||
|
||||
# convert line ending to string
|
||||
commented_yaml["line_ending"] = self.line_ending.value
|
||||
|
||||
@@ -1475,6 +1523,25 @@ class SerenaConfig(SharedConfig, ModeSelectionDefinitionWithBaseModes):
|
||||
return True
|
||||
return False
|
||||
|
||||
def determine_agent_interface(self, project_config: "ProjectConfig | None" = None, log_choice: bool = False) -> AgentInterface:
|
||||
"""
|
||||
Determines the effective agent interface: the project configuration takes precedence over the global configuration;
|
||||
if neither configures an interface, the tool interface is used.
|
||||
|
||||
:param project_config: the configuration of the project to be activated, if any
|
||||
:param log_choice: whether to log the choice
|
||||
:return: the effective agent interface
|
||||
"""
|
||||
if project_config is not None and project_config.agent_interface is not None:
|
||||
agent_interface, source = project_config.agent_interface, "project configuration"
|
||||
elif self.agent_interface is not None:
|
||||
agent_interface, source = self.agent_interface, "global configuration"
|
||||
else:
|
||||
agent_interface, source = AgentInterface.TOOLS, "default"
|
||||
if log_choice:
|
||||
log.info(f"Using agent interface '{agent_interface.value}' ({source})")
|
||||
return agent_interface
|
||||
|
||||
def determine_language_backend(self, project_config: ProjectConfig | None = None, log_choice: bool = False):
|
||||
language_backend = self.language_backend
|
||||
if project_config and project_config.language_backend is not None:
|
||||
|
||||
+5
-1
@@ -26,7 +26,7 @@ from serena.agent import (
|
||||
SerenaAgent,
|
||||
)
|
||||
from serena.config.context_mode import SerenaAgentContext
|
||||
from serena.config.serena_config import LanguageBackend, ModeSelectionDefinition, SerenaConfig
|
||||
from serena.config.serena_config import AgentInterface, LanguageBackend, ModeSelectionDefinition, SerenaConfig
|
||||
from serena.constants import DEFAULT_CONTEXT, SERENA_LOG_FORMAT
|
||||
from serena.tools import Tool, ToolCallError
|
||||
from serena.util.exception import show_fatal_exception_safe
|
||||
@@ -326,6 +326,7 @@ class SerenaMCPFactory:
|
||||
port: int = 8000,
|
||||
mode_selection_def: ModeSelectionDefinition | None = None,
|
||||
language_backend: LanguageBackend | None = None,
|
||||
agent_interface: AgentInterface | None = None,
|
||||
enable_web_dashboard: bool | None = None,
|
||||
enable_gui_log_window: bool | None = None,
|
||||
open_web_dashboard: bool | None = None,
|
||||
@@ -341,6 +342,7 @@ class SerenaMCPFactory:
|
||||
:param port: The port to bind to
|
||||
:param mode_selection_def: the mode selection definition to apply
|
||||
:param language_backend: the language backend to use, overriding the configuration setting.
|
||||
:param agent_interface: the agent interface to use, overriding the configuration setting.
|
||||
:param enable_web_dashboard: Whether to enable the web dashboard. If not specified, will take the value from the serena configuration.
|
||||
:param enable_gui_log_window: Whether to enable the GUI log window. It currently does not work on macOS, and setting this to True will be ignored then.
|
||||
If not specified, will take the value from the serena configuration.
|
||||
@@ -371,6 +373,8 @@ class SerenaMCPFactory:
|
||||
config.tool_timeout = tool_timeout
|
||||
if language_backend is not None:
|
||||
config.language_backend = language_backend
|
||||
if agent_interface is not None:
|
||||
config.agent_interface = agent_interface
|
||||
|
||||
self.agent = self._create_serena_agent(config, modes=mode_selection_def, project_activation_error=project_activation_error)
|
||||
|
||||
|
||||
@@ -68,6 +68,11 @@ line_ending:
|
||||
# is activated post-init, an error will be returned.
|
||||
language_backend:
|
||||
|
||||
# The interface through which the agent (LLM) accesses Serena's functionality (overrides the global setting).
|
||||
# Valid values: tools, REPL (see the global configuration for details); leave empty to use the global setting.
|
||||
# Note: the interface is fixed at startup. If a project is activated post-init, its setting is not applied.
|
||||
agent_interface:
|
||||
|
||||
# whether to use project's .gitignore files to ignore files
|
||||
ignore_all_files_in_gitignore: true
|
||||
|
||||
|
||||
@@ -7,6 +7,18 @@
|
||||
# in your IDE).
|
||||
language_backend: LSP
|
||||
|
||||
# The interface through which the agent (LLM) accesses Serena's functionality:
|
||||
# * tools: the classic tool interface, in which each operation is a separate tool.
|
||||
# The set of tools is configurable via the tool inclusion/exclusion settings (excluded_tools etc.)
|
||||
# of the configuration, the context, modes and the project.
|
||||
# * REPL: operations are accessed programmatically via the serena_repl tool, which executes Python code.
|
||||
# The set of tools is fixed (the REPL tool and the tools required for session management), and the tool
|
||||
# inclusion/exclusion settings do not apply; instead, the operations available in the REPL are configured
|
||||
# via the API inclusion/exclusion settings (excluded_apis etc.).
|
||||
# Leave empty to use Serena's default (currently "tools").
|
||||
# The interface is fixed at startup and can be overridden per project.
|
||||
agent_interface:
|
||||
|
||||
# line ending convention to use when writing source files.
|
||||
# Possible values: "lf" (Unix), "crlf" (Windows), "native" (platform default).
|
||||
# Note that Serena's own files (e.g. memories and configuration files) always use native line endings.
|
||||
|
||||
@@ -4,10 +4,10 @@ Tools which provide access to Serena's functionality through Python code executi
|
||||
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
from serena.tools.tools_base import Tool, ToolMarkerBeta
|
||||
from serena.tools.tools_base import Tool, ToolMarkerBeta, ToolMarkerOptional
|
||||
|
||||
|
||||
class SerenaReplTool(Tool, ToolMarkerBeta):
|
||||
class SerenaReplTool(Tool, ToolMarkerOptional, ToolMarkerBeta):
|
||||
"""
|
||||
Executes Python code which accesses Serena's functionality programmatically.
|
||||
"""
|
||||
|
||||
@@ -10,6 +10,7 @@ import pytest
|
||||
from serena.agent import SerenaAgent
|
||||
from serena.config.serena_config import (
|
||||
DEFAULT_PROJECT_SERENA_FOLDER_LOCATION,
|
||||
AgentInterface,
|
||||
LanguageBackend,
|
||||
ProjectConfig,
|
||||
RegisteredProject,
|
||||
@@ -218,6 +219,65 @@ class TestProjectConfigLanguageBackend:
|
||||
assert config.language_backend is None
|
||||
|
||||
|
||||
class TestAgentInterface:
|
||||
"""Tests for the agent_interface setting (global and per project)."""
|
||||
|
||||
@staticmethod
|
||||
def _project_config(agent_interface: AgentInterface | None) -> ProjectConfig:
|
||||
return ProjectConfig(project_name="test", language_servers=[LanguageServerId.PYTHON], agent_interface=agent_interface)
|
||||
|
||||
def test_agent_interface_roundtrips_through_project_yaml(self):
|
||||
assert self._project_config(AgentInterface.REPL)._to_yaml_dict()["agent_interface"] == "REPL"
|
||||
assert self._project_config(None)._to_yaml_dict()["agent_interface"] is None
|
||||
|
||||
def test_agent_interface_parsed_from_project_dict(self):
|
||||
data, _ = ProjectConfig._load_yaml_dict(PROJECT_TEMPLATE_FILE)
|
||||
data["project_name"] = "test"
|
||||
data["languages"] = ["python"]
|
||||
data["agent_interface"] = "repl" # case-insensitive
|
||||
assert ProjectConfig._from_dict(data, local_override_keys=[]).agent_interface == AgentInterface.REPL
|
||||
data.pop("agent_interface")
|
||||
assert ProjectConfig._from_dict(data, local_override_keys=[]).agent_interface is None
|
||||
|
||||
def test_determine_agent_interface_precedence(self):
|
||||
# default
|
||||
assert SerenaConfig().determine_agent_interface() == AgentInterface.TOOLS
|
||||
assert SerenaConfig().determine_agent_interface(self._project_config(None)) == AgentInterface.TOOLS
|
||||
# global configuration
|
||||
assert SerenaConfig(agent_interface=AgentInterface.REPL).determine_agent_interface() == AgentInterface.REPL
|
||||
# project configuration takes precedence
|
||||
config = SerenaConfig(agent_interface=AgentInterface.REPL)
|
||||
assert config.determine_agent_interface(self._project_config(AgentInterface.TOOLS)) == AgentInterface.TOOLS
|
||||
assert config.determine_agent_interface(self._project_config(None)) == AgentInterface.REPL
|
||||
|
||||
def test_repl_toolset_is_fixed_and_repl_follows_project_activation(self):
|
||||
"""
|
||||
In REPL mode, neither the exposed nor the active toolset is affected by tool inclusion/exclusion definitions
|
||||
(here: the project's exclusions and read-only setting), whereas the REPL's API scope follows the active project.
|
||||
"""
|
||||
config, name = _make_config_with_project("test_proj")
|
||||
config.agent_interface = AgentInterface.REPL
|
||||
project_config = config.projects[0].project_config
|
||||
project_config.excluded_tools = ["initial_instructions", "serena_repl"]
|
||||
project_config.excluded_apis = ["mem"]
|
||||
project_config.read_only = True
|
||||
|
||||
agent = SerenaAgent(project=None, serena_config=config)
|
||||
try:
|
||||
# before activation: the fixed toolset and the full set of facades
|
||||
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()
|
||||
|
||||
# after activation: the toolset is unchanged, the REPL reflects the project's API exclusions
|
||||
agent.activate_project_from_path_or_name(name)
|
||||
assert set(agent.get_active_tool_names()) == fixed_toolset
|
||||
assert "s.mem" not in agent.get_repl().entrypoint.overview()
|
||||
finally:
|
||||
agent.on_shutdown(timeout=5)
|
||||
|
||||
|
||||
def _make_config_with_project(
|
||||
project_name: str,
|
||||
language_backend: LanguageBackend | None = None,
|
||||
|
||||
@@ -14,7 +14,7 @@ from _pytest.mark import Mark, MarkDecorator, ParameterSet
|
||||
|
||||
from serena.agent import SerenaAgent
|
||||
from serena.config.context_mode import SerenaAgentContext
|
||||
from serena.config.serena_config import ProjectConfig, RegisteredProject, SerenaConfig
|
||||
from serena.config.serena_config import AgentInterface, ProjectConfig, RegisteredProject, SerenaConfig
|
||||
from serena.lsp.lsp_diagnostics import DiagnosticsContext
|
||||
from serena.project import Project
|
||||
from serena.tools import (
|
||||
@@ -30,6 +30,7 @@ from serena.tools import (
|
||||
ReplaceInFilesTool,
|
||||
ReplaceSymbolBodyTool,
|
||||
SafeDeleteSymbol,
|
||||
SerenaReplTool,
|
||||
Tool,
|
||||
)
|
||||
from solidlsp.ls_config import LanguageServerId
|
||||
@@ -901,6 +902,25 @@ class TestSerenaAgent:
|
||||
finally:
|
||||
agent.on_shutdown(timeout=5)
|
||||
|
||||
@pytest.mark.python
|
||||
@pytest.mark.skipif(not language_server_tests_enabled(LanguageServerId.PYTHON), reason="python tests are disabled in this environment")
|
||||
@pytest.mark.parametrize("context_name", ["desktop-app", "grok"], ids=["multi_project", "single_project"])
|
||||
def test_repl_interface_exposes_fixed_toolset(self, serena_config, context_name: str):
|
||||
# the toolset is fixed regardless of tool inclusions/exclusions (e.g. the context's or the configuration's);
|
||||
# only the single-project property of the context matters (no project activation in that case)
|
||||
serena_config.agent_interface = AgentInterface.REPL
|
||||
serena_config.included_optional_tools = ["get_diagnostics_for_symbol"]
|
||||
context = SerenaAgentContext.from_name(context_name)
|
||||
agent = SerenaAgent(project="test_repo_python", serena_config=serena_config, context=context)
|
||||
agent.execute_task(lambda: None)
|
||||
try:
|
||||
exposed = {tool.get_name() for tool in agent.get_exposed_tool_instances()}
|
||||
expected = {"serena_repl", "initial_instructions"} | (set() if context.single_project else {"activate_project"})
|
||||
assert exposed == expected
|
||||
assert "s.lsp" in agent.get_tool(SerenaReplTool).apply("s.info()")
|
||||
finally:
|
||||
agent.on_shutdown(timeout=5)
|
||||
|
||||
def _symbol_matches_expected_name(self, symbol: dict, expected_name: str) -> bool:
|
||||
return (
|
||||
symbol.get("name") == expected_name
|
||||
|
||||
Reference in new issue
Block a user