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.
This commit is contained in:
Dominik Jain authored and Dominik Jain committed 2026-09-15 12:50:44 +02:00
1 parent 6cb3d37e09
commit b483bf0159
9 files changed
+115 -41

No files matched your search

+10 -6
View File
@@ -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("<facade>")` /
`s.info("<facade>.<method>")` 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("<facade>")`) = 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("<facade>")`, 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("<facade>.<Type>")`
or bare `s.info("<Type>")`; 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("<facade>.<Type>")` or bare `s.info("<Type>")`; 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).
+1 -1
View File
@@ -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"],
+1 -1
View File
@@ -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"]
),
+1 -1
View File
@@ -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),
],
+1 -1
View File
@@ -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=[
+60 -21
View File
@@ -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}.<type name>")`)'
"Result types: "
+ ", ".join(result_type_names)
+ ' (request documentation via `s.info("<type name>")` only if you intend to process results in code)'
)
return "\n".join(parts)
+7 -1
View File
@@ -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()
)
+10 -3
View File
@@ -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("<facade>")` to see a facade's methods and
`s.info("<facade>.<method>")` 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("<facade>")` when you will use a facade's functionality (it documents all common
operations at once) and `s.info("<facade>.<method>")` 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("<facade>")` 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("<Type>")`, 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.
+24 -6
View File
@@ -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