diff --git a/letta/schemas/memory.py b/letta/schemas/memory.py
index 30150864..79c9cb5f 100644
--- a/letta/schemas/memory.py
+++ b/letta/schemas/memory.py
@@ -190,11 +190,49 @@ class Memory(BaseModel, validate_assignment=True):
s.write("\n")
s.write("\n")
+ def _render_memory_blocks_git(self, s: StringIO):
+ """Render memory blocks as individual file tags with YAML frontmatter.
+
+ Each block is rendered as ---frontmatter---value,
+ 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\nmemory/\n")
+ s.write("\n\n\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("")
@@ -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 ""
diff --git a/letta/server/rest_api/routers/v1/git_http.py b/letta/server/rest_api/routers/v1/git_http.py
index db8cd658..d935e4bf 100644
--- a/letta/server/rest_api/routers/v1/git_http.py
+++ b/letta/server/rest_api/routers/v1/git_http.py
@@ -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(
diff --git a/letta/services/block_manager_git.py b/letta/services/block_manager_git.py
index 22d97999..1fc4424a 100644
--- a/letta/services/block_manager_git.py
+++ b/letta/services/block_manager_git.py
@@ -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,
diff --git a/letta/services/memory_repo/memfs_client_base.py b/letta/services/memory_repo/memfs_client_base.py
index b5122bd3..08f61da6 100644
--- a/letta/services/memory_repo/memfs_client_base.py
+++ b/letta/services/memory_repo/memfs_client_base.py
@@ -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",
),