From b483bf0159c42335b86ecd2e95712570b4f0ef49 Mon Sep 17 00:00:00 2001 From: Dominik Jain Date: Tue, 8 Sep 2026 13:32:56 +0200 Subject: [PATCH] Structure REPL documentation into tiers; keep facade descriptions compact The REPL overview (tier 0, part of the tool description) now names the result type of methods returning objects that can be processed in code (e.g. `find_symbol -> LspSymbolCollection`), such that type documentation can be requested alongside a facade in one turn. The facade description (tier 1) documents the common operations in full, summarises methods marked as niche (new flag of facade_method; not yet set anywhere) with a pointer to their documentation, and lists result types by name only. Type documentation (tier 2) is provided on request only; provide_info_with_facade remains available but is no longer set for any type. The REPL tool description states this protocol, in particular that facade descriptions do not include result types and that type documentation should only be requested for processing results in code. --- .serena/memories/repl.md | 16 ++++--- src/serena/repl/api/edit_api.py | 2 +- src/serena/repl/api/fs_api.py | 2 +- src/serena/repl/api/jb_api.py | 2 +- src/serena/repl/api/lsp_api.py | 2 +- src/serena/repl/facade.py | 81 ++++++++++++++++++++++++--------- src/serena/repl/repl.py | 8 +++- src/serena/tools/repl_tools.py | 13 ++++-- test/serena/test_repl_tool.py | 30 +++++++++--- 9 files changed, 115 insertions(+), 41 deletions(-) diff --git a/.serena/memories/repl.md b/.serena/memories/repl.md index 8729d804..84f374b1 100644 --- a/.serena/memories/repl.md +++ b/.serena/memories/repl.md @@ -30,14 +30,18 @@ Code runs as a function body (`return` defines the result); a single expression - Progressive disclosure: a priori only facade names, descriptions and method names; `s.info("")` / `s.info(".")` give signature + docstring together, never a signature alone. `info(*items)` documents several items at once; unknown items are reported inline. +- Disclosure tiers: tier 0 (tool description) = facades, descriptions, method names with navigable return types + (`find_symbol -> LspSymbolCollection`); tier 1 (`s.info("")`) = all common methods in full, + `niche` methods (`@facade_method(niche=True)`, rarely needed + long docs) only as summary + pointer, result + types by name only; tier 2 = types on request. Types are never pushed (`provide_info_with_facade` exists but + is set nowhere); the tool description tells the model to request type docs only when processing results in code. - Result types: a type returned by a single method is documented under `:return:`. Types that are shared, contained (`LanguageServerSymbol`) or navigated are declared per facade as `ReferencedType`s (constructor - arg `types=`), with `provide_info_with_facade` (full description in `s.info("")`, else listed by name) - and an optional `members` whitelist (curation for foreign/large classes; listed methods are shown even if - undocumented, convention-derived ones only if documented). Types are documented via `s.info(".")` - or bare `s.info("")`; method docs point to their referenced return type. Result classes declare - attribute annotations at class level (attributes set only in `__init__` are not discoverable). - Annotations are rendered without module paths, so signature names equal lookup names. + arg `types=`), with an optional `members` whitelist (curation for foreign/large classes; listed methods are + shown even if undocumented, convention-derived ones only if documented). Types are documented via + `s.info(".")` or bare `s.info("")`; method docs point to their referenced return type. + Result classes declare attribute annotations at class level (attributes set only in `__init__` are not + discoverable). Annotations are rendered without module paths, so signature names equal lookup names. - APIs must not import `serena.tools` at module level except for tool classes in decorators; tools import APIs locally in `_api()` (API modules refer to tool classes). diff --git a/src/serena/repl/api/edit_api.py b/src/serena/repl/api/edit_api.py index c7222eb9..adac2218 100644 --- a/src/serena/repl/api/edit_api.py +++ b/src/serena/repl/api/edit_api.py @@ -75,7 +75,7 @@ class EditApi(FacadeApi): name="edit", description="modifying content within existing files (independent of the language backend)", types=[ - ReferencedType(ReplacementPreview, provide_info_with_facade=True), + ReferencedType(ReplacementPreview), ReferencedType( ReplacementOccurrence, members=["occurrence_id", "relative_path", "start_line", "end_line", "matched_text", "replacement", "is_ambiguous"], diff --git a/src/serena/repl/api/fs_api.py b/src/serena/repl/api/fs_api.py index 5461d2ac..19b681f4 100644 --- a/src/serena/repl/api/fs_api.py +++ b/src/serena/repl/api/fs_api.py @@ -176,7 +176,7 @@ class FsApi(FacadeApi): types=[ ReferencedType(FileContent), ReferencedType(DirectoryListing), - ReferencedType(PatternMatches, provide_info_with_facade=True), + ReferencedType(PatternMatches), ReferencedType( MatchedConsecutiveLines, members=["source_file_path", "matched_lines", "start_line", "end_line", "to_display_string"] ), diff --git a/src/serena/repl/api/jb_api.py b/src/serena/repl/api/jb_api.py index 66854b61..84e8cb35 100644 --- a/src/serena/repl/api/jb_api.py +++ b/src/serena/repl/api/jb_api.py @@ -186,7 +186,7 @@ class JetBrainsApi(FacadeApi): name="jb", description="operations on the codebase backed by the JetBrains IDE's code intelligence", types=[ - ReferencedType(JetBrainsSymbolCollection, provide_info_with_facade=True), + ReferencedType(JetBrainsSymbolCollection), ReferencedType(JetBrainsSymbolsOverview), ReferencedType(JsonObject), ], diff --git a/src/serena/repl/api/lsp_api.py b/src/serena/repl/api/lsp_api.py index 449fc603..fbecf02e 100644 --- a/src/serena/repl/api/lsp_api.py +++ b/src/serena/repl/api/lsp_api.py @@ -313,7 +313,7 @@ class LspApi(FacadeApi): name="lsp", description="symbol-level operations on the codebase backed by language servers", types=[ - ReferencedType(LspSymbolCollection, provide_info_with_facade=True), + ReferencedType(LspSymbolCollection), ReferencedType( LanguageServerSymbol, members=[ diff --git a/src/serena/repl/facade.py b/src/serena/repl/facade.py index 6e7d1857..caec69fb 100644 --- a/src/serena/repl/facade.py +++ b/src/serena/repl/facade.py @@ -143,6 +143,11 @@ class FacadeMethodInfo: """whether the method is in beta (not yet fully stable)""" can_edit: bool = False """whether the method can modify the codebase (relevant for read-only contexts)""" + niche: bool = False + """ + whether the method is rarely needed, such that the facade's description only summarises it (first line of its + documentation and a pointer to its full documentation) in order to keep the description compact + """ corresponding_tool: "type[Tool] | None" = None """the classic tool offering the same functionality, if any""" @@ -157,7 +162,12 @@ _FACADE_METHOD_INFO_ATTR = "__facade_method_info__" def facade_method( - *, optional: bool = False, beta: bool = False, can_edit: bool = False, corresponding_tool: "type[Tool] | None" = None + *, + optional: bool = False, + beta: bool = False, + can_edit: bool = False, + niche: bool = False, + corresponding_tool: "type[Tool] | None" = None, ) -> Callable[[TCallable], TCallable]: """ Marks a method of a `FacadeApi` as exposed through the facade, attaching the given metadata. @@ -166,13 +176,19 @@ def facade_method( :param optional: whether the method is disabled by default and must be enabled explicitly :param beta: whether the method is in beta :param can_edit: whether the method can modify the codebase + :param niche: whether the method is rarely needed (its documentation is then only summarised in the facade's description) :param corresponding_tool: the classic tool offering the same functionality, if any :return: the decorator """ def decorator(method: TCallable) -> TCallable: info = FacadeMethodInfo( - name=method.__name__, optional=optional, beta=beta, can_edit=can_edit, corresponding_tool=corresponding_tool + name=method.__name__, + optional=optional, + beta=beta, + can_edit=can_edit, + niche=niche, + corresponding_tool=corresponding_tool, ) setattr(method, _FACADE_METHOD_INFO_ATTR, info) return method @@ -282,23 +298,40 @@ class FacadeMethod: def __call__(self, *args: Any, **kwargs: Any) -> Any: return self._implementation(*args, **kwargs) - def describe(self, include_return_type_pointer: bool = True) -> str: + def get_referenced_return_types(self) -> list[ReferencedType]: """ - :param include_return_type_pointer: whether to append a pointer to the documentation of the return type, + :return: the types referenced by the facade which appear in the method's return type annotation + """ + return_annotation = format_annotation(inspect.signature(self._implementation).return_annotation) + return [t for t in self.parent.get_types() if re.search(rf"\b{re.escape(t.name)}\b", return_annotation)] + + def get_summary(self) -> str: + """ + :return: the first line of the method's documentation + """ + doc = inspect.getdoc(self._implementation) + return doc.splitlines()[0] if doc else "(no documentation)" + + def describe(self) -> str: + """ + :return: the method's signature and documentation, with a pointer to the documentation of its return type if it is a type referenced by the facade - :return: the method's signature and documentation """ signature = format_signature(self._implementation) doc = inspect.getdoc(self._implementation) or "(no documentation)" text = f"{self.qualified_name}{signature}\n{doc}\n" - if include_return_type_pointer: - return_annotation = format_annotation(inspect.signature(self._implementation).return_annotation) - referenced = [t for t in self.parent.get_types() if re.search(rf"\b{re.escape(t.name)}\b", return_annotation)] - if referenced: - pointers = ", ".join(f'`s.info("{self.facade_name}.{t.name}")`' for t in referenced) - text += f"Return type: see {pointers}\n" + referenced = self.get_referenced_return_types() + if referenced: + pointers = ", ".join(f'`s.info("{t.name}")`' for t in referenced) + text += f"Return type documentation: {pointers}\n" return text + def describe_summary(self) -> str: + """ + :return: the method's name and the first line of its documentation, with a pointer to its full documentation + """ + return f'{self.qualified_name}: {self.get_summary()} [full documentation: `s.info("{self.qualified_name}")`]\n' + class ApiScope: """ @@ -492,18 +525,24 @@ class Facade: as well as its referenced types (in full if so declared, otherwise by name) """ parts = [f"Facade '{self._name}': {self._description}", ""] - for method in self._methods.values(): - if method.enabled: + enabled_methods = self.get_enabled_methods() + for method in enabled_methods: + if not method.info.niche: parts.append(method.describe()) - described_types = [t for t in self._types.values() if t.provide_info_with_facade] - listed_types = [t for t in self._types.values() if not t.provide_info_with_facade] - for referenced_type in described_types: - parts.append(referenced_type.describe()) - if listed_types: + niche_methods = [m for m in enabled_methods if m.info.niche] + if niche_methods: + parts.append("Rarely needed methods (documented on request):\n" + "".join(m.describe_summary() for m in niche_methods)) + for referenced_type in self._types.values(): + if referenced_type.provide_info_with_facade: + parts.append(referenced_type.describe()) + result_type_names = sorted( + {t.name for m in enabled_methods for t in m.get_referenced_return_types() if not t.provide_info_with_facade} + ) + if result_type_names: parts.append( - "Further types: " - + ", ".join(t.name for t in listed_types) - + f' (request documentation via `s.info("{self._name}.")`)' + "Result types: " + + ", ".join(result_type_names) + + ' (request documentation via `s.info("")` only if you intend to process results in code)' ) return "\n".join(parts) diff --git a/src/serena/repl/repl.py b/src/serena/repl/repl.py index 93f00834..883ab4c7 100644 --- a/src/serena/repl/repl.py +++ b/src/serena/repl/repl.py @@ -54,9 +54,15 @@ class SerenaReplEntrypoint: def overview(self) -> str: """ :return: the list of available facades, each with a one-line description and the names of its methods + (with the result type of methods returning objects that can be processed in code) """ + + def method_entry(method: FacadeMethod) -> str: + return_types = method.get_referenced_return_types() + return method.name + (f" -> {'|'.join(t.name for t in return_types)}" if return_types else "") + return "\n".join( - f"s.{facade.name}: {facade.description}\n methods: {', '.join(facade.enabled_method_names)}" + f"s.{facade.name}: {facade.description}\n methods: {', '.join(method_entry(m) for m in facade.get_enabled_methods())}" for facade in self._facades.values() ) diff --git a/src/serena/tools/repl_tools.py b/src/serena/tools/repl_tools.py index 611241e4..c8e9c2e1 100644 --- a/src/serena/tools/repl_tools.py +++ b/src/serena/tools/repl_tools.py @@ -18,9 +18,16 @@ class SerenaReplTool(Tool, ToolMarkerOptional, ToolMarkerBeta): def apply(self, code: str) -> str: """ Executes the given Python code, which has access to Serena's functionality through the object `s`. - The functionality is organised in facades, which are attributes of `s` (e.g. `s.myfacade`). - Use `s.info()` to list the facades, `s.info("")` to see a facade's methods and - `s.info(".")` for the documentation of a single method. + The functionality is organised in facades, which are attributes of `s` (e.g. `s.myfacade`); the available + facades and their methods are listed below. + + Documentation: Use `s.info("")` when you will use a facade's functionality (it documents all common + operations at once) and `s.info(".")` for a single or a rarely needed operation. Several items + can be requested in one call, e.g. `s.info("lsp", "edit.replace_content")`. + `s.info("")` documents the facade's operations only, not their result types. Result types are given + in the method listing below (`method -> Type`); request their documentation via `s.info("")`, which + includes the types they contain, ONLY if you intend to process results in code (filter, aggregate, chain + calls). If you simply want the result, return it directly: returned objects are rendered for you. The code is executed as the body of a function, so use `return` to define the result; a single expression is evaluated and its value returned directly. diff --git a/test/serena/test_repl_tool.py b/test/serena/test_repl_tool.py index da13d65d..9afe043e 100644 --- a/test/serena/test_repl_tool.py +++ b/test/serena/test_repl_tool.py @@ -53,15 +53,18 @@ class TestReplExecution: assert "name_path_pattern" in method_info def test_type_discovery(self, repl: SerenaRepl) -> None: + # the overview names the result types of methods returning objects that can be processed in code + assert "find_symbol -> LspSymbolCollection" in repl.execute("s.info()") + # signatures render type names without module paths, and point to the documentation of referenced return types method_info = repl.execute('s.info("lsp.find_symbol")') assert "-> LspSymbolCollection" in method_info and "lsp_api." not in method_info - assert 's.info("lsp.LspSymbolCollection")' in method_info + assert 's.info("LspSymbolCollection")' in method_info - # the facade description includes types declared as important in full and lists the others by name + # the facade description documents the operations only and lists the result types by name facade_info = repl.execute('s.info("lsp")') - assert "type LspSymbolCollection" in facade_info and "symbols: list[LanguageServerSymbol]" in facade_info - assert "type LanguageServerSymbol" not in facade_info and "LanguageServerSymbol" in facade_info + assert "type LspSymbolCollection" not in facade_info + assert "Result types: " in facade_info and "LspSymbolCollection" in facade_info # types can be requested via the facade or by bare name, and their curated members are documented type_info = repl.execute('s.info("lsp.LanguageServerSymbol")') @@ -96,6 +99,14 @@ class TestFacade: def extra(self) -> str: return "extra" + @facade_method(niche=True) + def rarely(self, x: int) -> str: + """Rarely needed operation. + + :param x: some parameter + """ + return str(x) + def undecorated(self) -> str: """Public within Serena, but not exposed, since it is not decorated.""" return "internal" @@ -142,13 +153,13 @@ class TestFacade: ApiInclusionDefinition(included_apis=["dummy.add"]), ) facade = Facade.from_api(self.DummyApi(MagicMock()), scope) - assert set(facade.enabled_method_names) == {"add", "secret"} + assert set(facade.enabled_method_names) == {"add", "secret", "rarely"} def test_api_scope_read_only_excludes_editing_methods(self) -> None: scope = self._scope(included_apis=["dummy.secret"]) scope.exclude_editing() facade = Facade.from_api(self.DummyApi(MagicMock()), scope) - assert facade.enabled_method_names == ["add"] + assert set(facade.enabled_method_names) == {"add", "rarely"} def test_enabled_methods_delegate_to_implementation(self) -> None: facade = Facade.from_api(self.DummyApi(MagicMock()), ApiScope()) @@ -176,6 +187,13 @@ class TestFacade: assert "undecorated" not in facade.describe() assert "undecorated" not in facade.enabled_method_names + def test_niche_methods_are_summarised_in_facade_description(self) -> None: + facade = Facade.from_api(self.DummyApi(MagicMock()), ApiScope()) + description = facade.describe() + assert "dummy.rarely: Rarely needed operation." in description + assert ":param x:" not in description # only the summary, no signature or full documentation + assert ":param x:" in facade.describe_member("rarely") # full documentation on request + def test_optional_methods_are_disabled_by_default(self) -> None: facade = Facade.from_api(self.DummyApi(MagicMock()), ApiScope()) assert "extra" not in facade.enabled_method_names