Improve read-only memory handling, introducing MemoriesList

This commit is contained in:
Dominik Jain authored and Dominik Jain committed 2026-03-06 23:47:10 +01:00
1 parent 57fc582606
commit 4dc617ae6b
8 files changed
+65 -38

No files matched your search

+7 -4
View File
@@ -96,17 +96,16 @@ initial_prompt: |
rather than mere functions (i.e. use the strategy pattern, for example).
You avoid the use of low-level data structures in all cases where an object-oriented abstraction would be more appropriate.
For simple data storage, you use dataclasses instead of dictionaries or tuples.
You structure function implementations into functional blocks that are separated by blank lines.
Atop each functional block, you write an elliptical phrase (starting with lower-case letter) that describes the purpose of the
block in a concise manner.
Docstrings: You consistently use reStructuredText.
Comments:
When describing parameters, methods/functions and classes, you use a precise style, where the initial (elliptical) phrase
clearly defines *what* it is. Any details then follow in subsequent sentences.
# the encoding used by text files in the project
# For a list of possible encodings, see https://docs.python.org/3.11/library/codecs.html#standard-encodings
encoding: utf-8
@@ -143,3 +142,7 @@ symbol_info_budget:
# Note: the backend is fixed at startup. If a project with a different backend
# is activated post-init, an error will be returned.
language_backend:
# list of regex patterns which, when matched, mark a memory entry as read‑only.
# Extends the list from the global configuration, merging the two lists.
read_only_memory_patterns: []
+4 -4
View File
@@ -13,7 +13,7 @@ from typing import TYPE_CHECKING, Optional, TypeVar
from sensai.util import logging
from sensai.util.logging import LogTime
from sensai.util.string import TextBuilder, list_string
from sensai.util.string import TextBuilder, dict_string
from interprompt.jinja_template import JinjaTemplate
from serena import serena_version
@@ -603,17 +603,17 @@ class SerenaAgent:
def create_system_prompt(self) -> str:
available_tools = self._active_tools
available_markers = available_tools.tool_marker_names
global_memory_names = MemoriesManager(
global_memories = MemoriesManager(
serena_data_folder=None, read_only_memory_patterns=self.serena_config.read_only_memory_patterns
).list_global_memories()
global_memories_list = list_string(global_memory_names) if global_memory_names else ""
global_memories_str = dict_string(global_memories.to_dict()) if len(global_memories) > 0 else ""
log.info("Generating system prompt with available_tools=(see active tools), available_markers=%s", available_markers)
system_prompt = self.prompt_factory.create_system_prompt(
context_system_prompt=self._format_prompt(self._context.prompt),
mode_system_prompts=[self._format_prompt(mode.prompt) for mode in self.get_active_modes()],
available_tools=available_tools.tool_names,
available_markers=available_markers,
global_memories_list=global_memories_list,
global_memories_list=global_memories_str,
)
# If a project is active at startup, append its activation message
+1 -1
View File
@@ -501,7 +501,7 @@ class SerenaDashboardAPI:
# Get available memories if ReadMemoryTool is active
available_memories = None
if self._agent.tool_is_active("read_memory") and project is not None:
available_memories = project.memories_manager.list_memories()
available_memories = project.memories_manager.list_memories().get_full_list()
# Get list of languages for the active project
languages = []
+46 -19
View File
@@ -45,8 +45,8 @@ class MemoriesManager:
self._read_only_memory_patterns = [re.compile(pattern) for pattern in set(read_only_memory_patterns)]
def _is_read_only_memory(self, name: str) -> bool:
for read_only_memory in self._read_only_memory_patterns:
if read_only_memory.match(name):
for pattern in self._read_only_memory_patterns:
if pattern.fullmatch(name):
return True
return False
@@ -87,7 +87,7 @@ class MemoriesManager:
return self._project_memory_dir / filename
def _check_write_access(self, name: str, is_tool_context: bool) -> None:
# in tool context, global memory write access can be disabled
# in tool context, memories can be read-only
if is_tool_context and self._is_read_only_memory(name):
raise PermissionError(f"Attempted to write to read_only memory: '{name}')")
@@ -105,38 +105,64 @@ class MemoriesManager:
f.write(content)
return f"Memory {name} written."
def _list_memories(self, search_dir: Path, base_dir: Path, prefix: str = "") -> list[str]:
class MemoriesList:
def __init__(self) -> None:
self.memories: list[str] = []
self.read_only_memories: list[str] = []
def __len__(self) -> int:
return len(self.memories) + len(self.read_only_memories)
def add(self, memory_name: str, is_read_only: bool) -> None:
if is_read_only:
self.read_only_memories.append(memory_name)
else:
self.memories.append(memory_name)
def extend(self, other: "MemoriesManager.MemoriesList") -> None:
self.memories.extend(other.memories)
self.read_only_memories.extend(other.read_only_memories)
def to_dict(self) -> dict[str, list[str]]:
result = {}
if self.memories:
result["memories"] = sorted(self.memories)
if self.read_only_memories:
result["read_only_memories"] = sorted(self.read_only_memories)
return result
def get_full_list(self) -> list[str]:
return sorted(self.memories + self.read_only_memories)
def _list_memories(self, search_dir: Path, base_dir: Path, prefix: str = "") -> MemoriesList:
result = self.MemoriesList()
if not search_dir.exists():
return []
results = []
read_only_postfix = " (read only)"
return result
for md_file in search_dir.rglob("*.md"):
rel = str(md_file.relative_to(base_dir).with_suffix("")).replace(os.sep, "/")
memory_name = prefix + rel
if self._is_read_only_memory(memory_name):
memory_name += read_only_postfix
results.append(memory_name)
return results
result.add(memory_name, is_read_only=self._is_read_only_memory(memory_name))
return result
def list_global_memories(self, subtopic: str = "") -> list[str]:
def list_global_memories(self, subtopic: str = "") -> MemoriesList:
dir_path = self._global_memory_dir
if subtopic:
dir_path = dir_path / subtopic.replace("/", os.sep)
return self._list_memories(dir_path, self._global_memory_dir, self.GLOBAL_TOPIC + "/")
def list_project_memories(self, topic: str = "") -> list[str]:
def list_project_memories(self, topic: str = "") -> MemoriesList:
assert self._project_memory_dir is not None, "Project dir was not passed at initialization"
dir_path = self._project_memory_dir
if topic:
dir_path = dir_path / topic.replace("/", os.sep)
return self._list_memories(dir_path, self._project_memory_dir)
def list_memories(self, topic: str = "") -> list[str]:
def list_memories(self, topic: str = "") -> MemoriesList:
"""
Lists all memories, optionally filtered by topic.
If the topic is omitted, both global and project-specific memories are returned.
"""
memories: list[str]
memories: MemoriesManager.MemoriesList
if topic:
if self._is_global(topic):
@@ -146,9 +172,10 @@ class MemoriesManager:
else:
memories = self.list_project_memories(topic=topic)
else:
memories = self.list_project_memories() + self.list_global_memories()
memories = self.list_project_memories()
memories.extend(self.list_global_memories())
return sorted(memories)
return memories
def delete_memory(self, name: str, is_tool_context: bool) -> str:
self._check_write_access(name, is_tool_context)
@@ -219,7 +246,7 @@ class Project(ToStringMixin):
self._serena_data_folder = serena_config.get_project_serena_folder(self.project_root)
log.info("Serena project data folder: %s", self._serena_data_folder)
read_only_memory_patterns = project_config.read_only_memory_patterns
read_only_memory_patterns = serena_config.read_only_memory_patterns + project_config.read_only_memory_patterns
self.memories_manager = MemoriesManager(self._serena_data_folder, read_only_memory_patterns=read_only_memory_patterns)
self.language_server_manager: LanguageServerManager | None = None
@@ -320,7 +347,7 @@ class Project(ToStringMixin):
project_memories = self.memories_manager.list_project_memories()
if project_memories:
msg += (
f"\nAvailable project memories: {json.dumps(project_memories)}\n"
f"\nAvailable project memories: {json.dumps(project_memories.to_dict())}\n"
+ "Use the `read_memory` tool to read these memories later if they are relevant to the task."
)
if self.project_config.initial_prompt:
+2 -1
View File
@@ -124,5 +124,6 @@ initial_prompt: ""
# If null or missing, use the setting from the global configuration.
symbol_info_budget:
# Extends the same setting from the global configuration, merging the two lists.
# list of regex patterns which, when matched, mark a memory entry as read‑only.
# Extends the list from the global configuration, merging the two lists.
read_only_memory_patterns: []
@@ -68,8 +68,8 @@ ls_specific_settings: {}
# These patterns are merged additively with each project's own ignored_paths.
ignored_paths: []
# Mark memories as read-only through regex patterns.
# For example, "global/.*" will mark global memories as read-only.
# list of regex patterns which, when matched, mark a memory entry as read‑only.
# For example, "global/.*" will mark all global memories as read-only.
# You can extend the list on a per-project basis in the project.yml configuration file.
read_only_memory_patterns: []
+2 -6
View File
@@ -51,15 +51,11 @@ class ListMemoriesTool(Tool):
List available memories. Any memory can be read using the `read_memory` tool.
"""
def list_memories(self, topic: str = "") -> list[str]:
return self.memories_manager.list_memories(topic)
def apply(self, topic: str = "") -> str:
"""
List available memories, optionally filtered by topic. Memories marked as read-only cannot be
edited or deleted.
Lists available memories, optionally filtered by topic.
"""
return self._to_json(self.memories_manager.list_memories(topic))
return self._to_json(self.memories_manager.list_memories(topic).to_dict())
class DeleteMemoryTool(Tool, ToolMarkerCanEdit):
+1 -1
View File
@@ -517,4 +517,4 @@ class TestMemoriesManagerCustomPath:
manager.save_memory("topic_a", "content a", is_tool_context=False)
manager.save_memory("topic_b", "content b", is_tool_context=False)
memories = manager.list_project_memories()
assert sorted(memories) == ["topic_a", "topic_b"]
assert sorted(memories.get_full_list()) == ["topic_a", "topic_b"]