Feature/global memories (#1091)

Support for global (cross-project) memories, adds a news item

---------

Co-authored-by: Ariadne[bot] (butterflysky-ai) <good.town5863@butterflysky.dev>
Co-authored-by: Dominik Jain <dominik.jain@oraios-ai.de>
This commit is contained in:
authored and GitHub committed 2026-03-03 15:18:41 +01:00
1 parent 6e95e0e112
commit ccbd15082b
19 files changed
+466 -104

No files matched your search

+4 -14
View File
@@ -103,22 +103,12 @@ By default, Serena will perform an **onboarding process** when
it is started for the first time for a project.
The goal of the onboarding is for Serena to get familiar with the project
and to store memories, which it can then draw upon in future interactions.
If an LLM should fail to complete the onboarding and does not actually write the
respective memories to disk, you may need to ask it to do so explicitly.
The onboarding will usually read a lot of content from the project, thus filling
up the context. It can therefore be advisable to switch to another conversation
once the onboarding is complete.
After the onboarding, we recommend that you have a quick look at the memories and,
if necessary, edit them or add additional ones.
In general, **memories** provide a way for Serena to store and retrieve
information about the project, relevant conventions, and other relevant aspects.
**Memories** are files stored in `.serena/memories/` in the project directory,
which the agent can choose to read in subsequent interactions.
Feel free to read and adjust them as needed; you can also add new ones manually.
Every file in the `.serena/memories/` directory is a memory file.
Whenever Serena starts working on a project, the list of memories is
provided, and the agent can decide to read them.
We found that memories can significantly improve the user experience with Serena.
For more information on this, including how to manage
or disable these features, see [Memories & Onboarding](045-memories).
## Preparing Your Project
+84
View File
@@ -0,0 +1,84 @@
# Memories & Onboarding
Serena provides the functionality of a fully featured agent, and a useful aspect of this is Serena's memory system.
Despite its simplicity, we received positive feedback from many users who tend to combine it with their
agent's internal memory management (e.g., `AGENTS.md` files).
## Memories
Memories are simple, human-readable Markdown files that both you and
your agent can create, read, and edit.
Serena differentiates between
* **project-specific memories**, which are stored in the `.serena/memories/` directory within your project folder, and
* **global memories**, which are shared across all projects and, by default, are stored in `~/.serena/memories/global/`
The LLM is informed about the existence of memories and instructed to read them when appropriate,
inferring appropriateness from the file name.
When the agent starts working on a project, it receives the list of available memories.
The agent should be instructed to update memories by the user when appropriate.
### Organizing Memories
Memories can be organized into **topics** by using `/` in the memory name (e.g. `modules/frontend`).
The structure is mapped to the file system, where topics correspond to subdirectories.
The `list_memories` tool can filter by topic, allowing the agent to explore even large numbers of memories in a structured way.
(global-memories)=
### Global Memories
Global memories use the top-level topic `global`, i.e. whenever a memory name starts with `global/`,
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).
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)
in order to have a history of changes and the possibility to revert them if needed.
### Manually Editing Memories
You may edit memories directly in the file system, using your preferred text editor or IDE.
Alternatively, access them via the [Serena Dashboard](060_dashboard), which provides a graphical interface for
viewing, creating, editing, and deleting memories while Serena is running.
(onboarding)=
## Onboarding
By default, Serena performs an **onboarding process** when it encounters a project
for the first time (i.e., when no project memories exist yet).
The goal of the onboarding is for Serena to get familiar with the project —
its structure, build system, testing setup, and other essential aspects —
and to store this knowledge as memories for future interactions.
In further project activations, Serena will check whether onboarding was already
performed by looking for existing project memories and will skip the onboarding
process if memories are found.
### How Onboarding Works
1. When a project is activated, Serena checks whether onboarding was already
performed (by checking if any memories exist).
2. If no memories are found, Serena triggers the onboarding process, which
reads key files and directories to understand the project.
3. The gathered information is written into project-specific memory files (see above).
### Tips for Onboarding
- **Context usage**: The onboarding process will read a lot of content from the project,
filling up the context window. It is therefore advisable to **switch to a new conversation**
once the onboarding is complete.
- **LLM failures**: If an LLM fails to complete the onboarding and does not actually
write the respective memories to disk, you may need to ask it to do so explicitly.
- **Review the results**: After onboarding, we recommend having a quick look at the
generated memories and editing them or adding new ones as needed.
## Disabling Memories and Onboarding
If you do not require the functionality described in this section, you can selectively disable it.
* To disable all memory related tools (including onboarding), adding `no-memories` to the `base_modes`
in Serena's [global configuration](050-configuration).
* Similarly, to disable only onboarding, add `no-onboarding` to the `base_modes`.
+5 -2
View File
@@ -12,7 +12,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
from sensai.util.string import TextBuilder, list_string
from interprompt.jinja_template import JinjaTemplate
from serena import serena_version
@@ -29,7 +29,7 @@ from serena.config.serena_config import (
)
from serena.dashboard import SerenaDashboardAPI
from serena.ls_manager import LanguageServerManager, LanguageServerManagerInitialisationError
from serena.project import Project
from serena.project import MemoriesManager, Project
from serena.prompt_factory import SerenaPromptFactory
from serena.task_executor import TaskExecutor
from serena.tools import ActivateProjectTool, GetCurrentConfigTool, OpenDashboardTool, ReplaceContentTool, Tool, ToolMarker, ToolRegistry
@@ -611,12 +611,15 @@ 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_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(
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,
)
# If a project is active at startup, append its activation message
+9 -1
View File
@@ -85,6 +85,12 @@ class SerenaPaths:
"""
file containing the ID of the last read news snippet
"""
global_memories_path = Path(os.path.join(self.serena_user_home_dir, "memories", "global"))
global_memories_path.mkdir(parents=True, exist_ok=True)
self.global_memories_path = global_memories_path
"""
directory where global memories are stored, i.e. memories that are available across all projects
"""
self.last_returned_log_file_path: str | None = None
"""
the path to the last log file returned by `get_next_log_file_path`. If this is not None, the logs
@@ -549,6 +555,9 @@ 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
@@ -559,7 +568,6 @@ class SerenaConfig(SharedConfig):
If the budget is exceeded, Serena stops issuing further requests and returns partial info results.
0 disables the budget (no early stopping). Negative values are invalid.
"""
# *** fields that are NOT mapped to/from the configuration file ***
_loaded_commented_yaml: CommentedMap | None = None
+31 -4
View File
@@ -89,6 +89,11 @@ class RequestDeleteMemory(BaseModel):
memory_name: str
class RequestRenameMemory(BaseModel):
old_name: str
new_name: str
class ResponseGetSerenaConfig(BaseModel):
content: str
@@ -269,6 +274,18 @@ class SerenaDashboardAPI:
except Exception as e:
return {"status": "error", "message": str(e)}
@self._app.route("/rename_memory", methods=["POST"])
def rename_memory() -> dict[str, str]:
request_data = request.get_json()
if not request_data:
return {"status": "error", "message": "No data provided"}
request_rename_memory = RequestRenameMemory.model_validate(request_data)
try:
result_message = self._rename_memory(request_rename_memory)
return {"status": "success", "message": result_message}
except Exception as e:
return {"status": "error", "message": str(e)}
@self._app.route("/get_serena_config", methods=["GET"])
def get_serena_config() -> dict[str, Any]:
try:
@@ -556,8 +573,7 @@ class SerenaDashboardAPI:
project = self._agent.get_active_project()
if project is None:
raise ValueError("No active project")
project.memories_manager.save_memory(request_save_memory.memory_name, request_save_memory.content)
project.memories_manager.save_memory(request_save_memory.memory_name, request_save_memory.content, is_tool_context=False)
self._agent.execute_task(run, logged=True, name="SaveMemory")
@@ -566,11 +582,22 @@ class SerenaDashboardAPI:
project = self._agent.get_active_project()
if project is None:
raise ValueError("No active project")
project.memories_manager.delete_memory(request_delete_memory.memory_name)
project.memories_manager.delete_memory(request_delete_memory.memory_name, is_tool_context=False)
self._agent.execute_task(run, logged=True, name="DeleteMemory")
def _rename_memory(self, request_rename_memory: RequestRenameMemory) -> str:
def run() -> str:
project = self._agent.get_active_project()
if project is None:
raise ValueError("No active project")
return project.memories_manager.move_memory(
request_rename_memory.old_name, request_rename_memory.new_name, is_tool_context=False
)
return self._agent.execute_task(run, logged=True, name="RenameMemory")
def _get_serena_config(self) -> ResponseGetSerenaConfig:
config_path = self._agent.serena_config.config_file_path
if config_path is None or not os.path.exists(config_path):
@@ -33,6 +33,12 @@ class PromptFactory(PromptFactoryBase):
return self._render_prompt("prepare_for_new_conversation", locals())
def create_system_prompt(
self, *, available_markers: Any, available_tools: Any, context_system_prompt: Any, mode_system_prompts: Any
self,
*,
available_markers: Any,
available_tools: Any,
context_system_prompt: Any,
global_memories_list: Any,
mode_system_prompts: Any,
) -> str:
return self._render_prompt("system_prompt", locals())
+116 -33
View File
@@ -1,9 +1,10 @@
import json
import logging
import os
import shutil
import threading
from pathlib import Path
from typing import TYPE_CHECKING, Any
from typing import TYPE_CHECKING, Any, Literal
import pathspec
from sensai.util.logging import LogTime
@@ -12,12 +13,13 @@ from sensai.util.string import ToStringMixin
from serena.config.serena_config import (
DEFAULT_TOOL_TIMEOUT,
ProjectConfig,
SerenaPaths,
get_serena_managed_in_project_dir,
)
from serena.constants import SERENA_FILE_ENCODING, SERENA_MANAGED_DIR_NAME
from serena.ls_manager import LanguageServerFactory, LanguageServerManager
from serena.util.file_system import GitignoreParser, match_path
from serena.util.text_utils import MatchedConsecutiveLines, search_files
from serena.util.text_utils import ContentReplacer, MatchedConsecutiveLines, search_files
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
from solidlsp.ls_utils import FileUtils
@@ -29,26 +31,59 @@ log = logging.getLogger(__name__)
class MemoriesManager:
def __init__(self, project_root: str):
self._memory_dir = Path(get_serena_managed_in_project_dir(project_root)) / "memories"
self._memory_dir.mkdir(parents=True, exist_ok=True)
GLOBAL_TOPIC = "global"
_global_memory_dir = SerenaPaths().global_memories_path
def __init__(self, project_root: str, global_memory_tool_write_access: bool = False):
"""
:param project_root: the project's root directory
:param global_memory_tool_write_access: 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._encoding = SERENA_FILE_ENCODING
def _is_global(self, name: str) -> bool:
return name == self.GLOBAL_TOPIC or name.startswith(self.GLOBAL_TOPIC + "/")
def get_memory_file_path(self, name: str) -> Path:
# Strip .md extension if present
name = name.replace(".md", "")
# Split by "/" to handle subdirectories
if self._is_global(name):
if name == self.GLOBAL_TOPIC:
raise ValueError(
f'Bare "{self.GLOBAL_TOPIC}" is not a valid memory name. '
f'Use "{self.GLOBAL_TOPIC}/<name>" to address a global memory.'
)
# Strip "global/" prefix and resolve against global dir
sub_name = name[len(self.GLOBAL_TOPIC) + 1 :]
parts = sub_name.split("/")
filename = f"{parts[-1]}.md"
if len(parts) > 1:
subdir = self._global_memory_dir / "/".join(parts[:-1])
subdir.mkdir(parents=True, exist_ok=True)
return subdir / filename
return self._global_memory_dir / filename
# Project-local memory
parts = name.split("/")
filename = f"{parts[-1]}.md"
if len(parts) > 1:
# Create subdirectory path
subdir = self._memory_dir / "/".join(parts[:-1])
subdir = self._project_memory_dir / "/".join(parts[:-1])
subdir.mkdir(parents=True, exist_ok=True)
return subdir / filename
return self._memory_dir / filename
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
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}')")
def load_memory(self, name: str) -> str:
memory_file_path = self.get_memory_file_path(name)
@@ -57,47 +92,70 @@ class MemoriesManager:
with open(memory_file_path, encoding=self._encoding) as f:
return f.read()
def save_memory(self, name: str, content: str) -> str:
def save_memory(self, name: str, content: str, is_tool_context: bool) -> str:
self._check_write_access(name, is_tool_context)
memory_file_path = self.get_memory_file_path(name)
with open(memory_file_path, "w", encoding=self._encoding) as f:
f.write(content)
return f"Memory {name} written."
@staticmethod
def _list_memories(search_dir: Path, base_dir: Path, prefix: str = "") -> list[str]:
if not search_dir.exists():
return []
results = []
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)
return results
@classmethod
def list_global_memories(cls, subtopic: str = "") -> list[str]:
dir_path = cls._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 + "/")
def list_project_memories(self, topic: str = "") -> list[str]:
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]:
"""
List memories, optionally filtered by topic.
Lists all memories, optionally filtered by topic.
If the topic is omitted, both global and project-specific memories are returned.
"""
memories = []
memories: list[str]
if topic:
# Only list memories in specified subdirectory
search_dir = self._memory_dir / topic.replace("/", os.sep)
if not search_dir.exists():
return []
if self._is_global(topic):
topic_parts = topic.split("/")
subtopic = "/".join(topic_parts[1:])
memories = self.list_global_memories(subtopic=subtopic)
else:
memories = self.list_project_memories(topic=topic)
else:
search_dir = self._memory_dir
memories = self.list_project_memories() + self.list_global_memories()
# Recursively find all .md files
for md_file in search_dir.rglob("*.md"):
# Calculate relative path as memory name
rel_path = md_file.relative_to(self._memory_dir)
name = str(rel_path.with_suffix("")).replace(os.sep, "/")
memories.append(name)
# Sort alphabetically by name
return sorted(memories)
def delete_memory(self, name: str) -> str:
def delete_memory(self, name: str, is_tool_context: bool) -> str:
self._check_write_access(name, is_tool_context)
memory_file_path = self.get_memory_file_path(name)
if not memory_file_path.exists():
return f"Memory {name} not found."
memory_file_path.unlink()
return f"Memory {name} deleted."
def rename_memory(self, old_name: str, new_name: str) -> str:
def move_memory(self, old_name: str, new_name: str, is_tool_context: bool) -> str:
"""
Rename or move a memory file.
Moving between global and project scope (e.g. "global/foo" -> "bar") is supported.
"""
self._check_write_access(new_name, is_tool_context)
old_path = self.get_memory_file_path(old_name)
new_path = self.get_memory_file_path(new_name)
@@ -106,13 +164,35 @@ class MemoriesManager:
if new_path.exists():
raise FileExistsError(f"Memory {new_name} already exists.")
# Ensure target directory exists
new_path.parent.mkdir(parents=True, exist_ok=True)
shutil.move(old_path, new_path)
# Move/rename the file
old_path.rename(new_path)
return f"Memory renamed from {old_name} to {new_name}."
def edit_memory(
self, name: str, needle: str, repl: str, mode: Literal["literal", "regex"], allow_multiple_occurrences: bool, is_tool_context: bool
) -> str:
"""
Edit a memory by replacing content matching a pattern.
:param name: the memory name
:param needle: the string or regex to search for
:param repl: the replacement string
:param mode: "literal" or "regex"
:param allow_multiple_occurrences:
"""
self._check_write_access(name, is_tool_context)
memory_file_path = self.get_memory_file_path(name)
if not memory_file_path.exists():
raise FileNotFoundError(f"Memory {name} not found.")
with open(memory_file_path, encoding=self._encoding) as f:
original_content = f.read()
replacer = ContentReplacer(mode=mode, allow_multiple_occurrences=allow_multiple_occurrences)
updated_content = replacer.replace(original_content, needle, repl)
with open(memory_file_path, "w", encoding=self._encoding) as f:
f.write(updated_content)
return f"Memory {name} edited successfully."
class Project(ToStringMixin):
def __init__(
@@ -124,7 +204,10 @@ class Project(ToStringMixin):
):
self.project_root = project_root
self.project_config = project_config
self.memories_manager = MemoriesManager(project_root)
global_memory_write_access = serena_config.edit_global_memories if serena_config else False
self.memories_manager = MemoriesManager(project_root, global_memory_write_access)
self.language_server_manager: LanguageServerManager | None = None
self._is_newly_created = is_newly_created
@@ -220,10 +303,10 @@ class Project(ToStringMixin):
msg = f"The project with name '{self.project_name}' at {self.project_root} is activated."
languages_str = ", ".join([lang.value for lang in self.project_config.languages])
msg += f"\nProgramming languages: {languages_str}; file encoding: {self.project_config.encoding}"
memories = self.memories_manager.list_memories()
if memories:
project_memories = self.memories_manager.list_project_memories()
if project_memories:
msg += (
f"\nAvailable project memories: {json.dumps(memories)}\n"
f"\nAvailable project memories: {json.dumps(project_memories)}\n"
+ "Use the `read_memory` tool to read these memories later if they are relevant to the task."
)
if self.project_config.initial_prompt:
@@ -6,6 +6,7 @@ excluded_tools:
- read_memory
- delete_memory
- edit_memory
- rename_memory
- list_memories
- onboarding
- check_onboarding_performed
@@ -35,10 +35,13 @@ prompts:
You can understand relationships between symbols by using the `find_referencing_symbols` tool.
{% endif %}
{% if 'read_memory' in available_tools %}
{% if 'read_memory' in available_tools -%}
You generally have access to memories and it may be useful for you to read them.
You infer whether memories are relevant based on their names.
{% endif %}
{% if global_memories_list -%}
The following global (not project-specific) memories are available to you: {{ global_memories_list }}
{%- endif -%}
{%- endif %}
The context and modes of operation are described below. These determine how to interact with your user
and which kinds of interactions are expected of you.
@@ -1308,6 +1308,42 @@ body {
gap: 8px;
}
/* Memory Rename Styles */
.memory-rename-btn {
display: inline-flex;
align-items: center;
justify-content: center;
width: 24px;
height: 24px;
background: var(--bg-secondary);
border: 1px solid var(--border-color);
border-radius: 4px;
cursor: pointer;
color: var(--text-muted);
opacity: 0.5;
transition: opacity 0.2s ease, background-color 0.2s ease;
}
.memory-rename-btn:hover {
opacity: 1;
background-color: var(--border-color);
}
.memory-rename-input {
font-size: inherit;
font-weight: inherit;
font-family: inherit;
color: var(--text-primary);
background: transparent;
border: none;
border-bottom: 1px solid var(--btn-primary);
outline: none;
padding: 0;
flex: 1;
min-width: 200px;
max-width: 80%;
}
/* Log Action Buttons (Save, Copy, Clear) */
.log-action-buttons {
position: absolute;
+74 -3
View File
@@ -307,6 +307,8 @@ class Dashboard {
this.$modalCloseRemove = $('.modal-close-remove');
this.$editMemoryModal = $('#edit-memory-modal');
this.$editMemoryName = $('#edit-memory-name');
this.$editMemoryRenameBtn = $('#edit-memory-rename-btn');
this.$editMemoryRenameInput = $('#edit-memory-rename-input');
this.$editMemoryContent = $('#edit-memory-content');
this.$editMemorySaveBtn = $('#edit-memory-save-btn');
this.$editMemoryCancelBtn = $('#edit-memory-cancel-btn');
@@ -365,6 +367,19 @@ class Dashboard {
this.$editMemoryCancelBtn.click(this.closeEditMemoryModal.bind(this));
this.$modalCloseEditMemory.click(this.closeEditMemoryModal.bind(this));
this.$editMemoryContent.on('input', this.trackMemoryChanges.bind(this));
this.$editMemoryRenameBtn.click(this.startMemoryRename.bind(this));
this.$editMemoryRenameInput.keydown(function (e) {
if (e.which === 13) { // Enter key
e.preventDefault();
self.commitMemoryRename();
} else if (e.which === 27) { // Escape key
e.preventDefault();
self.cancelMemoryRename();
}
});
this.$editMemoryRenameInput.on('blur', function () {
self.cancelMemoryRename();
});
this.$deleteMemoryOkBtn.click(this.confirmDeleteMemoryOk.bind(this));
this.$deleteMemoryCancelBtn.click(this.closeDeleteMemoryModal.bind(this));
this.$modalCloseDeleteMemory.click(this.closeDeleteMemoryModal.bind(this));
@@ -1887,6 +1902,62 @@ class Dashboard {
});
}
startMemoryRename() {
this.$editMemoryName.hide();
this.$editMemoryRenameBtn.hide();
this.$editMemoryRenameInput.val(this.currentMemoryName).show().focus().select();
}
cancelMemoryRename() {
this.$editMemoryRenameInput.hide();
this.$editMemoryName.show();
this.$editMemoryRenameBtn.show();
}
commitMemoryRename() {
const newName = this.$editMemoryRenameInput.val().trim();
const oldName = this.currentMemoryName;
// If name unchanged, just cancel
if (!newName || newName === oldName) {
this.cancelMemoryRename();
return;
}
// Validate memory name (alphanumeric, underscores, and slashes for subdirectories)
if (!/^[a-zA-Z0-9_]+(?:\/[a-zA-Z0-9_]+)*$/.test(newName)) {
alert('Memory name can only contain letters, numbers, underscores, and "/" for subdirectories (e.g., "topic/memory_name")');
this.$editMemoryRenameInput.focus();
return;
}
const self = this;
this.$editMemoryRenameInput.prop('disabled', true);
$.ajax({
url: '/rename_memory', type: 'POST', contentType: 'application/json', data: JSON.stringify({
old_name: oldName, new_name: newName
}), success: function (response) {
if (response.status === 'success') {
self.currentMemoryName = newName;
self.$editMemoryName.text(newName);
self.cancelMemoryRename();
// Reload config to reflect the rename in the memory list
self.loadConfigOverview();
} else {
alert('Error: ' + response.message);
self.$editMemoryRenameInput.focus();
}
}, error: function (xhr, status, error) {
console.error('Error renaming memory:', error);
alert('Error renaming memory: ' + (xhr.responseJSON ? xhr.responseJSON.message : error));
self.$editMemoryRenameInput.focus();
}, complete: function () {
self.$editMemoryRenameInput.prop('disabled', false);
}
});
}
confirmDeleteMemory(memoryName) {
// Set memory name to delete
this.memoryToDelete = memoryName;
@@ -1960,9 +2031,9 @@ class Dashboard {
return;
}
// Validate memory name (alphanumeric and underscores only)
if (!/^[a-zA-Z0-9_]+$/.test(memoryName)) {
alert('Memory name can only contain letters, numbers, and underscores');
// Validate memory name (alphanumeric, underscores, and slashes for subdirectories)
if (!/^[a-zA-Z0-9_]+(?:\/[a-zA-Z0-9_]+)*$/.test(memoryName)) {
alert('Memory name can only contain letters, numbers, underscores, and "/" for subdirectories (e.g., "topic/memory_name")');
return;
}
+14 -3
View File
@@ -275,7 +275,17 @@
<div id="edit-memory-modal" class="modal" style="display:none">
<div class="modal-content modal-content-large">
<span class="modal-close modal-close-edit-memory">&times;</span>
<h3 style="margin-bottom: 20px;">Memory: <span id="edit-memory-name"></span></h3>
<h3 style="margin-bottom: 20px; display: flex; align-items: center; gap: 8px;">
Memory:
<span id="edit-memory-name" class="memory-name-display"></span>
<span id="edit-memory-rename-btn" class="memory-rename-btn" title="Rename memory">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M17 3a2.83 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5L17 3z"></path>
</svg>
</span>
<input type="text" id="edit-memory-rename-input" class="memory-rename-input" style="display: none;">
</h3>
<textarea id="edit-memory-content" class="memory-editor" rows="20"
style="width: 100%; padding: 10px; border: 1px solid var(--border-color); border-radius: 4px; background: var(--bg-primary); color: var(--text-primary); font-family: 'Courier New', monospace; font-size: 13px; resize: vertical;"></textarea>
<div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px;">
@@ -312,13 +322,14 @@
</p>
<p style="margin: 15px 0; color: var(--text-muted); font-size: 13px; line-height: 1.5;">
Memory names should be descriptive and use underscores instead of spaces (e.g., "api_architecture",
"testing_guidelines").
"testing_guidelines"). Use "/" to organize memories into subdirectories (e.g., "architecture/api_design"),
use the "global/" prefix to write memories that are shared across projects (e.g., "global/java/style_guide").
</p>
<div style="margin: 20px 0;">
<label for="create-memory-name-input" style="display: block; margin-bottom: 8px; font-weight: 500;">Memory
Name:</label>
<input type="text" id="create-memory-name-input" class="memory-name-input"
placeholder="e.g., project_overview"
placeholder="e.g., project_overview or topic/memory_name"
style="width: 100%; padding: 8px 10px; border-radius: 4px; border: 1px solid var(--border-color); background: var(--bg-primary); color: var(--text-primary); font-size: 14px;">
</div>
<div style="display: flex; gap: 10px; justify-content: flex-end; margin-top: 20px;">
@@ -0,0 +1,12 @@
<div class="news-item">
<h3>Nested and Global Memories</h3>
<p class="date">March 03, 2026</p>
<p>
Serena's memory system has been significantly extended in functionality.
It keeps the simplicity that made it popular but now supports structuring memories
into topics and sharing them across projects.
</p>
<p>
For more detailed information, see the <a href="https://oraios.github.io/serena/02-usage/045_memories.html">documentation</a>.
</p>
</div>
@@ -68,6 +68,12 @@ 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
# timeout, in seconds, after which tool executions are terminated
tool_timeout: 240
+2 -4
View File
@@ -192,10 +192,8 @@ class ReplaceContentTool(Tool, ToolMarkerCanEdit):
If mode is "regex", the string can contain backreferences to matched groups in the needle regex,
specified using the syntax $!1, $!2, etc. for groups 1, 2, etc.
:param mode: either "literal" or "regex", specifying how the `needle` parameter is to be interpreted.
:param allow_multiple_occurrences: if True, the regex may match multiple occurrences in the file
and all of them will be replaced.
If this is set to False and the regex matches multiple occurrences, an error will be returned
(and you may retry with a revised, more specific regex).
:param allow_multiple_occurrences: whether to allow matching and replacing multiple occurrences.
If false and multiple occurrences are found, an error will be returned
"""
return self.replace_content(
relative_path, needle, repl, mode=mode, allow_multiple_occurrences=allow_multiple_occurrences, require_not_ignored=True
+33 -16
View File
@@ -1,17 +1,20 @@
from typing import Literal
from serena.tools import ReplaceContentTool, Tool, ToolMarkerCanEdit
from serena.tools import Tool, ToolMarkerCanEdit
class WriteMemoryTool(Tool, ToolMarkerCanEdit):
"""
Writes a named memory (for future reference) to Serena's project-specific memory store.
Write some information (utf-8-encoded) about this project that can be useful for future tasks to a memory in md format.
The memory name should be meaningful.
"""
def apply(self, memory_name: str, content: str, max_chars: int = -1) -> str:
"""
Write some information (utf-8-encoded) about this project that can be useful for future tasks to a memory in md format.
Write information (utf-8-encoded) about this project that can be useful for future tasks to a memory in md format.
The memory name should be meaningful and can include "/" to organize into topics (e.g., "auth/login/logic").
If explicitly instructed, use the "global/" prefix for writing a memory that is shared across projects
(e.g., "global/java/style_guide")
:param max_chars: the maximum number of characters to write. By default, determined by the config,
change only if instructed to do so.
@@ -24,28 +27,33 @@ class WriteMemoryTool(Tool, ToolMarkerCanEdit):
f"Content for {memory_name} is too long. Max length is {max_chars} characters. " + "Please make the content shorter."
)
return self.memories_manager.save_memory(memory_name, content)
return self.memories_manager.save_memory(memory_name, content, is_tool_context=True)
class ReadMemoryTool(Tool):
"""
Reads the memory with the given name from Serena's project-specific memory store.
Read the content of a memory file. This tool should only be used if the information
is relevant to the current task. You can infer whether the information
is relevant from the memory file name.
You should not read the same memory file multiple times in the same conversation.
"""
def apply(self, memory_name: str) -> str:
"""
Read the content of a memory. Should only be used if the information
is relevant to the current task, with relevance inferred from the memory name.
You should not read the same memory file multiple times in the same conversation.
Reads the contents of a memory. Should only be used if the information
is likely to be relevant to the current task, inferring relevance from the memory name.
"""
return self.memories_manager.load_memory(memory_name)
class ListMemoriesTool(Tool):
"""
Lists memories in Serena's project-specific memory store.
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.
@@ -55,35 +63,44 @@ class ListMemoriesTool(Tool):
class DeleteMemoryTool(Tool, ToolMarkerCanEdit):
"""
Deletes a memory from Serena's project-specific memory store.
Delete a memory file. Should only happen if a user asks for it explicitly,
for example by saying that the information retrieved from a memory file is no longer correct
or no longer relevant for the project.
"""
def apply(self, memory_name: str) -> str:
"""
Delete a memory, only call if instructed explicitly or permission was granted by the user.
"""
return self.memories_manager.delete_memory(memory_name)
return self.memories_manager.delete_memory(memory_name, is_tool_context=True)
class RenameMemoryTool(Tool, ToolMarkerCanEdit):
"""
Renames or moves a memory in Serena's project-specific memory store.
Renames or moves a memory. Moving between project and global scope is supported
(e.g., renaming "global/foo" to "bar" moves it from global to project scope).
"""
def apply(self, old_name: str, new_name: str) -> str:
"""
Rename or move a memory, use "/" in the name to organize into topics.
The "global" topic should only be used if explicitly instructed.
"""
return self.memories_manager.rename_memory(old_name, new_name)
return self.memories_manager.move_memory(old_name, new_name, is_tool_context=True)
class EditMemoryTool(Tool, ToolMarkerCanEdit):
"""
Replaces content matching a regular expression in a memory.
"""
def apply(
self,
memory_name: str,
needle: str,
repl: str,
mode: Literal["literal", "regex"],
allow_multiple_occurrences: bool = False,
) -> str:
r"""
Replaces content matching a regular expression in a memory.
@@ -95,7 +112,7 @@ class EditMemoryTool(Tool, ToolMarkerCanEdit):
with flags DOTALL and MULTILINE enabled).
:param repl: the replacement string (verbatim).
:param mode: either "literal" or "regex", specifying how the `needle` parameter is to be interpreted.
:param allow_multiple_occurrences: whether to allow matching and replacing multiple occurrences.
If false and multiple occurrences are found, an error will be returned.
"""
replace_content_tool = self.agent.get_tool(ReplaceContentTool)
rel_path = self.memories_manager.get_memory_file_path(memory_name).relative_to(self.get_project_root())
return replace_content_tool.replace_content(str(rel_path), needle, repl, mode=mode, require_not_ignored=False)
return self.memories_manager.edit_memory(memory_name, needle, repl, mode, allow_multiple_occurrences, is_tool_context=True)
+2 -1
View File
@@ -382,7 +382,8 @@ tool_packages = ["serena.tools"]
class ToolRegistry:
def __init__(self) -> None:
self._tool_dict: dict[str, RegisteredTool] = {}
for cls in iter_subclasses(Tool):
inclusion_predicate = lambda c: "apply" in c.__dict__ # include only concrete tool classes that implement apply
for cls in iter_subclasses(Tool, inclusion_predicate=inclusion_predicate):
if not any(cls.__module__.startswith(pkg) for pkg in tool_packages):
continue
is_optional = issubclass(cls, ToolMarkerOptional)
+12 -15
View File
@@ -2,7 +2,6 @@
Tools supporting the general workflow of the agent
"""
import json
import platform
from serena.tools import Tool, ToolMarkerDoesNotRequireActiveProject, ToolMarkerOptional
@@ -18,23 +17,21 @@ class CheckOnboardingPerformedTool(Tool):
Checks whether project onboarding was already performed.
You should always call this tool before beginning to actually work on the project/after activating a project.
"""
from .memory_tools import ListMemoriesTool
list_memories_tool = self.agent.get_tool(ListMemoriesTool)
memories = json.loads(list_memories_tool.apply())
if len(memories) == 0:
return (
project_memories = self.memories_manager.list_project_memories()
if len(project_memories) == 0:
msg = (
"Onboarding not performed yet (no memories available). "
+ "You should perform onboarding by calling the `onboarding` tool before proceeding with the task."
"You should perform onboarding by calling the `onboarding` tool before proceeding with the task. "
)
else:
return f"""The onboarding was already performed, below is the list of available memories.
Do not read them immediately, just remember that they exist and that you can read them later, if it is necessary
for the current task.
Some memories may be based on previous conversations, others may be general for the current project.
You should be able to tell which one you need based on the name of the memory.
{memories}"""
# Not reporting the list of memories here, as they were already reported at project activation
# (with the system prompt if the project was activated at startup)
msg = (
f"Onboarding was already performed: {len(project_memories)} project memories are available. "
"Consider reading memories if they appear relevant to the task at hand."
)
msg += " If you have not read the 'Serena Instructions Manual', do so now."
return msg
class OnboardingTool(Tool):
+13 -5
View File
@@ -1,6 +1,6 @@
import logging
import os
from collections.abc import Generator
from collections.abc import Callable, Iterator
from typing import TypeVar
from serena.util.file_system import find_all_non_ignored_files
@@ -11,12 +11,20 @@ T = TypeVar("T")
log = logging.getLogger(__name__)
def iter_subclasses(cls: type[T], recursive: bool = True) -> Generator[type[T], None, None]:
"""Iterate over all subclasses of a class. If recursive is True, also iterate over all subclasses of all subclasses."""
def iter_subclasses(
cls: type[T], recursive: bool = True, inclusion_predicate: Callable[[type[T]], bool] = lambda t: True
) -> Iterator[type[T]]:
"""Iterate over all subclasses of a class.
:param cls: The class whose subclasses to iterate over.
:param recursive: If True, also iterate over all subclasses of all subclasses.
:param inclusion_predicate: a predicate function to decide whether to include a subclass in the result
"""
for subclass in cls.__subclasses__():
yield subclass
if inclusion_predicate(subclass):
yield subclass
if recursive:
yield from iter_subclasses(subclass, recursive)
yield from iter_subclasses(subclass, recursive, inclusion_predicate)
def determine_programming_language_composition(repo_path: str) -> dict[Language, float]: