Skip to content
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
37 changes: 36 additions & 1 deletion 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


class WriteMemoryTool(Tool, ToolMarkerCanEdit):
Expand Down Expand Up @@ -58,6 +59,40 @@ def apply(self, topic: str = "") -> str:
return self._to_json(self.memories_manager.list_memories(topic).to_dict())


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 DeleteMemoryTool(Tool, ToolMarkerCanEdit):
"""
Delete a memory file. Should only happen if a user asks for it explicitly,
Expand Down
80 changes: 80 additions & 0 deletions src/serena/util/frontmatter.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
"""
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)


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
Loading