mirror of
https://github.com/tiennm99/serena.git
synced 2026-10-12 14:15:14 +00:00
Feature: configure read-only memories through regex patterns
This commit is contained in:
1 parent
b805b58fe3
commit
60d4cba822
7 files changed
+51
-31
No files matched your search
@@ -31,8 +31,9 @@ Global memories use the top-level topic `global`, i.e. whenever a memory name st
|
||||
it is stored in the global memories directory and is shared across all projects.
|
||||
|
||||
By default, deletion and editing of global memories is allowed.
|
||||
If you want to primarily manage such memories yourself and protect them from accidental modification by the agent,
|
||||
set `edit_global_memories: False` in Serena's [global configuration](050_configuration).
|
||||
If you want to protect them from accidental modification by the agent,
|
||||
you can add regex patterns to `read_only_memory_patterns` in your global or
|
||||
project-level [configuration](050_configuration). For example, setting "global/.*" will mark all global memories as read-only. The agent will be informed which memories are read-only.
|
||||
|
||||
Since global memories are not versioned alongside your project files,
|
||||
it can be helpful to track global memories with git (i.e. to make `~/.serena/memories/` a git repository)
|
||||
|
||||
+3
-1
@@ -611,7 +611,9 @@ class SerenaAgent:
|
||||
def create_system_prompt(self) -> str:
|
||||
available_tools = self._active_tools
|
||||
available_markers = available_tools.tool_marker_names
|
||||
global_memory_names = MemoriesManager.list_global_memories()
|
||||
global_memory_names = MemoriesManager(
|
||||
project_root=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 ""
|
||||
log.info("Generating system prompt with available_tools=(see active tools), available_markers=%s", available_markers)
|
||||
system_prompt = self.prompt_factory.create_system_prompt(
|
||||
|
||||
@@ -183,6 +183,7 @@ class SharedConfig(ModeSelectionDefinition, ToolInclusionDefinition, ToStringMix
|
||||
|
||||
symbol_info_budget: float | None = None
|
||||
language_backend: LanguageBackend | None = None
|
||||
read_only_memory_patterns: list[str] = field(default_factory=list)
|
||||
|
||||
|
||||
class SerenaConfigError(Exception):
|
||||
@@ -376,6 +377,7 @@ class ProjectConfig(SharedConfig):
|
||||
fixed_tools=data["fixed_tools"],
|
||||
included_optional_tools=data["included_optional_tools"],
|
||||
read_only=data["read_only"],
|
||||
read_only_memory_patterns=data.get("read_only_memory_patterns", []),
|
||||
ignore_all_files_in_gitignore=data["ignore_all_files_in_gitignore"],
|
||||
initial_prompt=data["initial_prompt"],
|
||||
encoding=data["encoding"],
|
||||
@@ -555,9 +557,6 @@ class SerenaConfig(SharedConfig):
|
||||
"""List of paths to ignore across all projects. Same syntax as gitignore, so you can use * and **.
|
||||
These patterns are merged additively with each project's own ignored_paths."""
|
||||
|
||||
edit_global_memories: bool = True
|
||||
"""Whether global memories are allowed to be deleted or edited."""
|
||||
|
||||
# settings with overridden defaults
|
||||
default_modes: Sequence[str] | None = ("interactive", "editing")
|
||||
symbol_info_budget: float = 10.0
|
||||
|
||||
+34
-19
@@ -1,8 +1,10 @@
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import threading
|
||||
from collections.abc import Sequence
|
||||
from pathlib import Path
|
||||
from typing import TYPE_CHECKING, Any, Literal
|
||||
|
||||
@@ -34,15 +36,23 @@ class MemoriesManager:
|
||||
GLOBAL_TOPIC = "global"
|
||||
_global_memory_dir = SerenaPaths().global_memories_path
|
||||
|
||||
def __init__(self, project_root: str, global_memory_tool_write_access: bool = False):
|
||||
def __init__(self, project_root: str | None, read_only_memory_patterns: Sequence[str] = ()):
|
||||
"""
|
||||
:param project_root: the project's root directory
|
||||
:param global_memory_tool_write_access: whether to allow writing global memories in tool execution contexts
|
||||
:param project_root: the project's root directory. We allow
|
||||
:param read_only_memory_patterns: whether to allow writing global memories in tool execution contexts
|
||||
"""
|
||||
self._project_memory_dir = Path(get_serena_managed_in_project_dir(project_root)) / "memories"
|
||||
self._project_memory_dir.mkdir(parents=True, exist_ok=True)
|
||||
self._global_memory_tool_write_access = global_memory_tool_write_access
|
||||
self._project_memory_dir: Path | None = None
|
||||
if project_root is not None:
|
||||
self._project_memory_dir = Path(get_serena_managed_in_project_dir(project_root)) / "memories"
|
||||
self._project_memory_dir.mkdir(parents=True, exist_ok=True)
|
||||
self._encoding = SERENA_FILE_ENCODING
|
||||
self._read_only_memory_patterns = [re.compile(pattern) for pattern in 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):
|
||||
return True
|
||||
return False
|
||||
|
||||
def _is_global(self, name: str) -> bool:
|
||||
return name == self.GLOBAL_TOPIC or name.startswith(self.GLOBAL_TOPIC + "/")
|
||||
@@ -68,6 +78,7 @@ class MemoriesManager:
|
||||
return self._global_memory_dir / filename
|
||||
|
||||
# Project-local memory
|
||||
assert self._project_memory_dir is not None, "Project dir was not passed at initialization"
|
||||
parts = name.split("/")
|
||||
filename = f"{parts[-1]}.md"
|
||||
|
||||
@@ -81,9 +92,8 @@ class MemoriesManager:
|
||||
|
||||
def _check_write_access(self, name: str, is_tool_context: bool) -> None:
|
||||
# in tool context, global memory write access can be disabled
|
||||
if is_tool_context:
|
||||
if self._is_global(name) and not self._global_memory_tool_write_access:
|
||||
raise PermissionError(f"Writing to global memories is not allowed (attempted to write to '{name}')")
|
||||
if is_tool_context and self._is_read_only_memory(name):
|
||||
raise PermissionError(f"Attempted to write to read_only memory: '{name}')")
|
||||
|
||||
def load_memory(self, name: str) -> str:
|
||||
memory_file_path = self.get_memory_file_path(name)
|
||||
@@ -99,24 +109,27 @@ class MemoriesManager:
|
||||
f.write(content)
|
||||
return f"Memory {name} written."
|
||||
|
||||
@staticmethod
|
||||
def _list_memories(search_dir: Path, base_dir: Path, prefix: str = "") -> list[str]:
|
||||
def _list_memories(self, search_dir: Path, base_dir: Path, prefix: str = "") -> list[str]:
|
||||
if not search_dir.exists():
|
||||
return []
|
||||
results = []
|
||||
read_only_postfix = " (read only)"
|
||||
for md_file in search_dir.rglob("*.md"):
|
||||
rel = str(md_file.relative_to(base_dir).with_suffix("")).replace(os.sep, "/")
|
||||
results.append(prefix + rel)
|
||||
memory_name = prefix + rel
|
||||
if self._is_read_only_memory(memory_name):
|
||||
memory_name += read_only_postfix
|
||||
results.append(memory_name)
|
||||
return results
|
||||
|
||||
@classmethod
|
||||
def list_global_memories(cls, subtopic: str = "") -> list[str]:
|
||||
dir_path = cls._global_memory_dir
|
||||
def list_global_memories(self, subtopic: str = "") -> list[str]:
|
||||
dir_path = self._global_memory_dir
|
||||
if subtopic:
|
||||
dir_path = dir_path / subtopic.replace("/", os.sep)
|
||||
return cls._list_memories(dir_path, cls._global_memory_dir, cls.GLOBAL_TOPIC + "/")
|
||||
return self._list_memories(dir_path, self._global_memory_dir, self.GLOBAL_TOPIC + "/")
|
||||
|
||||
def list_project_memories(self, topic: str = "") -> list[str]:
|
||||
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)
|
||||
@@ -204,9 +217,11 @@ class Project(ToStringMixin):
|
||||
):
|
||||
self.project_root = project_root
|
||||
self.project_config = project_config
|
||||
|
||||
global_memory_write_access = serena_config.edit_global_memories if serena_config else False
|
||||
self.memories_manager = MemoriesManager(project_root, global_memory_write_access)
|
||||
if serena_config is not None:
|
||||
read_only_memory_patterns = serena_config.read_only_memory_patterns + project_config.read_only_memory_patterns
|
||||
else:
|
||||
read_only_memory_patterns = project_config.read_only_memory_patterns
|
||||
self.memories_manager = MemoriesManager(project_root, read_only_memory_patterns=read_only_memory_patterns)
|
||||
|
||||
self.language_server_manager: LanguageServerManager | None = None
|
||||
self._is_newly_created = is_newly_created
|
||||
|
||||
@@ -123,3 +123,6 @@ initial_prompt: ""
|
||||
# This overrides the corresponding setting in the global configuration; see the documentation there.
|
||||
# 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.
|
||||
read_only_memory_patterns: []
|
||||
@@ -68,11 +68,10 @@ ls_specific_settings: {}
|
||||
# These patterns are merged additively with each project's own ignored_paths.
|
||||
ignored_paths: []
|
||||
|
||||
# whether global memories (the "global" topic) are allowed to be deleted or edited by the agent.
|
||||
# Global memories are stored in ~/.serena/memories/global/ and shared across all projects.
|
||||
# Set to false to protect global memories from accidental modification by agents.
|
||||
# Note: We recommend tracking global memories with git.
|
||||
edit_global_memories: True
|
||||
# Mark memories as read-only through regex patterns.
|
||||
# For example, "global/.*" will mark 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: []
|
||||
|
||||
# timeout, in seconds, after which tool executions are terminated
|
||||
tool_timeout: 240
|
||||
|
||||
@@ -56,7 +56,8 @@ class ListMemoriesTool(Tool):
|
||||
|
||||
def apply(self, topic: str = "") -> str:
|
||||
"""
|
||||
List available memories, optionally filtered by topic.
|
||||
List available memories, optionally filtered by topic. Memories marked as read-only cannot be
|
||||
edited or deleted.
|
||||
"""
|
||||
return self._to_json(self.memories_manager.list_memories(topic))
|
||||
|
||||
|
||||
Reference in new issue
Block a user