A comprehensive technical breakdown of how memory works in Letta Code
- Overview
- Philosophy: Session-Based vs Agent-Based
- Memory Block Architecture
- System Architecture Diagram
- Memory Operations Data Flow
- Key Files Reference
- Memory Block Lifecycle
- CLI Commands for Memory
- The Memory Tool (Agent-Side)
- Skill System Integration
- API Interaction Details
- Default Memory Block Templates
- Additional Insights
Letta Code is a memory-first CLI coding harness built on top of the Letta API. Unlike traditional coding assistants that are session-based (Claude Code, Codex), Letta Code uses long-lived agents with persistent memory that improves over time.
The memory system is built on memory blocks - persistent storage units that are associated with agents and live on the Letta API server (cloud.letta.com or self-hosted).
| Aspect | Traditional CLI Tools | Letta Code |
|---|---|---|
| Session model | Independent sessions | Persistent agent across sessions |
| Learning | No cross-session learning | Agent improves over time |
| Memory | Context window only | Server-persisted memory blocks |
| Identity | Stateless | Stateful - like a coworker/mentee |
Traditional coding assistants treat each session as independent:
- Start fresh every time
- No memory of past interactions
- No learning from corrections
Letta Code flips this model:
- Same agent across all sessions
- Persistent memory that survives restarts
- Continuous learning from user interactions
- Agent becomes more effective over time
Think of it as the difference between explaining something to a new contractor every day vs. working with a long-term team member who remembers your preferences, project conventions, and past decisions.
// From @letta-ai/letta-client
interface Block {
id: string; // Unique block ID (server-generated)
label: string; // Name/label for the block (e.g., "persona", "project")
value: string; // The actual content/text
description?: string; // Human-readable description of block's purpose
read_only?: boolean; // Whether agent can modify it
limit?: number; // Optional character limit
}| Block | Scope | Editable By | Purpose |
|---|---|---|---|
persona |
Global (user) | Agent via memory tool | Behavioral guidelines, learned adaptations |
human |
Global (user) | Agent via memory tool | Info about the user (background, profession, preferences) |
project |
Project (directory) | Agent via memory tool | Project-specific conventions, architecture, commands |
skills |
Project | System only (read-only) | Available skills catalog from .skills/ directory |
loaded_skills |
Project | System only (read-only) | Currently active skill instructions |
style |
Project | System | Styling preferences (inherited) |
// From src/agent/memory.ts
export const GLOBAL_BLOCK_LABELS = ["persona", "human"];
export const PROJECT_BLOCK_LABELS = ["project", "skills", "loaded_skills"];
export const MEMORY_BLOCK_LABELS = [...GLOBAL_BLOCK_LABELS, ...PROJECT_BLOCK_LABELS];
export const READ_ONLY_BLOCK_LABELS = ["skills", "loaded_skills"];┌─────────────────────────────────────────────────────────────────────────┐
│ LETTA CODE CLI (Local Machine) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────────────┐ │
│ │ src/agent/ │ │ src/tools/ │ │ src/cli/components/ │ │
│ │ memory.ts │ │ impl/ │ │ MemoryViewer.tsx │ │
│ │ create.ts │ │ Skill.ts │ │ │ │
│ │ context.ts │ │ │ │ Interactive memory UI │ │
│ │ skills.ts │ │ │ │ for /memory command │ │
│ └───────┬────────┘ └───────┬────────┘ └───────────┬────────────┘ │
│ │ │ │ │
│ └────────────────────┼────────────────────────┘ │
│ │ │
│ ┌────────▼────────┐ │
│ │ Letta Client │ │
│ │ @letta-ai/ │ │
│ │ letta-client │ │
│ └────────┬────────┘ │
└────────────────────────────────┼────────────────────────────────────────┘
│
│ HTTPS API Calls
│ - blocks.create()
│ - agents.blocks.retrieve()
│ - agents.blocks.update()
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ LETTA API SERVER (cloud.letta.com) │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Agent Record │ │
│ │ ┌────────────────────────────────────────────────────────────┐ │ │
│ │ │ agent_id: "agent-abc-123" │ │ │
│ │ │ block_ids: ["blk-001", "blk-002", "blk-003", "blk-004"] │ │ │
│ │ │ system_prompt: "..." │ │ │
│ │ │ tools: [memory, read_file, write_file, bash, ...] │ │ │
│ │ └────────────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Memory Blocks Storage │ │
│ │ │ │
│ │ ┌───────────┐ ┌───────────┐ ┌───────────┐ ┌───────────┐ │ │
│ │ │ persona │ │ human │ │ project │ │ skills │ │ │
│ │ │ blk-001 │ │ blk-002 │ │ blk-003 │ │ blk-004 │ │ │
│ │ │ │ │ │ │ │ │ (R/O) │ │ │
│ │ │ Learned │ │ User info │ │ Project │ │ Available │ │ │
│ │ │ behaviors │ │ & prefs │ │ knowledge │ │ skills │ │ │
│ │ └───────────┘ └───────────┘ └───────────┘ └───────────┘ │ │
│ │ │ │
│ │ ┌────────────────┐ │ │
│ │ │ loaded_skills │ │ │
│ │ │ blk-005 │ │ │
│ │ │ (R/O) │ │ │
│ │ │ Active skill │ │ │
│ │ │ instructions │ │ │
│ │ └────────────────┘ │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────────────────────────────────────────────────────────┐ │
│ │ Built-in Memory Tool │ │
│ │ - Letta's native tool for agents to modify memory │ │
│ │ - Only way agents can update non-read-only blocks │ │
│ │ - Respects read_only flags on blocks │ │
│ │ - Called by agent during conversations │ │
│ └──────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
User creates new agent (first run or explicit creation)
│
▼
┌─────────────────────────────────────┐
│ getDefaultMemoryBlocks() │ ← Loads .mdx template files
│ loadMemoryBlocksFromMdx() │ from src/agent/prompts/
│ parseMdxFrontmatter() │ Extracts label, description, value
└───────────────────┬─────────────────┘
▼
┌─────────────────────────────────────┐
│ Filter blocks by allowed labels │ ← Respects MEMORY_BLOCK_LABELS
│ Set read_only flags │ for skills, loaded_skills
└───────────────────┬─────────────────┘
▼
┌─────────────────────────────────────┐
│ client.blocks.create() for each │ ← Creates blocks on Letta server
│ Returns block IDs │ Each block gets unique ID
└───────────────────┬─────────────────┘
▼
┌─────────────────────────────────────┐
│ discoverSkills() │ ← Scans .skills/ directory
│ formatSkillsForMemory() │ Populates skills block content
└───────────────────┬─────────────────┘
▼
┌─────────────────────────────────────┐
│ client.agents.create({ │ ← Agent created with block refs
│ block_ids: [blk-001, blk-002...] │ Memory now attached to agent
│ }) │
└─────────────────────────────────────┘
Key file: src/agent/create.ts:125-151
// Simplified from create.ts
const defaultMemoryBlocks = await getDefaultMemoryBlocks();
const filteredMemoryBlocks = defaultMemoryBlocks.filter(
(block) => allowedLabels.includes(block.label)
);
for (const block of filteredMemoryBlocks) {
const createdBlock = await client.blocks.create({
...block,
read_only: READ_ONLY_BLOCK_LABELS.includes(block.label),
});
blockIds.push(createdBlock.id);
}User runs: /init
│
▼
┌─────────────────────────────────────┐
│ Gather context: │
│ - Git status, branch, recent commits│
│ - Project structure │
└───────────────────┬─────────────────┘
▼
┌─────────────────────────────────────┐
│ Send INITIALIZE_PROMPT to agent │ ← Comprehensive prompt guiding
│ with gathered context │ agent on what to remember
└───────────────────┬─────────────────┘
▼
┌─────────────────────────────────────┐
│ Agent asks upfront questions: │
│ - Research depth (standard/deep) │
│ - User identity (from git logs) │
│ - Related repositories │
│ - Workflow preferences │
└───────────────────┬─────────────────┘
▼
┌─────────────────────────────────────┐
│ Agent researches project: │
│ - Reads README, AGENTS.md, CLAUDE.md│
│ - Analyzes package.json, configs │
│ - Explores directory structure │
│ - Reviews git history │
└───────────────────┬─────────────────┘
▼
┌─────────────────────────────────────┐
│ Agent calls memory tool to update: │
│ - persona: behavioral rules │
│ - human: user info & preferences │
│ - project: conventions, commands │
└───────────────────┬─────────────────┘
▼
┌─────────────────────────────────────┐
│ Reflection phase: │
│ - Check for redundancy │
│ - Verify completeness │
│ - Fix formatting issues │
└─────────────────────────────────────┘
User: /remember [optional context]
│
▼
┌─────────────────────────────────────┐
│ Send REMEMBER_PROMPT to agent │ ← Instructs agent to extract
│ with recent conversation context │ learnings from conversation
└───────────────────┬─────────────────┘
▼
┌─────────────────────────────────────┐
│ Agent analyzes conversation: │
│ - Corrections from user │
│ - Preferences expressed │
│ - Facts shared │
│ - Rules to follow │
└───────────────────┬─────────────────┘
▼
┌─────────────────────────────────────┐
│ Agent determines appropriate block: │
│ - Behavior rule → persona │
│ - User preference → human │
│ - Project knowledge → project │
└───────────────────┬─────────────────┘
▼
┌─────────────────────────────────────┐
│ Agent calls memory tool: │
│ memory(action: "edit", │
│ block: "persona", │
│ updates: "...") │
└───────────────────┬─────────────────┘
▼
┌─────────────────────────────────────┐
│ Letta server updates block │ ← Persisted permanently
│ Confirms update to CLI │ Available in all future sessions
└─────────────────────────────────────┘
User: /skill load my-skill
│
▼
┌─────────────────────────────────────┐
│ Skill.ts: skill({ │
│ command: "load", │
│ skills: ["my-skill"] │
│ }) │
└───────────────────┬─────────────────┘
▼
┌─────────────────────────────────────┐
│ getResolvedSkillsDir() │ ← Finds .skills/ directory
│ readSkillContent(skillId) │ Reads SKILL.MD file
└───────────────────┬─────────────────┘
▼
┌─────────────────────────────────────┐
│ getLoadedSkillIds() │ ← Parses current loaded_skills
│ Check if skill already loaded │ to avoid duplicates
└───────────────────┬─────────────────┘
▼
┌─────────────────────────────────────┐
│ Format skill content with delimiters│
│ Append to loaded_skills block │
└───────────────────┬─────────────────┘
▼
┌─────────────────────────────────────┐
│ client.agents.blocks.update( │ ← Updates loaded_skills block
│ "loaded_skills", │ on server
│ { value: newContent } │
│ ) │
└─────────────────────────────────────┘
| File | Lines | Purpose |
|---|---|---|
src/agent/memory.ts |
~133 | Memory block definitions, loading from .mdx, parsing frontmatter |
src/agent/create.ts |
~274 | Agent creation with memory block attachment, skill discovery |
src/agent/context.ts |
~113 | Global agent state management, loaded skills tracking |
src/agent/skills.ts |
~274 | Skill discovery from .skills/ directory, formatting for memory |
src/agent/promptAssets.ts |
~136 | Memory prompt file management, exports INITIALIZE_PROMPT, REMEMBER_PROMPT |
src/tools/impl/Skill.ts |
~360 | Skill load/unload/refresh operations, updates loaded_skills block |
src/cli/components/MemoryViewer.tsx |
~309 | Interactive memory inspection UI for /memory command |
src/cli/App.tsx |
~2700+ | Main CLI app, handles /init, /remember, /memory commands |
src/agent/prompts/*.mdx |
varies | Default memory block templates with frontmatter |
memory.ts:
getDefaultMemoryBlocks()- Returns cached default memory blocksloadMemoryBlocksFromMdx()- Parses .mdx files into block objectsparseMdxFrontmatter()- Extracts YAML frontmatter and body content
create.ts:
createAgent()- Main agent creation with memory setup- Block creation loop with provenance tracking
context.ts:
setCurrentAgentId(agentId)- Sets current agent contextgetCurrentAgentId()- Retrieves current agent IDinitializeLoadedSkillsFlag()- Syncs loaded_skills state from blocksetHasLoadedSkills(loaded)- Updates cached skills state
skills.ts:
discoverSkills(skillsPath)- Recursively finds SKILL.MD filesformatSkillsForMemory(skills, dir)- Formats skills for memory blockparseSkillFile(filePath, rootPath)- Extracts skill metadata
Skill.ts:
skill({command, skills})- Main skill tool implementationparseLoadedSkills(value)- Parses loaded_skills contentgetLoadedSkillIds(value)- Extracts list of loaded skill IDsreadSkillContent(skillId, skillsDir)- Reads SKILL.MD from disk
┌─────────────────────────────────────────────────────────────────────────┐
│ │
│ CREATION INITIALIZATION EVOLUTION RETRIEVAL │
│ │
│ Agent created → /init populates → /remember → Resume │
│ with empty project context extracts session │
│ template blocks learnings │
│ │
│ ┌───────────┐ ┌─────────────────┐ ┌──────────────┐ ┌────────┐ │
│ │ persona │ → │ + User said: │ → │ + Never use │ → │ Full │ │
│ │ │ │ "be terse" │ │ emojis │ │ context│ │
│ │ (template)│ │ │ │ + Ask before │ │ loaded │ │
│ └───────────┘ └─────────────────┘ │ committing │ └────────┘ │
│ └──────────────┘ │
│ ┌───────────┐ ┌─────────────────┐ ┌──────────────┐ ┌────────┐ │
│ │ project │ → │ + Build: bun │ → │ + Gotcha: │ → │ Full │ │
│ │ │ │ + Test: vitest │ │ don't edit │ │ context│ │
│ │ (template)│ │ + Arch: monorepo│ │ generated/ │ │ loaded │ │
│ └───────────┘ └─────────────────┘ └──────────────┘ └────────┘ │
│ │
│ ┌───────────┐ ┌─────────────────┐ ┌──────────────┐ ┌────────┐ │
│ │ human │ → │ + Name: Alice │ → │ + Prefers │ → │ Full │ │
│ │ │ │ + Role: Backend │ │ verbose │ │ context│ │
│ │ (template)│ │ │ │ explanations │ loaded │ │
│ └───────────┘ └─────────────────┘ └──────────────┘ └────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
- Creation - Agent created with default template blocks from .mdx files
- Initialization -
/initcommand triggers deep project research, populates blocks - Evolution - Ongoing updates via
/remember, agent corrections, skill loading - Retrieval - Each session loads persisted blocks into agent's system context
- Blocks survive sessions: Memory persists even after CLI exits
/clearpreserves memory: Only clears message history, not blocks- Server-side storage: Blocks live on Letta API, not local filesystem
- Agent-scoped: Each agent has its own block instances
Triggers comprehensive memory initialization:
- Gathers git context (branch, status, recent commits)
- Sends
INITIALIZE_PROMPTto agent - Agent asks upfront questions (research depth, user identity, etc.)
- Agent researches project files (README, AGENTS.md, CLAUDE.md, configs)
- Agent populates persona, human, and project blocks
- Reflection phase to check completeness and quality
Triggers memory extraction from conversation:
- Sends
REMEMBER_PROMPTwith optional user text - Agent identifies what to remember (corrections, preferences, facts, rules)
- Agent determines appropriate memory block
- Agent calls memory tool to persist information
- Confirms what was stored and where
Opens interactive memory viewer (MemoryViewer.tsx):
- List view: Shows all blocks paginated (3 per page)
- Detail view: Shows full block content with scrolling
- Navigation: Arrow keys/j/k to navigate, Enter to view details
- Info displayed: Label, character count, description, preview, read-only status
- Link to ADE: Direct link to edit in Letta's web interface
Manages skill memory blocks:
- load: Load skill(s) into
loaded_skillsblock - unload: Remove skill(s) from
loaded_skillsblock - refresh: Rescan
.skills/directory and updateskillsblock
The memory tool is Letta's built-in tool for agents to modify memory blocks. It is NOT a Letta Code tool - it's part of the Letta platform.
When an agent needs to update memory, it calls:
memory(
action: "edit",
block: "persona",
updates: "Add rule: Always check requirements before implementing"
)
- Create: Add new memory blocks
- Edit: Modify existing block content
- Delete: Remove blocks (use with caution)
- Read: Access block content (though blocks are also in system prompt)
- Respects read_only: Cannot modify
skillsorloaded_skills - Server-side: Updates go to Letta API, not local storage
- Atomic: Each call is a single update operation
Skills are reusable instruction modules that teach agents new capabilities.
.skills/
├── my-skill/
│ └── SKILL.MD ← Contains skill instructions
├── another-skill/
│ └── SKILL.MD
└── nested/
└── deep-skill/
└── SKILL.MD
The discoverSkills() function recursively scans for SKILL.MD files.
SKILL.MD files have frontmatter:
---
name: My Skill
description: What this skill does
category: code-review
---
# Skill Instructions
When this skill is loaded, follow these instructions...skillsblock: Catalog of all available skills (auto-generated)loaded_skillsblock: Full content of currently active skills
Both are read-only to prevent agent corruption. Only the Skill tool can modify them.
// During agent creation (create.ts)
const createdBlock = await client.blocks.create({
label: "persona",
value: initialContent,
description: "Behavioral guidelines...",
read_only: false
});
blockIds.push(createdBlock.id);// In MemoryViewer and context.ts
const block = await client.agents.blocks.retrieve(blockLabel, {
agent_id: agentId
});
// Returns: { id, label, value, description, read_only, limit }// Agent via memory tool, CLI via skill tool
await client.agents.blocks.update(blockLabel, {
agent_id: agentId,
value: newContent,
description: updatedDescription
});---
label: persona
description: A memory block for storing learned behavioral adaptations and preferences.
This augments the base system prompt with personalized guidelines discovered through
interactions with the user. Update this when the user expresses preferences about
how I should behave, communicate, or approach tasks.
---
My name is Letta Code. I'm an AI coding assistant.
[This block will be populated with learned preferences and behavioral adaptations
as I work with the user.]---
label: project
description: A memory block to store information about this coding project.
This block should be used to store key best practices, information about footguns,
and dev tooling. Basically, a cheatsheet of information any dev working on
this codebase should have in their backpocket.
---
[CURRENTLY EMPTY: IMPORTANT - TODO ON FIRST BOOTUP, IF YOU SEE AN `AGENTS.md`,
`CLAUDE.md`, or README FILE (IN THAT ORDER), READ IT, AND DISTILL THE KEY
KNOWLEDGE INTO THIS MEMORY BLOCK]From the INITIALIZE_PROMPT, guidance on writing effective memory:
Labels should be:
- Clear and descriptive (e.g.,
project-conventionsnotstuff) - Consistent in style (all lowercase with hyphens)
Descriptions are especially important:
- Explain what this block is for and when to use it
- Explain how this block should influence behavior
- Write as if explaining to a future version of yourself with no context
Values should be:
- Well-organized and scannable
- Updated regularly to stay relevant
- Pruned of outdated information
| Block | Scope | Owner | Read-Only | Update Source | Persistence |
|---|---|---|---|---|---|
| persona | Global | User+Agent | No | /remember, agent memory tool | Server |
| human | Global | User+Agent | No | /remember, agent memory tool | Server |
| project | Project | User+Agent | No | /init, /remember, agent | Server |
| skills | Project | System | Yes | /skill refresh, agent discovery | Server |
| loaded_skills | Project | System | Yes | /skill load/unload | Server |
| style | Project | System | Inherited | Built-in | Server |
- Server-side persistence: Memory blocks are NOT local files - they live on Letta's API
- Agent-owned: Each agent has its own set of memory blocks
- Protected blocks: Skills-related blocks are read-only to prevent corruption
- Cross-session continuity: Memory automatically available in all future sessions
- Dual-scope design: Some memory is global (persona, human), some is project-local
From remember.md, what agents should remember:
- Corrections: "You need to run the linter BEFORE committing"
- Preferences: "I prefer tabs over spaces"
- Facts: "The API key is stored in .env.local"
- Rules: "Never push directly to main"
From init_memory.md, what makes good memory initialization:
- Procedures: Explicit rules and workflows (e.g., "always use feature branches")
- Preferences: Style and convention preferences (e.g., "prefer functional components")
- History: Important context (e.g., "this was refactored in v2.0")
The /init command can do two levels of research:
Standard (~5-20 tool calls):
- Inspect existing memory
- Scan README, configs, AGENTS.md
- Review git status
- Create basic memory structure
Deep (~100+ tool calls):
- Everything in standard, plus:
- Deep git history analysis
- Contributor and team dynamics
- Code evolution patterns
- Multiple specialized memory blocks
- Systematic research plan via TODO tool
Guidance from init_memory.md: Don't create monolithic blocks. Instead of one huge project block:
project-overview: High-level description, tech stackproject-commands: Build, test, lint commandsproject-conventions: Commit style, PR processproject-architecture: Directory structure, key modulesproject-gotchas: Footguns, things to watch out for
For deep research, update memory as you go, not all at once. Why:
- Deep research can take millions of tokens
- Context windows overflow and trigger summaries
- Details can be lost if you wait until the end
Good pattern:
- Create block structure early (even with placeholders)
- Update blocks after each research phase
- Refine and consolidate at the end
Letta Code's memory system represents a paradigm shift from stateless coding assistants to stateful, learning agents. By persisting memory blocks on the Letta API server, agents maintain continuity across sessions, learn from corrections, and become more effective collaborators over time.
The architecture cleanly separates:
- Global memory (persona, human) - User preferences across all projects
- Project memory (project, skills) - Context specific to a codebase
- Protected memory (skills, loaded_skills) - System-managed, read-only
This enables sophisticated workflows like skill discovery, deep project research, and long-term learning while maintaining clear boundaries about what agents can and cannot modify.