Skip to content
Open
7 changes: 6 additions & 1 deletion src/serena/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
from serena.constants import SERENA_FILE_ENCODING
from serena.ls_manager import LanguageServerFactory, LanguageServerManager
from serena.util.file_system import GitignoreParser, match_path
from serena.util.frontmatter import parse_frontmatter
from serena.util.text_utils import ContentReplacer, MatchedConsecutiveLines, search_files
from solidlsp import SolidLanguageServer
from solidlsp.ls_config import Language
Expand Down Expand Up @@ -96,7 +97,11 @@ def load_memory(self, name: str) -> str:
if not memory_file_path.exists():
return f"Memory file {name} not found, consider creating it with the `write_memory` tool if you need it."
with open(memory_file_path, encoding=self._encoding) as f:
return f.read()
raw = f.read()

# Strip optional frontmatter block if present (used by frontmatter tools)
_frontmatter, body = parse_frontmatter(raw)
return body

def save_memory(self, name: str, content: str, is_tool_context: bool) -> str:
self._check_write_access(name, is_tool_context)
Expand Down
103 changes: 101 additions & 2 deletions src/serena/tools/memory_tools.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from typing import Literal

from serena.tools import Tool, ToolMarkerCanEdit
from serena.tools import Tool, ToolMarkerCanEdit, ToolMarkerOptional
from serena.util.frontmatter import parse_frontmatter, render_frontmatter


class WriteMemoryTool(Tool, ToolMarkerCanEdit):
Expand Down Expand Up @@ -54,8 +55,106 @@ class ListMemoriesTool(Tool):
def apply(self, topic: str = "") -> str:
"""
Lists available memories, optionally filtered by topic.
If the optional memory_get_frontmatter tool is active, frontmatter metadata
is included for each listed memory.
"""
return self._to_json(self.memories_manager.list_memories(topic).to_dict())
memories_list = self.memories_manager.list_memories(topic)
memories = memories_list.to_dict()

if "memory_get_frontmatter" not in self.agent.get_active_tool_names():
return self._to_json(memories)

memory_frontmatter = {}
for memory_name in memories_list.get_full_list():
memory_file_path = self.memories_manager.get_memory_file_path(memory_name)

if not memory_file_path.exists():
continue

with open(memory_file_path, encoding=self.memories_manager._encoding) as f:
raw = f.read()

frontmatter, _ = parse_frontmatter(raw)
if frontmatter:
memory_frontmatter[memory_name] = frontmatter

if memory_frontmatter:
memories["frontmatter"] = memory_frontmatter

return self._to_json(memories)


class MemoryGetFrontmatterTool(Tool, ToolMarkerOptional):
"""
OPTIONAL. Reads and returns the frontmatter of a memory file (if present).

The frontmatter is a YAML-like block at the very top of a memory file:

---
key: "value"
another_key: "value2"
---
Body content...

Use this tool only when the metadata is useful for the current task.
Avoid storing long summaries in frontmatter, as it can waste tokens.
"""

def apply(self, memory_name: str) -> str:
"""
Returns the frontmatter as JSON (a dict). If the memory has no frontmatter,
returns an empty dict.

:param memory_name: memory name (may include "/")
"""
memory_file_path = self.memories_manager.get_memory_file_path(memory_name)
if not memory_file_path.exists():
return self._to_json({"error": f"Memory {memory_name} not found"})

with open(memory_file_path, encoding=self.memories_manager._encoding) as f:
raw = f.read()

frontmatter, _ = parse_frontmatter(raw)
return self._to_json(frontmatter)


class MemoryAddFrontmatterTool(Tool, ToolMarkerCanEdit, ToolMarkerOptional):
"""
OPTIONAL. Adds or updates a frontmatter key/value pair in a memory file.

Frontmatter is a YAML-like metadata block at the top of a memory file.

Use this tool only for short metadata that can improve memory organization
or retrieval.
"""

def apply(self, memory_name: str, key: str, value: str) -> str:
"""
Adds or updates a frontmatter field in a memory.

:param memory_name: memory name (may include "/")
:param key: frontmatter field name
:param value: frontmatter field value
"""
memory_file_path = self.memories_manager.get_memory_file_path(memory_name)

if not memory_file_path.exists():
return self._to_json({"error": f"Memory {memory_name} not found"})

with open(memory_file_path, encoding=self.memories_manager._encoding) as f:
raw = f.read()

frontmatter, body = parse_frontmatter(raw)

frontmatter[key] = value

updated_content = render_frontmatter(frontmatter, body)

return self.memories_manager.save_memory(
memory_name,
updated_content,
is_tool_context=True,
)


class DeleteMemoryTool(Tool, ToolMarkerCanEdit):
Expand Down
103 changes: 103 additions & 0 deletions src/serena/util/frontmatter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""
Minimal frontmatter parsing utilities for memory files.

Supports a simple YAML-like frontmatter block at the top of a file.

Example:

---
summary: Some short text
author: Mehdi
priority: high
---

Body content...

"""

from __future__ import annotations

from dataclasses import dataclass


@dataclass(frozen=True)
class FrontmatterParseResult:
frontmatter: dict[str, str]
body: str


class FrontmatterParser:
"""
Minimal YAML-like frontmatter parser.

It extracts key/value pairs from a frontmatter block at the top of a file and
returns the remaining body content.
"""

@staticmethod
def parse(content: str) -> FrontmatterParseResult:
frontmatter: dict[str, str] = {}

if not content.startswith("---"):
return FrontmatterParseResult(frontmatter=frontmatter, body=content)

lines = content.splitlines()
if len(lines) < 3:
return FrontmatterParseResult(frontmatter=frontmatter, body=content)

if lines[0].strip() != "---":
return FrontmatterParseResult(frontmatter=frontmatter, body=content)

closing_index = None
for i in range(1, len(lines)):
if lines[i].strip() == "---":
closing_index = i
break

if closing_index is None:
return FrontmatterParseResult(frontmatter=frontmatter, body=content)

for line in lines[1:closing_index]:
if ":" not in line:
continue

key, value = line.split(":", 1)
key = key.strip()
value = value.strip().strip('"')
frontmatter[key] = value

body = "\n".join(lines[closing_index + 1 :])
return FrontmatterParseResult(frontmatter=frontmatter, body=body)

@staticmethod
def render(frontmatter: dict[str, str], body: str) -> str:
if not frontmatter:
return body

lines = ["---"]
for key, value in frontmatter.items():
lines.append(f"{key}: {value}")
lines.append("---")
lines.append("")
lines.append(body)

return "\n".join(lines)


def parse_frontmatter(content: str) -> tuple[dict[str, str], str]:
"""
Backwards-compatible functional wrapper.

:return: (frontmatter_dict, body_content)
"""
result = FrontmatterParser.parse(content)
return result.frontmatter, result.body


def render_frontmatter(frontmatter: dict[str, str], body: str) -> str:
"""
Backwards-compatible functional wrapper.

:return: memory content with frontmatter
"""
return FrontmatterParser.render(frontmatter, body)
Loading