refactor: drop memory/ prefix from git memory repo file paths and update core memory rendering [LET-7356] (#9395)

This commit is contained in:
Sarah Wooders
2026-02-09 20:49:41 -08:00
committed by Caren Thomas
parent 5fd5a6dd07
commit bbc648909b
4 changed files with 70 additions and 36 deletions

View File

@@ -190,11 +190,49 @@ class Memory(BaseModel, validate_assignment=True):
s.write("\n")
s.write("\n</memory_blocks>")
def _render_memory_blocks_git(self, s: StringIO):
"""Render memory blocks as individual file tags with YAML frontmatter.
Each block is rendered as <label.md>---frontmatter---value</label.md>,
matching the format stored in the git repo. Labels without a 'system/'
prefix get one added automatically.
"""
renderable = self._get_renderable_blocks()
if not renderable:
return
for idx, block in enumerate(renderable):
label = block.label or "block"
# Ensure system/ prefix
if not label.startswith("system/"):
label = f"system/{label}"
tag = f"{label}.md"
value = block.value or ""
s.write(f"\n\n<{tag}>\n")
# Build frontmatter (same fields as serialize_block)
front_lines = []
if block.description:
front_lines.append(f"description: {block.description}")
if block.limit is not None:
front_lines.append(f"limit: {block.limit}")
if getattr(block, "read_only", False):
front_lines.append("read_only: true")
if front_lines:
s.write("---\n")
s.write("\n".join(front_lines))
s.write("\n---\n")
s.write(f"{value}\n")
s.write(f"</{tag}>")
def _render_memory_filesystem(self, s: StringIO):
"""Render a filesystem tree view of all memory blocks.
Only rendered for git-memory-enabled agents. Shows all blocks
(system and non-system) as a tree with char counts and descriptions.
Only rendered for git-memory-enabled agents. Uses box-drawing
characters (├──, └──, │) like the Unix `tree` command.
"""
if not self.blocks:
return
@@ -211,26 +249,23 @@ class Memory(BaseModel, validate_assignment=True):
node = node.setdefault(part, {})
node[parts[-1]] = block
s.write("\n\n<memory_filesystem>\nmemory/\n")
s.write("\n\n<memory_filesystem>\n")
def _render_tree(node: dict, indent: int = 1):
prefix = " " * indent
def _render_tree(node: dict, prefix: str = ""):
# Sort: directories first, then files
dirs = sorted(k for k, v in node.items() if isinstance(v, dict))
files = sorted(k for k, v in node.items() if not isinstance(v, dict))
entries = [(d, True) for d in dirs] + [(f, False) for f in files]
for d in dirs:
s.write(f"{prefix}{d}/\n")
_render_tree(node[d], indent + 1)
for f in files:
block = node[f]
chars = len(block.value or "")
desc = block.description or ""
line = f"{prefix}{f}.md ({chars} chars)"
if desc:
line += f" - {desc}"
s.write(f"{line}\n")
for i, (name, is_dir) in enumerate(entries):
is_last = i == len(entries) - 1
connector = "└── " if is_last else "├── "
if is_dir:
s.write(f"{prefix}{connector}{name}/\n")
extension = " " if is_last else ""
_render_tree(node[name], prefix + extension)
else:
s.write(f"{prefix}{connector}{name}.md\n")
_render_tree(tree)
s.write("</memory_filesystem>")
@@ -353,15 +388,15 @@ class Memory(BaseModel, validate_assignment=True):
# Memory blocks (not for react/workflow). Always include wrapper for preview/tests.
if not is_react:
if is_line_numbered:
if self.git_enabled:
# Git-enabled: filesystem tree + file-style block rendering
self._render_memory_filesystem(s)
self._render_memory_blocks_git(s)
elif is_line_numbered:
self._render_memory_blocks_line_numbered(s)
else:
self._render_memory_blocks_standard(s)
# For git-memory-enabled agents, render a filesystem tree of all blocks
if self.git_enabled:
self._render_memory_filesystem(s)
if tool_usage_rules is not None:
desc = getattr(tool_usage_rules, "description", None) or ""
val = getattr(tool_usage_rules, "value", None) or ""

View File

@@ -498,10 +498,10 @@ async def _sync_after_push(actor_id: str, agent_id: str) -> None:
synced = 0
for file_path, content in files.items():
if not file_path.startswith("memory/") or not file_path.endswith(".md"):
if not file_path.endswith(".md"):
continue
label = file_path[len("memory/") : -3]
label = file_path[:-3]
expected_labels.add(label)
# Parse frontmatter to extract metadata alongside value
@@ -524,12 +524,12 @@ async def _sync_after_push(actor_id: str, agent_id: str) -> None:
logger.exception("Failed to sync block %s to PostgreSQL (agent=%s)", label, agent_id)
if synced == 0:
logger.warning("No memory/*.md files found in repo HEAD during post-push sync (agent=%s)", agent_id)
logger.warning("No *.md files found in repo HEAD during post-push sync (agent=%s)", agent_id)
else:
# Detach blocks that were removed in git.
#
# We treat git as the source of truth for which blocks are attached to
# this agent. If a memory/*.md file disappears from HEAD, detach the
# this agent. If a *.md file disappears from HEAD, detach the
# corresponding block from the agent in Postgres.
try:
existing_blocks = await _server_instance.agent_manager.list_agent_blocks_async(

View File

@@ -389,7 +389,7 @@ class GitEnabledBlockManager(BlockManager):
# Check which blocks are missing from repo
missing_blocks = []
for block in blocks:
expected_path = f"memory/{block.label}.md"
expected_path = f"{block.label}.md"
if expected_path not in repo_files:
missing_blocks.append(block)
@@ -552,7 +552,7 @@ class GitEnabledBlockManager(BlockManager):
if self.memory_repo_manager is None:
raise ValueError("Memory repo manager not configured")
path = f"memory/{label}.md" if label else None
path = f"{label}.md" if label else None
return await self.memory_repo_manager.get_history_async(
agent_id=agent_id,
actor=actor,

View File

@@ -26,8 +26,7 @@ from letta.utils import enforce_types
logger = get_logger(__name__)
# File paths within the memory repository
MEMORY_DIR = "memory"
# File paths within the memory repository (blocks stored at repo root as {label}.md)
# Default local storage path
DEFAULT_LOCAL_PATH = os.path.expanduser("~/.letta/memfs")
@@ -88,7 +87,7 @@ class MemfsClient:
initial_files = {}
for block in initial_blocks:
file_path = f"{MEMORY_DIR}/{block.label}.md"
file_path = f"{block.label}.md"
initial_files[file_path] = serialize_block(
value=block.value or "",
description=block.description,
@@ -137,8 +136,8 @@ class MemfsClient:
# Convert block files to PydanticBlock (metadata is in frontmatter)
blocks = []
for file_path, content in files.items():
if file_path.startswith(f"{MEMORY_DIR}/") and file_path.endswith(".md"):
label = file_path[len(f"{MEMORY_DIR}/") : -3]
if file_path.endswith(".md"):
label = file_path[:-3]
parsed = parse_block_markdown(content)
@@ -235,7 +234,7 @@ class MemfsClient:
await self._ensure_repo_exists(agent_id, actor)
file_path = f"{MEMORY_DIR}/{label}.md"
file_path = f"{label}.md"
file_content = serialize_block(
value=value,
description=description,
@@ -289,7 +288,7 @@ class MemfsClient:
changes = [
FileChange(
path=f"{MEMORY_DIR}/{block.label}.md",
path=f"{block.label}.md",
content=file_content,
change_type="add",
),
@@ -333,7 +332,7 @@ class MemfsClient:
changes = [
FileChange(
path=f"{MEMORY_DIR}/{label}.md",
path=f"{label}.md",
content=None,
change_type="delete",
),