diff --git a/src/serena/config/serena_config.py b/src/serena/config/serena_config.py index 4702444d..47e4e1d6 100644 --- a/src/serena/config/serena_config.py +++ b/src/serena/config/serena_config.py @@ -171,7 +171,7 @@ class ProjectConfig(ToolInclusionDefinition, ModeSelectionDefinition, ToStringMi ignore_all_files_in_gitignore: bool = True initial_prompt: str = "" encoding: str = DEFAULT_SOURCE_FILE_ENCODING - include_info_hover_budget_seconds: float | None = None + hover_budget: float | None = None """ Per-project override for include_info hover budget (seconds). @@ -329,16 +329,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 include_info_hover_budget_seconds - hover_budget_raw = data["include_info_hover_budget_seconds"] + # Validate hover_budget + hover_budget_raw = data["hover_budget"] hover_budget = hover_budget_raw if hover_budget is not None: try: hover_budget = float(hover_budget_raw) except (TypeError, ValueError) as e: - raise ValueError(f"include_info_hover_budget_seconds must be a number or null, got: {hover_budget_raw}") from 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"include_info_hover_budget_seconds cannot be negative, got: {hover_budget}") + raise ValueError(f"hover_budget cannot be negative, got: {hover_budget}") return cls( project_name=data["project_name"], @@ -353,7 +353,7 @@ class ProjectConfig(ToolInclusionDefinition, ModeSelectionDefinition, ToStringMi encoding=data["encoding"], base_modes=data["base_modes"], default_modes=data["default_modes"], - include_info_hover_budget_seconds=hover_budget, + hover_budget=hover_budget, ) def _to_yaml_dict(self) -> dict: @@ -511,7 +511,7 @@ class SerenaConfig(ToolInclusionDefinition, ModeSelectionDefinition, ToStringMix ls_specific_settings: dict = field(default_factory=dict) """Advanced configuration option allowing to configure language server implementation specific options, see SolidLSPSettings for more info.""" - include_info_hover_budget_seconds: float = 5.0 + hover_budget: float = 5.0 """ Time budget (seconds) for LSP hover requests when tools request include_info. @@ -660,15 +660,15 @@ class SerenaConfig(ToolInclusionDefinition, ModeSelectionDefinition, ToStringMix instance.log_level = loaded_commented_yaml["gui_log_level"] del loaded_commented_yaml["gui_log_level"] - # Load and validate include_info_hover_budget_seconds - hover_budget_raw = get_value_or_default("include_info_hover_budget_seconds") + # Load and validate hover_budget + hover_budget_raw = get_value_or_default("hover_budget") try: hover_budget = float(hover_budget_raw) except (TypeError, ValueError) as e: - raise ValueError(f"include_info_hover_budget_seconds must be a number, got: {hover_budget_raw}") from e + raise ValueError(f"hover_budget must be a number, got: {hover_budget_raw}") from e if hover_budget < 0: - raise ValueError(f"include_info_hover_budget_seconds cannot be negative, got: {hover_budget}") - instance.include_info_hover_budget_seconds = hover_budget + raise ValueError(f"hover_budget cannot be negative, got: {hover_budget}") + instance.hover_budget = hover_budget # re-save the configuration file if any migrations were performed if num_migrations > 0: diff --git a/src/serena/resources/project.template.yml b/src/serena/resources/project.template.yml index ee126f56..af563037 100644 --- a/src/serena/resources/project.template.yml +++ b/src/serena/resources/project.template.yml @@ -110,7 +110,6 @@ default_modes: # (contrary to the memories, which are loaded on demand). initial_prompt: "" -# Per-project override for include_info hover budget (seconds). -# If null or missing, global config is used. -# 0 disables the budget (no early stopping). Negative values are invalid. -include_info_hover_budget_seconds: null +# Override of the correspounding setting in serena_config.yml, see the documentation there. +# If null or missing, the value from the global config is used. +hover_budget: null diff --git a/src/serena/resources/serena_config.template.yml b/src/serena/resources/serena_config.template.yml index b8d0c3b3..501e32be 100644 --- a/src/serena/resources/serena_config.template.yml +++ b/src/serena/resources/serena_config.template.yml @@ -103,10 +103,14 @@ default_max_tool_answer_chars: 150000 # estimate the token count using the Claude Sonnet 4 tokenizer. token_count_estimator: CHAR_COUNT -# 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. +# Time budget (seconds) budget for LSP request_hover calls within an MCP tool call (in seconds). +# 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. -include_info_hover_budget_seconds: 5 +# Can be overridden in project.yml. +hover_budget: 10 # the list of registered project paths (updated automatically). projects: [] diff --git a/src/serena/symbol.py b/src/serena/symbol.py index 71d87677..a15e4731 100644 --- a/src/serena/symbol.py +++ b/src/serena/symbol.py @@ -552,17 +552,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 = 5) -> float: + def _get_hover_budget(self, default_budget: float = 10) -> float: """Project -> global -> default""" - hover_budget_seconds = default_budget + hover_budget = default_budget if self.agent is not None: - hover_budget_seconds = self.agent.serena_config.include_info_hover_budget_seconds + hover_budget = self.agent.serena_config.hover_budget active_project = self.agent.get_active_project() if active_project is not None: - project_hover_budget = active_project.project_config.include_info_hover_budget_seconds + project_hover_budget = active_project.project_config.hover_budget if project_hover_budget is not None: - hover_budget_seconds = project_hover_budget - return hover_budget_seconds + hover_budget = project_hover_budget + return hover_budget def request_info_for_symbol_batch( self, @@ -577,7 +577,7 @@ class LanguageServerSymbolRetriever: 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 (include_info_hover_budget_seconds) limits total time spent on hover + The hover budget (hover_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 @@ -617,7 +617,8 @@ class LanguageServerSymbolRetriever: file_hover_lookups = 0 for sym in file_symbols: - # Check budget before starting new hover request + # 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: skipped_due_to_budget += 1 info = None diff --git a/test/serena/config/test_serena_config.py b/test/serena/config/test_serena_config.py index 3bfefa09..79bb905d 100644 --- a/test/serena/config/test_serena_config.py +++ b/test/serena/config/test_serena_config.py @@ -127,7 +127,7 @@ class TestProjectConfig: class TestProjectConfigHoverBudget: - """Test class for include_info_hover_budget_seconds validation in ProjectConfig.""" + """Test class for hover_budget validation in ProjectConfig.""" def _base_project_dict(self, **overrides): data = { @@ -140,7 +140,7 @@ class TestProjectConfigHoverBudget: "ignore_all_files_in_gitignore": True, "initial_prompt": "", "encoding": "utf-8", - "include_info_hover_budget_seconds": None, + "hover_budget": None, } data.update(overrides) return data @@ -149,23 +149,23 @@ class TestProjectConfigHoverBudget: """Test that _from_dict accepts null hover budget (for inheritance).""" data = self._base_project_dict() config = ProjectConfig._from_dict(data) - assert config.include_info_hover_budget_seconds is None + assert config.hover_budget is None def test_from_dict_accepts_positive_budget(self): """Test that _from_dict accepts positive hover budget.""" - data = self._base_project_dict(include_info_hover_budget_seconds=10.5) + data = self._base_project_dict(hover_budget=10.5) config = ProjectConfig._from_dict(data) - assert config.include_info_hover_budget_seconds == 10.5 + assert config.hover_budget == 10.5 def test_from_dict_accepts_zero_budget(self): """Test that _from_dict accepts zero hover budget (disabled).""" - data = self._base_project_dict(include_info_hover_budget_seconds=0) + data = self._base_project_dict(hover_budget=0) config = ProjectConfig._from_dict(data) - assert config.include_info_hover_budget_seconds == 0.0 + assert config.hover_budget == 0.0 def test_from_dict_rejects_negative_budget(self): """Test that _from_dict raises ValueError for negative hover budget.""" - data = self._base_project_dict(include_info_hover_budget_seconds=-1.0) + data = self._base_project_dict(hover_budget=-1.0) with pytest.raises(ValueError) as exc_info: ProjectConfig._from_dict(data) assert "cannot be negative" in str(exc_info.value) @@ -174,14 +174,14 @@ class TestProjectConfigHoverBudget: """Test that _apply_defaults_to_dict preserves None for hover budget.""" data = {"project_name": "test", "languages": ["python"]} result = ProjectConfig._apply_defaults_to_dict(data) - assert result["include_info_hover_budget_seconds"] is None + assert result["hover_budget"] is None def test_apply_defaults_preserves_explicit_value(self): """Test that _apply_defaults_to_dict preserves explicit hover budget value.""" data = { "project_name": "test", "languages": ["python"], - "include_info_hover_budget_seconds": 15.0, + "hover_budget": 15.0, } result = ProjectConfig._apply_defaults_to_dict(data) - assert result["include_info_hover_budget_seconds"] == 15.0 + assert result["hover_budget"] == 15.0 diff --git a/test/serena/test_symbol.py b/test/serena/test_symbol.py index 865d603a..94353f46 100644 --- a/test/serena/test_symbol.py +++ b/test/serena/test_symbol.py @@ -256,14 +256,14 @@ def _make_mock_symbols(count: int, *, relative_path: str = "test_repo/services.p @pytest.mark.python class TestHoverBudget: - """Tests for include_info_hover_budget_seconds time budget behavior.""" + """Tests for hover_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.include_info_hover_budget_seconds = 10.0 + mock_agent.serena_config.hover_budget = 10.0 mock_agent.get_active_project.return_value = None symbol_retriever = LanguageServerSymbolRetriever(language_server, agent=mock_agent) @@ -293,7 +293,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.include_info_hover_budget_seconds = 0.1 + mock_agent.serena_config.hover_budget = 0.1 mock_agent.get_active_project.return_value = None symbol_retriever = LanguageServerSymbolRetriever(language_server, agent=mock_agent) @@ -339,7 +339,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.include_info_hover_budget_seconds = 0.0 + mock_agent.serena_config.hover_budget = 0.0 mock_agent.get_active_project.return_value = None symbol_retriever = LanguageServerSymbolRetriever(language_server, agent=mock_agent) @@ -368,10 +368,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.include_info_hover_budget_seconds = 0.05 + mock_project.project_config.hover_budget = 0.05 mock_agent = MagicMock() - mock_agent.serena_config.include_info_hover_budget_seconds = 10.0 + mock_agent.serena_config.hover_budget = 10.0 mock_agent.get_active_project.return_value = mock_project symbol_retriever = LanguageServerSymbolRetriever(language_server, agent=mock_agent) @@ -410,10 +410,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.include_info_hover_budget_seconds = None + mock_project.project_config.hover_budget = None mock_agent = MagicMock() - mock_agent.serena_config.include_info_hover_budget_seconds = 10.0 + mock_agent.serena_config.hover_budget = 10.0 mock_agent.get_active_project.return_value = mock_project symbol_retriever = LanguageServerSymbolRetriever(language_server, agent=mock_agent)