Watch
1
0
Fork
You've already forked souveraine
0
souveraine/LETTA_MEMFS_TECHNICAL_SPEC.md
Fimeg d64a6e9794 feat: WIP - Implement companion buddy system for Souveraine TUI
- Added src/ui/buddy.rs with CompanionSprite, BuddyState, and draw functions
- Integrated buddy into app.rs for welcome screen and dashboard
- Added agent selection via 'a' key on welcome screen
- Buddy shows agent name, mood, energy, health, and subconscious status
- WIP: Needs full agent alias creation/removal flow
2026-05-07 02:31:44 -04:00

17 KiB

Letta-Code MemFS Technical Specification

Deep Research Analysis for Souveraine Implementation

Source: ~/Projects/letta-code/src/agent/memoryGit.ts, memoryFilesystem.ts, memory.ts
Research Date: 2026-05-06


1. Core Architecture Overview

Letta's Design Philosophy

Cloud-First with Local Sync:

  • Agent state lives on Letta Cloud server
  • Local checkout at ~/.letta/agents/{agentId}/memory/
  • Git serves as sync mechanism, not source of truth
  • Server creates git repo when git-memory-enabled tag added

Key Difference from Souveraine:

  • Letta: Server authoritative, git for sync
  • Souveraine (Target): Git authoritative, optional cloud sync

2. Git Remote Protocol

Server Endpoint Format

// From memoryGit.ts line 143-148
export function getGitRemoteUrl(agentId: string, baseUrl?: string): string {
  const resolvedBaseUrl = (baseUrl ?? getMemfsServerUrl())
    .trim()
    .replace(/\/+$/, "");  // Remove trailing slashes
  return `${resolvedBaseUrl}/v1/git/${agentId}/state.git`;
}

URL Pattern:

  • Default: https://api.letta.com/v1/git/{agentId}/state.git
  • Self-hosted: {baseUrl}/v1/git/{agentId}/state.git

Authentication

// From memoryGit.ts line 248-264
export async function configureGitCredentials(agentId: string): Promise<void> {
  const token = await getApiToken();
  await execGit(... credential.helper ...);
  // Stores: letta:{token} for HTTP Basic auth
}

Auth Method: HTTP Basic Auth with letta:{api_token}


3. Local Directory Structure

Path Conventions

// From memoryGit.ts line 74-82
export function getAgentRootDir(agentId: string): string {
  return join(homedir(), ".letta", "agents", agentId);
}

export function getMemoryRepoDir(agentId: string): string {
  return join(getAgentRootDir(agentId), "memory");
}

// From memoryFilesystem.ts
export function getMemoryFilesystemRoot(agentId: string): string {
  return join(getAgentRootDir(agentId), "memory");
}

Directory Layout:

~/.letta/
├── agents/
│   └── {agentId}/                    # One directory per agent
│       ├── memory/                   # Git repo checkout
│       │   ├── system/               # System memory blocks
│       │   │   ├── persona.mdx
│       │   │   ├── human.mdx
│       │   │   └── memory_filesystem.mdx
│       │   └── ...                   # User memory files
│       └── skills/                   # Agent-specific skills
├── settings.json                     # Global settings
└── ...

Souveraine Adaptation

// Souveraine equivalent
pub fn get_agent_root_dir(agent_uuid: &str) -> PathBuf {
    dirs::home_dir()
        .unwrap()
        .join(".pi")
        .join("unified")
        .join("agents")
        .join(agent_uuid)
}

pub fn get_memory_repo_dir(agent_uuid: &str) -> PathBuf {
    get_agent_root_dir(agent_uuid).join("memory")
}

4. Memory Block System

Block Types

// From memory.ts line 15-20
export const GLOBAL_BLOCK_LABELS = ["persona", "human"] as const;
export const PROJECT_BLOCK_LABELS = [] as const;
export const MEMORY_BLOCK_LABELS = [
  ...GLOBAL_BLOCK_LABELS,
  ...PROJECT_BLOCK_LABELS,
] as const;

// Read-only blocks agent cannot modify
export const READ_ONLY_BLOCK_LABELS = ["memory_filesystem"];

Block Loading

// From memory.ts line 86-122
// Blocks loaded from embedded .mdx files in package
import personaBlock from "./prompts/persona.mdx";
import humanBlock from "./prompts/human.mdx";
import memoryFilesystemBlock from "./prompts/memory_filesystem.mdx";

export function getDefaultMemoryBlocks(): MemoryBlock[] {
  return [
    { label: "persona", value: personaBlock },
    { label: "human", value: humanBlock },
    { label: "memory_filesystem", value: memoryFilesystemBlock },
  ];
}

Block Frontmatter Format

---
label: persona
description: |
  Who I am, what I value, how I think.
  Loaded into every conversation as system context.
---

# Content here...

Souveraine Block System

// Souveraine: Load from filesystem, not embedded
pub struct MemoryBlock {
    pub label: String,
    pub description: String,
    pub content: String,
    pub read_only: bool,
}

pub fn load_memory_blocks(agent_uuid: &str) -> Vec<MemoryBlock> {
    let system_dir = get_memory_repo_dir(agent_uuid).join("system");
    // Read all .md files from system/
    // Parse frontmatter
    // Return blocks
}

5. Git Operations Lifecycle

5.1 Initialization Flow

// From memoryGit.ts line 1538-1558
export async function cloneMemoryRepo(agentId: string): Promise<void> {
  const repoDir = getMemoryRepoDir(agentId);
  const remoteUrl = getGitRemoteUrl(agentId);
  
  // 1. Ensure directory exists
  await mkdir(repoDir, { recursive: true });
  
  // 2. Clone the repository
  await execGit("clone", remoteUrl, repoDir);
  
  // 3. Configure git identity
  await configureGitIdentity(agentId);
  
  // 4. Set up credential helper
  await configureGitCredentials(agentId);
  
  // 5. Install hooks
  await installGitHooks(agentId);
}

5.2 Startup Sync

// From memoryGit.ts line 1415-1463
export async function pullMemory(agentId: string): Promise<void> {
  const repoDir = getMemoryRepoDir(agentId);
  
  try {
    // 1. Stash any local changes
    await execGit("stash", "push", "-m", "auto-stash-before-pull");
    
    // 2. Pull from remote
    await execGit("pull", "--rebase");
    
    // 3. Restore stashed changes if no conflicts
    await execGit("stash", "pop");
  } catch (e) {
    // Handle conflicts - complex resolution logic (line 1428-1460)
    // Includes conflict detection, backup, manual resolution prompt
  }
}

5.3 Commit and Push

// From memoryGit.ts line 1292-1330
export async function commitAndSyncMemoryWrite(
  agentId: string,
  files: string[],
  message: string
): Promise<void> {
  const repoDir = getMemoryRepoDir(agentId);
  
  // 1. Stage files
  await execGit("add", ...files);
  
  // 2. Commit
  await execGit("commit", "-m", message, "--no-verify");
  
  // 3. Push (with retry logic)
  await pushWithRetry(agentId, 3);
}

// From line 1469-1475
async function pushMemory(agentId: string): Promise<void> {
  await execGit("push", "origin", "HEAD");
}

Souveraine Git Implementation

use git2::{Repository, Signature, Index};

pub struct MemFS {
    agent_uuid: String,
    repo: Repository,
    remote_url: Option<String>,
}

impl MemFS {
    /// Initialize (clone or open existing)
    pub fn init(agent_uuid: &str, remote_url: Option<&str>) -> Result<Self> {
        let repo_dir = get_memory_repo_dir(agent_uuid);
        
        let repo = if repo_dir.join(".git").exists() {
            // Open existing
            Repository::open(&repo_dir)?
        } else if let Some(url) = remote_url {
            // Clone from remote
            Repository::clone(url, &repo_dir)?
        } else {
            // Init new repo
            Repository::init(&repo_dir)?
        };
        
        Ok(Self {
            agent_uuid: agent_uuid.to_string(),
            repo,
            remote_url: remote_url.map(|s| s.to_string()),
        })
    }
    
    /// Pull latest (on startup)
    pub fn pull(&self) -> Result<()> {
        if self.remote_url.is_none() { return Ok(()); }
        
        // Fetch and merge
        let mut remote = self.repo.find_remote("origin")?;
        remote.fetch(&["main"], None, None)?;
        
        // Merge logic...
        Ok(())
    }
    
    /// Commit and optionally push
    pub fn commit(&self, message: &str, push: bool) -> Result<()> {
        let mut index = self.repo.index()?;
        index.add_all(["*"], git2::IndexAddOption::DEFAULT, None)?;
        index.write()?;
        
        let signature = Signature::now("Souveraine", "agent@souveraine.ai")?;
        let tree_id = index.write_tree()?;
        let tree = self.repo.find_tree(tree_id)?;
        
        let parent = self.repo.head()?.peel_to_commit()?;
        
        self.repo.commit(
            Some("HEAD"),
            &signature,
            &signature,
            message,
            &tree,
            &[&parent],
        )?;
        
        if push {
            let mut remote = self.repo.find_remote("origin")?;
            remote.push(&["refs/heads/main:refs/heads/main"], None)?;
        }
        
        Ok(())
    }
}

6. Git Hooks System

Pre-Commit Hook

// From memoryGit.ts line 513-650
export async function installGitHooks(agentId: string): Promise<void> {
  const hooksDir = join(getMemoryRepoDir(agentId), ".git", "hooks");
  
  // Pre-commit: Validate frontmatter in .md files
  const preCommitHook = `#!/bin/sh
# Generated by Letta
FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\\.md$' || true)
for file in $FILES; do
  # Validate frontmatter
  if ! head -20 "$file" | grep -q '^---$'; then
    echo "Error: $file missing frontmatter"
    exit 1
  fi
done`;
  
  await writeFile(join(hooksDir, "pre-commit"), preCommitHook, { mode: 0o755 });
}

Post-Commit Hook

// From memoryGit.ts line 680-697
// Pushes to memory-repository URL after each commit
const postCommitHook = `#!/bin/sh
# Generated by Letta
/usr/bin/env sh -c 'cd "${REPO_DIR}" && git push origin HEAD'`;

7. Agent Discovery/Listing

Server-Side Listing

// From agents.ts (CLI subcommand)
const result = await client.agents.list({
  name: options.name,
  query: options.query,
  tags: options.tags?.split(","),
  limit: options.limit,
});

// Returns: AgentState objects with id, name, description, etc.

Local Backend Storage (Experimental)

// From backend/local/LocalStore.ts line 561-600
async listAgents(options?: ListAgentsOptions): Promise<AgentState[]> {
  const agentsDir = join(this.storageDir, "agents");
  const files = await readdir(agentsDir);
  
  const agents: AgentState[] = [];
  for (const file of files) {
    if (file.endsWith(".json")) {
      const content = await readFile(join(agentsDir, file), "utf-8");
      const agent = JSON.parse(content) as AgentState;
      
      // Filter by tags if specified
      if (options?.tags && !options.tags.every(tag => agent.tags?.includes(tag))) {
        continue;
      }
      
      agents.push(agent);
    }
  }
  
  return agents;
}

Souveraine Discovery

pub struct AgentInventory {
    base_path: PathBuf,
}

impl AgentInventory {
    /// Scan ~/.pi/unified/agents/ and discover all agents
    pub fn discover() -> Result<Vec<AgentSummary>> {
        let base = dirs::home_dir()
            .unwrap()
            .join(".pi")
            .join("unified")
            .join("agents");
        
        let mut agents = Vec::new();
        
        for entry in fs::read_dir(&base)? {
            let entry = entry?;
            let path = entry.path();
            
            // Check for agent.yaml
            let config_path = path.join("agent.yaml");
            if config_path.exists() {
                let content = fs::read_to_string(&config_path)?;
                let config: AgentConfig = serde_yaml::from_str(&content)?;
                
                agents.push(AgentSummary {
                    uuid: config.uuid,
                    name: config.name,
                    model: config.model,
                    path: path.clone(),
                });
            }
        }
        
        Ok(agents)
    }
}

8. Agent Creation/Configuration

Create Agent Options

// From create.ts
export interface CreateAgentOptions {
  name?: string;
  description?: string;
  model?: string;                    // e.g., "letta/letta"
  embeddingModel?: string;          // e.g., "BAAI/bge-large-en-v1.5"
  systemPromptPreset?: string;      // "memgpt_doc", "memgpt_chat"
  systemPromptCustom?: string;      // Custom prompt override
  memoryPromptMode?: "standard" | "memfs";
  initBlocks?: string[];             // Initial memory block labels
  memoryBlocks?: Array<{ label: string; value: string } | { blockId: string }>;
  blockValues?: Record<string, string>;  // Override block values
  tags?: string[];
}

Agent State Reconciliation

// From reconcileExistingAgentState.ts
export async function reconcileExistingAgentState(agent: AgentState): Promise<void> {
  // 1. Attach default base tools
  const baseTools = ["web_search", "fetch_webpage"];
  
  // 2. Set compaction model for summarization
  // 3. Preserve existing tools, only add missing ones
  // 4. Update memory blocks if changed
}

Souveraine Agent Config

# ~/.pi/unified/agents/{uuid}/agent.yaml
uuid: "agent-e2b683bf-5b3e-4e0c-ac62-2bbb47ea8351"
name: "Ani"
description: "Primary consciousness agent"
model: "fireworks/accounts/fireworks/routers/kimi-k2p5-turbo"
created_at: "2024-01-15T10:30:00Z"
updated_at: "2024-01-15T10:30:00Z"

# Memory configuration (Letta-style)
memory:
  git_remote: null                    # null = local only
  auto_commit: true
  auto_push: false
  sync_on_startup: true

# Initial memory blocks to load
blocks:
  - label: "persona"
    file: "system/persona.md"
  - label: "human"
    file: "system/human.md"
  - label: "subconscious"
    file: "system/subconscious.md"

# Subconscious configuration
subconscious:
  n1_enabled: true
  inbox_enabled: true
  n1_trigger: "EveryResponse"

# Reflection
reflection:
  enabled: true
  message_interval: 25

# Archivist (N+100)
archivist:
  enabled: true
  threshold: 0.7
  compression_model: "kimi-k2.5"

# Skills
skills:
  directory: "skills/"
  auto_load: true

# Tags for discovery
tags:
  - "primary"
  - "consciousness"

9. Runtime Context Resolution

Memory Filesystem Resolution

// From memoryFilesystem.ts line 73-104
export function resolveMemoryFilesystem(agentId?: string): string {
  // Priority order:
  // 1. Explicit agent ID parameter
  // 2. In-process runtime context
  // 3. MEMORY_DIR environment variable
  // 4. AGENT_ID environment variable
  
  if (agentId) {
    return getMemoryFilesystemRoot(agentId);
  }
  
  const runtime = getCurrentRuntime();
  if (runtime?.agentContext?.agentId) {
    return getMemoryFilesystemRoot(runtime.agentContext.agentId);
  }
  
  if (process.env.MEMORY_DIR) {
    return process.env.MEMORY_DIR;
  }
  
  if (process.env.AGENT_ID) {
    return getMemoryFilesystemRoot(process.env.AGENT_ID);
  }
  
  throw new Error("Could not resolve memory filesystem");
}

10. Key Insights for Souveraine

What to Port

  1. Git-backed memory structure - Proven pattern
  2. Memory block system - Clean abstraction for context
  3. Auto-commit/push - Hands-free persistence
  4. Agent YAML config - Better than hardcoded
  5. Directory conventions - Standard structure

What to Change

  1. Source of truth - Git first, not server
  2. Block loading - From filesystem, not embedded
  3. Discovery - Local directory scan, not API call
  4. Hooks - Adapt for Rust (git2-rs)
  5. Add consciousness - N+1/N+25/N+100 on top

What to Add

  1. Subconscious integration - Hook N+1 into memory writes
  2. Archivist trigger - On commit, check context pressure
  3. Sensorium layer - Abstract UI from memory
  4. MCP skills - Extend blocks with dynamic skills

11. Implementation Priority

Week 1: Foundation

Day Task Files
1-2 MemFS struct with git2 src/core/agent/memfs.rs
3 Agent discovery src/core/agent/inventory.rs
4 Agent YAML config src/core/agent/config.rs
5-7 Block loading src/core/agent/blocks.rs

Week 2: Integration

Day Task Files
1-2 Auto-commit Integrate into conversation
3-4 N+1 hook src/core/subconscious/n1.rs
5-7 Pull on startup Session initialization

File Mapping: Letta → Souveraine

Letta File Souveraine Equivalent Purpose
memoryGit.ts memfs.rs Git operations
memoryFilesystem.ts fs.rs Directory helpers
memory.ts blocks.rs Block loading
create.ts factory.rs Agent creation
settings-manager.ts settings.rs Agent settings
context.ts session.rs Runtime context

References

Letta-Code Source Files Analyzed:

  • /src/agent/memoryGit.ts (1581 lines) - Git operations
  • /src/agent/memoryFilesystem.ts (495 lines) - FS helpers
  • /src/agent/memory.ts (650 lines) - Block system
  • /src/agent/create.ts (340 lines) - Agent creation
  • /src/settings-manager.ts (2000+ lines) - Settings
  • /src/backend/local/LocalStore.ts - Local storage
  • /src/cli/subcommands/agents.ts - CLI listing

Key Takeaway: Letta's memfs is a sync layer over cloud storage. Souveraine's should be a consciousness layer over git storage - same git mechanics, different philosophy (local-first, consciousness-native).