refactor: drop memory/ prefix from git memory repo file paths and update core memory rendering [LET-7356] (#9395)
This commit is contained in:
committed by
Caren Thomas
parent
5fd5a6dd07
commit
bbc648909b
@@ -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 ""
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user