Skip to content

Commit c6fbd1c

Browse files
authored
Merge pull request #2073 from oraios/change-session-handling
Make session IDs explicit tool parameters instead of auto-injecting them
2 parents bd2712f + 7a16b3d commit c6fbd1c

8 files changed

Lines changed: 37 additions & 50 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ Status of the `main` branch. Changes prior to the next official version change w
3434
the project's root path, so a `<project root>/**` entry matches only paths below the root and therefore
3535
trusts no project at all; the template now shows the bare root form alongside the parent-directory
3636
glob (#2001)
37+
- Session IDs are now created and tracked internally by Serena instead of being derived from the
38+
MCP session, since the MCP SDK v2 no longer provides session identifiers and client session usage
39+
was inconsistent anyway. Tools that need a session id (e.g. `activate_project`, the REPL tool) now
40+
take it as an explicit parameter, obtained from `initial_instructions`
3741

3842
* CLI:
3943
- Fix: `project health-check` reported `Health check passed - All tools working correctly` and

‎src/serena/agent.py‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1156,15 +1156,18 @@ def get_session(self, session_id: str) -> SerenaSession:
11561156
"""
11571157
return self._session_registry.get_session(session_id)
11581158

1159-
def create_system_prompt(self, session_id: str = "global") -> str:
1159+
def create_system_prompt(self) -> str:
11601160
"""
11611161
Returns the 'Serena Instructions Manual', i.e. Serena's system prompt.
11621162
The prompt also establishes a new Serena session (see `SerenaSession`), stating its id for use with tools
1163-
which require it (e.g. the REPL tool).
1163+
which require it (e.g. the REPL tool and project activation tool).
11641164
1165-
:param session_id: the client session ID for the case where this is run from a tool; "global" for the connection time case
11661165
:return: the prompt
11671166
"""
1167+
# establish a Serena session
1168+
serena_session = self.create_session()
1169+
session_id = serena_session.session_id
1170+
11681171
global_memories = self._create_global_memory_manager().list_global_memories()
11691172
global_memories_str = dict_string(global_memories.to_dict()) if len(global_memories) > 0 else ""
11701173

@@ -1193,10 +1196,9 @@ def create_system_prompt(self, session_id: str = "global") -> str:
11931196
elif self._project_activation_error:
11941197
system_prompt += f"\n\nNo project is active ({self._project_activation_error})."
11951198

1196-
# establish a Serena session and state its id, which the LLM must pass to tools which require it
1197-
serena_session = self.create_session()
1199+
# inform about the session id
11981200
system_prompt += "\n\n" + self._format_prompt_tag(
1199-
f"Your Serena session id is `{serena_session.session_id}`. Pass it as the `session` parameter to tools which require it.",
1201+
f"Your Serena session id is `{session_id}`. Pass it as the `session_id` parameter to tools which require it.",
12001202
tag="session",
12011203
)
12021204

‎src/serena/session.py‎

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,15 @@ def __init__(self, max_sessions: int = 100, idle_ttl_seconds: float = 6 * 3600)
4848
self._idle_ttl_seconds = idle_ttl_seconds
4949
self._sessions: OrderedDict[str, SerenaSession] = OrderedDict()
5050

51+
@staticmethod
52+
def _next_session_id() -> str:
53+
return secrets.token_hex(4)
54+
5155
def create_session(self) -> SerenaSession:
5256
"""
5357
:return: a new session with a random id
5458
"""
55-
return self.get_session(secrets.token_hex(4))
59+
return self.get_session(self._next_session_id())
5660

5761
def get_session(self, session_id: str) -> SerenaSession:
5862
"""

‎src/serena/tools/config_tools.py‎

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,13 +41,12 @@ class ActivateProjectTool(Tool, ToolMarkerDoesNotRequireActiveProject):
4141
Activates a project based on the project name or path.
4242
"""
4343

44-
# noinspection PyIncorrectDocstring
45-
# (session_id is injected via apply_ex)
4644
def apply(self, project: str, session_id: str) -> str:
4745
"""
4846
Activates the project with the given name or path.
4947
5048
:param project: the name of a registered project to activate or a path to a project directory
49+
:param session_id: your Serena session id, as provided in Serena's instructions (call `initial_instructions` if you do not have one)
5150
"""
5251
is_new_activation = self.agent.activate_project_from_path_or_name(project)
5352
mark_used(is_new_activation)

‎src/serena/tools/repl_tools.py‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def get_apply_docstring(self) -> str:
2020
docs += "\n\nAvailable facades are provided at project activation"
2121
return docs
2222

23-
def apply(self, session: str, code: str) -> str:
23+
def apply(self, session_id: str, code: str) -> str:
2424
"""
2525
Executes the given Python code, which has access to Serena's functionality through the object `s`.
2626
The functionality is organised in facades, which are attributes of `s` (e.g. `s.myfacade`).
@@ -47,8 +47,8 @@ def apply(self, session: str, code: str) -> str:
4747
`s.vars()` lists the persisted items, `s.clear()` removes them. Do not store facades (`s.<facade>`) in
4848
variables; access them via `s` at call time. Do not keep large results longer than needed.
4949
50-
:param session: your Serena session id, as provided in Serena's instructions (call `initial_instructions` if you do not have one)
50+
:param session_id: your Serena session id, as provided in Serena's instructions (call `initial_instructions` if you do not have one)
5151
:param code: the Python code to execute
5252
:return: the representation of the returned value, or the error if execution failed
5353
"""
54-
return self.agent.get_repl().execute(code, self.agent.get_session(session))
54+
return self.agent.get_repl().execute(code, self.agent.get_session(session_id))

‎src/serena/tools/tools_base.py‎

Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55
from abc import ABC
66
from collections.abc import Callable, Iterable
77
from dataclasses import dataclass
8-
from functools import cached_property
98
from typing import TYPE_CHECKING, Any, Protocol, TypeVar, cast
109

1110
from mcp import Implementation
@@ -140,32 +139,12 @@ class Tool(Component):
140139
# (which is use by the LLM, so a good description is important)
141140
# and to validate the tool call arguments.
142141

143-
SESSION_ID_PARAM_NAME = "session_id"
144-
"""
145-
parameter name to use in apply method for the client session ID.
146-
This parameter will be ignored by the MCP interface but will be populated with the session ID of the current client session
147-
when the tool is called, allowing tools to be session-aware if needed.
148-
"""
149-
150142
_last_tool_call_client_str: str | None = None
151143
"""We can only get the client info from within a tool call. Each tool call will update this variable."""
152144

153145
def __init__(self, agent: "SerenaAgent"):
154146
super().__init__(agent)
155147

156-
@cached_property
157-
def _is_session_aware(self) -> bool:
158-
"""
159-
:return: whether the tool is session-aware, i.e. whether the apply method expects a session_id (str) parameter.
160-
"""
161-
# check apply method for session_id arg
162-
apply_fn = self.get_apply_fn()
163-
sig = inspect.signature(apply_fn)
164-
for param in sig.parameters.values():
165-
if param.name == self.SESSION_ID_PARAM_NAME:
166-
return True
167-
return False
168-
169148
@staticmethod
170149
def _sanitize_input_param(raw_param: str) -> str:
171150
# some clients replace < and > with their escaped html versions, we need to counteract this
@@ -254,9 +233,9 @@ def get_apply_fn_metadata_from_cls(cls, structured_output: bool | None = None) -
254233
if apply_fn is None:
255234
raise AttributeError(f"apply method not defined in {cls}. Did you forget to implement it?")
256235

257-
return func_metadata(apply_fn, skip_names=["self", "cls", cls.SESSION_ID_PARAM_NAME], structured_output=structured_output)
236+
return func_metadata(apply_fn, skip_names=["self", "cls"], structured_output=structured_output)
258237

259-
def _log_tool_application(self, frame: Any, session_id: str) -> None:
238+
def _log_tool_application(self, frame: Any) -> None:
260239
params = {}
261240
ignored_params = {"self", "log_call", "catch_exceptions", "args", "apply_fn"}
262241
for param, value in frame.f_locals.items():
@@ -266,7 +245,7 @@ def _log_tool_application(self, frame: Any, session_id: str) -> None:
266245
params.update(value)
267246
else:
268247
params[param] = value
269-
log.info(f"{self.get_name_from_cls()}: {dict_string(params)}; session_id: {session_id}")
248+
log.info(f"{self.get_name_from_cls()}: {dict_string(params)}")
270249

271250
def _resolve_max_answer_chars(self, max_answer_chars: int) -> int:
272251
"""
@@ -321,10 +300,8 @@ def apply_ex(self, log_call: bool = True, catch_exceptions: bool = True, mcp_ctx
321300
:param catch_exceptions: whether to catch exceptions and return their messages as strings, instead of raising a ToolCallError
322301
"""
323302
# obtain session ID and client info
324-
session_id = "global"
325303
if mcp_ctx is not None:
326304
try:
327-
session_id = "%x" % id(mcp_ctx.session)
328305
client_params = mcp_ctx.session.client_params
329306
if client_params is not None:
330307
client_info = cast(Implementation, client_params.clientInfo)
@@ -345,7 +322,7 @@ def task() -> str:
345322
)
346323

347324
if log_call:
348-
self._log_tool_application(inspect.currentframe(), session_id)
325+
self._log_tool_application(inspect.currentframe())
349326

350327
# check whether the tool requires an active project and language server
351328
if not isinstance(self, ToolMarkerDoesNotRequireActiveProject):
@@ -357,8 +334,6 @@ def task() -> str:
357334

358335
# construct apply kwargs, adding session_id if the tool is session-aware
359336
apply_kwargs = dict(kwargs)
360-
if self._is_session_aware:
361-
apply_kwargs["session_id"] = session_id
362337

363338
# apply the actual tool
364339
try:

‎src/serena/tools/workflow_tools.py‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -31,15 +31,13 @@ class InitialInstructionsTool(Tool, ToolMarkerDoesNotRequireActiveProject):
3131
for clients that do not read the initial instructions when the MCP server is connected.
3232
"""
3333

34-
# noinspection PyIncorrectDocstring
35-
# (session_id is injected via apply_ex)
36-
def apply(self, session_id: str) -> str:
34+
def apply(self) -> str:
3735
"""
3836
Provides the 'Serena Instructions Manual', which contains essential information on how to use the Serena toolbox.
3937
IMPORTANT: If you have not yet read the manual, call this tool immediately after you are given your task by the user,
4038
as it will critically inform you!
4139
"""
42-
return self.agent.create_system_prompt(session_id=session_id)
40+
return self.agent.create_system_prompt()
4341

4442

4543
class SerenaInfoTool(Tool, ToolMarkerOptional, ToolMarkerDoesNotRequireActiveProject):

‎test/serena/test_serena_agent.py‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
from serena.config.serena_config import AgentInterface, ProjectConfig, RegisteredProject, SerenaConfig
1818
from serena.lsp.lsp_diagnostics import DiagnosticsContext
1919
from serena.project import Project
20+
from serena.session import SessionRegistry
2021
from serena.tools import (
2122
SUCCESS_RESULT,
2223
ActivateProjectTool,
@@ -1400,13 +1401,17 @@ def test_safe_delete_symbol_succeeds_when_no_references(self, serena_agent: Sere
14001401

14011402

14021403
class TestPromptProvision:
1403-
class MockContext:
1404-
def __init__(self, session_id: str):
1405-
self.session = session_id
1406-
14071404
@classmethod
14081405
def _call_tool(cls, agent: SerenaAgent, tool_class: type[Tool], session_id: str = "global", **kwargs) -> str:
1409-
result = agent.get_tool(tool_class).apply_ex(mcp_ctx=cls.MockContext(session_id), catch_exceptions=False, **kwargs)
1406+
old_method = SessionRegistry._next_session_id
1407+
if tool_class == InitialInstructionsTool:
1408+
SessionRegistry._next_session_id = lambda x: session_id # type: ignore
1409+
else:
1410+
kwargs["session_id"] = session_id
1411+
try:
1412+
result = agent.get_tool(tool_class).apply_ex(catch_exceptions=False, **kwargs)
1413+
finally:
1414+
SessionRegistry._next_session_id = old_method
14101415
return result
14111416

14121417
@staticmethod

0 commit comments

Comments
 (0)