mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-14 20:17:42 +00:00
Add sessions to the REPL; document contained types once per session
Type documentation now transitively includes the declared types a type's members reference, such that e.g. LspSymbolCollection's documentation includes LanguageServerSymbol. To avoid repeating widely shared types, the REPL tracks per session which types have been documented: a contained type documented earlier in the session is only pointed to (an explicit request always yields it in full). Since MCP provides no reliable session identification and clients keep a stdio server across conversations, the REPL's session identity is supplied by the LLM: the instructions (system prompt) establish a SerenaSession and state its id, and the REPL tool takes a required session_id parameter. Sessions are managed by a SessionRegistry (creation on demand, LRU eviction). Tools which may be called before the instructions have been read (activate_project, initial_instructions) are unchanged.
This commit is contained in:
1 parent
b067f5f71d
commit
24b41dcb88
10 files changed
+229
-21
No files matched your search
@@ -11,6 +11,7 @@ from serena.config.serena_config import ApiInclusionDefinition
|
||||
from serena.repl.api.lsp_api import LspApi
|
||||
from serena.repl.facade import ApiScope, Facade, FacadeApi, FacadeMethodInfo, facade_method
|
||||
from serena.repl.repl import SerenaRepl
|
||||
from serena.session import SerenaSession
|
||||
from serena.tools import FindSymbolTool, SerenaReplTool
|
||||
from solidlsp.ls_config import LanguageServerId
|
||||
from test.conftest import agent_for_project_context
|
||||
@@ -71,7 +72,24 @@ class TestReplExecution:
|
||||
assert type_info == repl.execute('s.info("LanguageServerSymbol")')
|
||||
assert "get_name_path() -> str" in type_info and "iter_children()" in type_info
|
||||
assert "to_dict" not in type_info # not among the curated members
|
||||
assert "represent" not in repl.execute('s.info("LspSymbolCollection")') # the representation mechanism is not exposed
|
||||
assert "represent()" not in repl.execute('s.info("LspSymbolCollection")') # the representation mechanism is not exposed
|
||||
|
||||
def test_contained_types_are_documented_once_per_session(self, repl: SerenaRepl) -> None:
|
||||
session = SerenaSession("test")
|
||||
|
||||
# a type's documentation includes the types it contains (transitively)
|
||||
first = repl.execute('s.info("LspReferenceCollection")', session)
|
||||
assert "type LspReferenceCollection" in first
|
||||
assert "type ReferenceInLanguageServerSymbol" in first and "type LanguageServerSymbol" in first
|
||||
|
||||
# a contained type documented earlier in the session is only pointed to; an explicit request yields it again
|
||||
second = repl.execute('s.info("LspSymbolCollection")', session)
|
||||
assert "type LspSymbolCollection" in second
|
||||
assert "type LanguageServerSymbol: documented earlier" in second and "get_name_path()" not in second
|
||||
assert "get_name_path()" in repl.execute('s.info("LanguageServerSymbol")', session)
|
||||
|
||||
# another session is unaffected
|
||||
assert "get_name_path()" in repl.execute('s.info("LspSymbolCollection")', SerenaSession("other"))
|
||||
|
||||
def test_info_documents_several_items(self, repl: SerenaRepl) -> None:
|
||||
info = repl.execute('s.info("lsp.find_symbol", "nope", "lsp.LspSymbolCollection")')
|
||||
@@ -237,14 +255,16 @@ class TestLspFacade:
|
||||
def test_find_symbol_via_repl(self) -> None:
|
||||
with agent_for_project_context(LanguageServerId.PYTHON) as agent:
|
||||
tool = agent.get_tool(SerenaReplTool)
|
||||
session_id = agent.create_session().session_id
|
||||
|
||||
# a returned collection is rendered, identifying the symbol and its file
|
||||
rendered = tool.apply('return s.lsp.find_symbol("create_user")')
|
||||
rendered = tool.apply(session_id, 'return s.lsp.find_symbol("create_user")')
|
||||
assert "create_user" in rendered
|
||||
assert "services.py" in rendered
|
||||
|
||||
# the underlying symbols are accessible from code, e.g. to retrieve a body without rendering the collection
|
||||
body = tool.apply(
|
||||
f'result = s.lsp.find_symbol("create_user", relative_path={self._SERVICES_FILE!r})\nreturn result.symbols[0].body'
|
||||
session_id,
|
||||
f'result = s.lsp.find_symbol("create_user", relative_path={self._SERVICES_FILE!r})\nreturn result.symbols[0].body',
|
||||
)
|
||||
assert body.startswith("def create_user")
|
||||
@@ -917,12 +917,18 @@ class TestSerenaAgent:
|
||||
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()")
|
||||
assert "s.lsp" in agent.get_tool(SerenaReplTool).apply(agent.create_session().session_id, "s.info()")
|
||||
|
||||
# prompts refer to operations by their qualified REPL names, e.g. `lsp.find_symbol` instead of the tool name
|
||||
system_prompt = agent.create_system_prompt()
|
||||
assert "`lsp.find_symbol`" in system_prompt
|
||||
assert "`find_symbol`" not in system_prompt
|
||||
|
||||
# the instructions establish a session, whose id can be used with session-aware tools
|
||||
session_id_match = re.search(r"session id is `(\w+)`", system_prompt)
|
||||
assert session_id_match is not None
|
||||
session_id = session_id_match.group(1)
|
||||
assert "s.lsp" in agent.get_tool(SerenaReplTool).apply(session_id, "s.info()")
|
||||
finally:
|
||||
agent.on_shutdown(timeout=5)
|
||||
|
||||
|
||||
Reference in new issue
Block a user