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

664 lines
17 KiB
Markdown

# 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```rust
// 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
```typescript
// 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
```typescript
// 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
```yaml
---
label: persona
description: |
Who I am, what I value, how I think.
Loaded into every conversation as system context.
---
# Content here...
```
### Souveraine Block System
```rust
// 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
```typescript
// 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
```typescript
// 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
```typescript
// 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
```rust
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
```typescript
// 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
```typescript
// 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
```typescript
// 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)
```typescript
// 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
```rust
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
```typescript
// 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
```typescript
// 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
```yaml
# ~/.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
```typescript
// 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).