mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-14 06:17:58 +00:00
Added a base class ToolBehaviorConfig, renamed hover to symbol_info
The latter because we likely will eventually start using the same field for JB tools
This commit is contained in:
1 parent
4086ce5e6e
commit
9c50d2882a
5 files changed
+58
-52
No files matched your search
@@ -134,6 +134,16 @@ class ModeSelectionDefinition:
|
||||
default_modes: Sequence[str] | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class ToolBehaviorConfig:
|
||||
"""Shared between SerenaConfig and ProjectConfig, the latter used to override values in the form
|
||||
(same as in ModeSelectionDefinition).
|
||||
The defaults here shall be none and should be set to the global default values in SerenaConfig.
|
||||
"""
|
||||
|
||||
symbol_info_budget: float | None = None
|
||||
|
||||
|
||||
class SerenaConfigError(Exception):
|
||||
pass
|
||||
|
||||
@@ -163,7 +173,7 @@ class LanguageBackend(Enum):
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class ProjectConfig(ToolInclusionDefinition, ModeSelectionDefinition, ToStringMixin):
|
||||
class ProjectConfig(ToolInclusionDefinition, ToolBehaviorConfig, ModeSelectionDefinition, ToStringMixin):
|
||||
project_name: str
|
||||
languages: list[Language]
|
||||
ignored_paths: list[str] = field(default_factory=list)
|
||||
@@ -171,12 +181,6 @@ class ProjectConfig(ToolInclusionDefinition, ModeSelectionDefinition, ToStringMi
|
||||
ignore_all_files_in_gitignore: bool = True
|
||||
initial_prompt: str = ""
|
||||
encoding: str = DEFAULT_SOURCE_FILE_ENCODING
|
||||
hover_budget: float | None = None
|
||||
"""
|
||||
Per-project override for include_info hover budget (seconds).
|
||||
|
||||
If None, global config is used. 0 disables the budget. Negative values are invalid.
|
||||
"""
|
||||
|
||||
SERENA_DEFAULT_PROJECT_FILE = "project.yml"
|
||||
FIELDS_WITHOUT_DEFAULTS = {"project_name", "languages"}
|
||||
@@ -329,16 +333,16 @@ class ProjectConfig(ToolInclusionDefinition, ModeSelectionDefinition, ToStringMi
|
||||
f"Invalid language: {orig_language_str}.\nValid language_strings are: {[l.value for l in Language]}"
|
||||
) from e
|
||||
|
||||
# Validate hover_budget
|
||||
hover_budget_raw = data["hover_budget"]
|
||||
hover_budget = hover_budget_raw
|
||||
if hover_budget is not None:
|
||||
# Validate symbol_info_budget
|
||||
symbol_info_budget_raw = data["symbol_info_budget"]
|
||||
symbol_info_budget = symbol_info_budget_raw
|
||||
if symbol_info_budget is not None:
|
||||
try:
|
||||
hover_budget = float(hover_budget_raw)
|
||||
symbol_info_budget = float(symbol_info_budget_raw)
|
||||
except (TypeError, ValueError) as e:
|
||||
raise ValueError(f"hover_budget must be a number or null, got: {hover_budget_raw}") from e
|
||||
if hover_budget < 0:
|
||||
raise ValueError(f"hover_budget cannot be negative, got: {hover_budget}")
|
||||
raise ValueError(f"symbol_info_budget must be a number or null, got: {symbol_info_budget_raw}") from e
|
||||
if symbol_info_budget < 0:
|
||||
raise ValueError(f"symbol_info_budget cannot be negative, got: {symbol_info_budget}")
|
||||
|
||||
return cls(
|
||||
project_name=data["project_name"],
|
||||
@@ -353,7 +357,7 @@ class ProjectConfig(ToolInclusionDefinition, ModeSelectionDefinition, ToStringMi
|
||||
encoding=data["encoding"],
|
||||
base_modes=data["base_modes"],
|
||||
default_modes=data["default_modes"],
|
||||
hover_budget=hover_budget,
|
||||
symbol_info_budget=symbol_info_budget,
|
||||
)
|
||||
|
||||
def _to_yaml_dict(self) -> dict:
|
||||
@@ -481,7 +485,7 @@ class RegisteredProject(ToStringMixin):
|
||||
|
||||
|
||||
@dataclass(kw_only=True)
|
||||
class SerenaConfig(ToolInclusionDefinition, ModeSelectionDefinition, ToStringMixin):
|
||||
class SerenaConfig(ToolInclusionDefinition, ToolBehaviorConfig, ModeSelectionDefinition, ToStringMixin):
|
||||
"""
|
||||
Holds the Serena agent configuration, which is typically loaded from a YAML configuration file
|
||||
(when instantiated via :method:`from_config_file`), which is updated when projects are added or removed.
|
||||
@@ -525,16 +529,16 @@ class SerenaConfig(ToolInclusionDefinition, ModeSelectionDefinition, ToStringMix
|
||||
"""List of paths to ignore across all projects. Same syntax as gitignore, so you can use * and **.
|
||||
These patterns are merged additively with each project's own ignored_paths."""
|
||||
|
||||
hover_budget: float = 10.0
|
||||
"""
|
||||
Time budget (seconds) for LSP hover requests when tools request include_info.
|
||||
|
||||
If budget is exceeded, Serena stops issuing further hover requests and returns partial info results.
|
||||
0 disables the budget (no early stopping). Negative values are invalid.
|
||||
"""
|
||||
|
||||
# settings with overridden defaults
|
||||
default_modes: Sequence[str] | None = ("interactive", "editing")
|
||||
symbol_info_budget: float = 10.0
|
||||
"""
|
||||
Time budget (seconds) for requests when tools request include_info (currently
|
||||
only supported for LSP-based tools).
|
||||
|
||||
If the budget is exceeded, Serena stops issuing further requests and returns partial info results.
|
||||
0 disables the budget (no early stopping). Negative values are invalid.
|
||||
"""
|
||||
|
||||
# *** fields that are NOT mapped to/from the configuration file ***
|
||||
|
||||
|
||||
@@ -111,6 +111,6 @@ default_modes:
|
||||
# (contrary to the memories, which are loaded on demand).
|
||||
initial_prompt: ""
|
||||
|
||||
# Override of the correspounding setting in serena_config.yml, see the documentation there.
|
||||
# Override of the corresponding setting in serena_config.yml, see the documentation there.
|
||||
# If null or missing, the value from the global config is used.
|
||||
hover_budget: null
|
||||
symbol_info_budget: null
|
||||
@@ -108,14 +108,16 @@ default_max_tool_answer_chars: 150000
|
||||
# estimate the token count using the Claude Sonnet 4 tokenizer.
|
||||
token_count_estimator: CHAR_COUNT
|
||||
|
||||
# Time budget (seconds) budget for LSP request_hover calls within an MCP tool call (in seconds).
|
||||
# Time budget (seconds) for requests when tools request symbol info (currently
|
||||
# only supported for LSP-based tools).
|
||||
# If the budget is exceeded, Serena stops issuing further symbol-info-related
|
||||
# requests and returns partial info results.
|
||||
# 0 disables the budget (no early stopping). Negative values are invalid.
|
||||
# This is an advanced setting that can help alleviate problems with LSP servers
|
||||
# that have a slow implementation of request_hover (clangd is one of those)
|
||||
# or with tool calls that find very many symbols.
|
||||
# If the configured budget is exceeded, Serena stops issuing further hover requests and returns partial info results.
|
||||
# 0 disables the budget (no early stopping). Negative values are invalid.
|
||||
# Can be overridden in project.yml.
|
||||
hover_budget: 10
|
||||
symbol_info_budget: 10
|
||||
|
||||
# the list of registered project paths (updated automatically).
|
||||
projects: []
|
||||
+13
-13
@@ -587,17 +587,17 @@ class LanguageServerSymbolRetriever:
|
||||
return None
|
||||
return self._request_info(relative_file_path=symbol.relative_path, line=symbol.line, column=symbol.column) # type: ignore[arg-type]
|
||||
|
||||
def _get_hover_budget(self, default_budget: float = 10) -> float:
|
||||
def _get_symbol_info_budget(self, default_budget: float = 10) -> float:
|
||||
"""Project -> global -> default"""
|
||||
hover_budget = default_budget
|
||||
symbol_info_budget = default_budget
|
||||
if self.agent is not None:
|
||||
hover_budget = self.agent.serena_config.hover_budget
|
||||
symbol_info_budget = self.agent.serena_config.symbol_info_budget
|
||||
active_project = self.agent.get_active_project()
|
||||
if active_project is not None:
|
||||
project_hover_budget = active_project.project_config.hover_budget
|
||||
if project_hover_budget is not None:
|
||||
hover_budget = project_hover_budget
|
||||
return hover_budget
|
||||
project_symbol_info_budget = active_project.project_config.symbol_info_budget
|
||||
if project_symbol_info_budget is not None:
|
||||
symbol_info_budget = project_symbol_info_budget
|
||||
return symbol_info_budget
|
||||
|
||||
def request_info_for_symbol_batch(
|
||||
self,
|
||||
@@ -606,13 +606,13 @@ class LanguageServerSymbolRetriever:
|
||||
"""Retrieves information for multiple symbols while staying within a time budget.
|
||||
|
||||
The request_hover operation used here is potentially expensive, we optimize by grouping by file
|
||||
and stop executing it (returning the info as None) after the hover_budget is exceeded.
|
||||
and stop executing it (returning the info as None) after the symbol_info_budget is exceeded.
|
||||
The hover budget is 5s by default
|
||||
|
||||
Groups symbols by file path to minimize file switching overhead and uses a per-file
|
||||
cache keyed by (line, col) to avoid duplicate hover lookups.
|
||||
|
||||
The hover budget (hover_budget) limits total time spent on hover
|
||||
The hover budget (symbol_info_budget) limits total time spent on hover
|
||||
requests. If exceeded, remaining symbols get info=None (partial results).
|
||||
|
||||
:param symbols: list of symbols to get info for
|
||||
@@ -640,7 +640,7 @@ class LanguageServerSymbolRetriever:
|
||||
symbols_by_file.setdefault(file_path, []).append(sym)
|
||||
|
||||
hover_spent_seconds = 0.0
|
||||
hover_budget_seconds = self._get_hover_budget()
|
||||
symbol_info_budget_seconds = self._get_symbol_info_budget()
|
||||
# the vars below are only for debug logging
|
||||
per_file_stats: list[tuple[str, int, float]] = []
|
||||
total_hover_lookups = 0
|
||||
@@ -653,8 +653,8 @@ class LanguageServerSymbolRetriever:
|
||||
|
||||
for sym in file_symbols:
|
||||
# Check budget before starting a new hover request
|
||||
# hover_budget_seconds=0 disables the budget mechanism (the first inequality)
|
||||
if 0 < hover_budget_seconds <= hover_spent_seconds:
|
||||
# symbol_info_budget_seconds=0 disables the budget mechanism (the first inequality)
|
||||
if 0 < symbol_info_budget_seconds <= hover_spent_seconds:
|
||||
skipped_due_to_budget += 1
|
||||
info = None
|
||||
# log once when budget exceeded
|
||||
@@ -685,7 +685,7 @@ class LanguageServerSymbolRetriever:
|
||||
log.debug(
|
||||
f"perf: request_info_for_symbols {total_elapsed_ms=:.2f} {total_symbols=} {skipped_symbols=} "
|
||||
f"{total_hover_lookups=} {hover_cache_hits=} {unique_files=} "
|
||||
f"{hover_budget_seconds=:.1f} {hover_spent_seconds=:.2f} {budget_exceeded=} {skipped_due_to_budget=}"
|
||||
f"{symbol_info_budget_seconds=:.1f} {hover_spent_seconds=:.2f} {budget_exceeded=} {skipped_due_to_budget=}"
|
||||
)
|
||||
|
||||
for file_path, lookup_count, elapsed_ms in per_file_stats:
|
||||
|
||||
@@ -278,14 +278,14 @@ def _make_mock_symbols(count: int, *, relative_path: str = "test_repo/services.p
|
||||
|
||||
@pytest.mark.python
|
||||
class TestHoverBudget:
|
||||
"""Tests for hover_budget time budget behavior."""
|
||||
"""Tests for symbol_info_budget time budget behavior."""
|
||||
|
||||
@pytest.mark.parametrize("language_server", [Language.PYTHON], indirect=True)
|
||||
def test_budget_not_exceeded_all_lookups_performed(self, language_server: SolidLanguageServer, monkeypatch: pytest.MonkeyPatch):
|
||||
"""With a large budget, all hover lookups are performed."""
|
||||
# Create symbol retriever with a mock agent that has large budget
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.serena_config.hover_budget = 10.0
|
||||
mock_agent.serena_config.symbol_info_budget = 10.0
|
||||
mock_agent.get_active_project.return_value = None
|
||||
|
||||
symbol_retriever = LanguageServerSymbolRetriever(language_server, agent=mock_agent)
|
||||
@@ -315,7 +315,7 @@ class TestHoverBudget:
|
||||
"""With a small budget, hover lookups stop and remaining symbols get None info."""
|
||||
# Create symbol retriever with a mock agent that has small budget (0.1s)
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.serena_config.hover_budget = 0.1
|
||||
mock_agent.serena_config.symbol_info_budget = 0.1
|
||||
mock_agent.get_active_project.return_value = None
|
||||
|
||||
symbol_retriever = LanguageServerSymbolRetriever(language_server, agent=mock_agent)
|
||||
@@ -361,7 +361,7 @@ class TestHoverBudget:
|
||||
"""With budget=0, all hover lookups proceed (no early stopping)."""
|
||||
# Create symbol retriever with budget=0 (unlimited)
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.serena_config.hover_budget = 0.0
|
||||
mock_agent.serena_config.symbol_info_budget = 0.0
|
||||
mock_agent.get_active_project.return_value = None
|
||||
|
||||
symbol_retriever = LanguageServerSymbolRetriever(language_server, agent=mock_agent)
|
||||
@@ -390,10 +390,10 @@ class TestHoverBudget:
|
||||
"""Project-level budget overrides global budget."""
|
||||
# Create symbol retriever with global budget 10.0 but project budget 0.05
|
||||
mock_project = MagicMock()
|
||||
mock_project.project_config.hover_budget = 0.05
|
||||
mock_project.project_config.symbol_info_budget = 0.05
|
||||
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.serena_config.hover_budget = 10.0
|
||||
mock_agent.serena_config.symbol_info_budget = 10.0
|
||||
mock_agent.get_active_project.return_value = mock_project
|
||||
|
||||
symbol_retriever = LanguageServerSymbolRetriever(language_server, agent=mock_agent)
|
||||
@@ -432,10 +432,10 @@ class TestHoverBudget:
|
||||
"""When project budget is None, global budget is used."""
|
||||
# Create symbol retriever with project budget=None (inherit global)
|
||||
mock_project = MagicMock()
|
||||
mock_project.project_config.hover_budget = None
|
||||
mock_project.project_config.symbol_info_budget = None
|
||||
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.serena_config.hover_budget = 10.0
|
||||
mock_agent.serena_config.symbol_info_budget = 10.0
|
||||
mock_agent.get_active_project.return_value = mock_project
|
||||
|
||||
symbol_retriever = LanguageServerSymbolRetriever(language_server, agent=mock_agent)
|
||||
|
||||
Reference in new issue
Block a user