How to Set Up a Custom Letta Agent with a Different Memory Block Architecture
You can configure a custom memory block architecture by creating a new .af JSON file that defines your specific block labels, importing it via importDefaultAgent in scripts/agent_config.ts, and setting the resulting agent ID in your environment or config file—no hook modifications required.
The Claude-Subconscious system treats Letta agents as declarative JSON files (.af format) containing configurable memory blocks. When you need to track project health metrics, coding style preferences, or ticket statuses instead of the default memory layout, you can design a custom agent with a different memory block architecture by editing the agent definition and reusing the existing synchronization infrastructure.
Understanding the Memory Block Architecture
In the letta-ai/claude-subconscious codebase, an agent is simply a JSON-encoded .af file containing a blocks array. Each block is a label-value pair with optional description and limit fields, persisted in Letta’s git-backed memory filesystem (memfs).
The sync logic is completely generic. When Claude Code hooks run, fetchAgent in scripts/conversation_utils.ts (lines 441-447) loads whatever blocks exist, formatMemoryBlocksAsXml (lines 442-466) converts them to XML for injection into CLAUDE.md, and detectChangedBlocks in scripts/sync_letta_memory.ts (lines 108-119) calculates diffs between runs. Because these functions operate on the blocks array dynamically, you can freely redesign the memory-block layout without changing any TypeScript code.
Steps to Create a Custom Agent Memory Architecture
1. Define Your Custom Blocks in a New .af File
Create a new agent file (e.g., my-agent.af) following the schema demonstrated in Subconscious.af (lines 24-30). Define your custom memory blocks with unique labels, descriptions, and token limits.
{
"agents": [
{
"name": "MyCustomAgent",
"memory_blocks": [],
"tools": [],
"block_ids": [
"block-0",
"block-1",
"block-2"
],
"tags": [
"origin:claude-subconcious"
],
"system": "You are a custom agent that tracks project health, user-style preferences, and pending tickets.",
"blocks": [
{
"label": "project_health",
"description": "High-level health metrics (build status, test coverage, recent failures).",
"value": "(No data yet)",
"limit": 4000
},
{
"label": "style_preferences",
"description": "User-specific coding style choices (indentation, naming, lint rules).",
"value": "(No data yet)",
"limit": 3000
},
{
"label": "pending_tickets",
"description": "Open TODOs / tickets mentioned in the session.",
"value": "(No pending tickets)",
"limit": 3000
}
]
}
],
"blocks": [],
"tools": []
}
2. Import the Custom Agent
Use the importDefaultAgent helper in scripts/agent_config.ts (lines 78-86) to POST your .af file to the Letta server. This function reads the file, uploads it to the /agents/import endpoint, and returns the new agent ID.
import { importDefaultAgent } from './agent_config.js';
import * as path from 'path';
const CUSTOM_AGENT_PATH = path.resolve('my-agent.af');
async function importMyAgent(apiKey: string): Promise<string> {
// Temporarily override the default path constant
(global as any).DEFAULT_AGENT_FILE = CUSTOM_AGENT_PATH;
const agentId = await importDefaultAgent(apiKey);
console.log('✅ Imported custom agent:', agentId);
return agentId;
}
3. Configure the Agent ID
Expose the new ID to the hooks by either:
- Setting the
LETTA_AGENT_IDenvironment variable, or - Saving it to
~/.letta/claude-subconscious/config.json
The validation logic in agent_config.ts (lines 33-42) checks both locations when resolving which agent to synchronize.
export LETTA_AGENT_ID=agent-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx
# Optional: persist for future sessions
echo '{"agentId":"'"$LETTA_AGENT_ID"'"}' > ~/.letta/claude-subconscious/config.json
4. Ensure Required Tags
The sync scripts only act on agents carrying the git-memory-enabled and origin:claude-subconcious tags. Call ensureRequiredAgentTags in scripts/agent_config.ts (lines 155-162) to verify and add these automatically if missing.
5. Run Claude Code
The existing hooks—session_start.ts (line 185), sync_letta_memory.ts, and pretool_sync.ts—will automatically read your custom blocks and inject them into CLAUDE.md or stdout. Because formatMemoryBlocksAsXml processes the agent.blocks array generically, your custom architecture works immediately without code changes.
How the Runtime Handles Custom Architectures
The synchronization pipeline treats memory blocks as opaque data structures. When sync_letta_memory.ts executes, it:
- Fetches the agent via
fetchAgentinconversation_utils.ts - Iterates over
agent.blocksregardless of label names - Formats each block using
formatMemoryBlocksAsXmlinto XML tags like<project_health>...</project_health> - Detects changes via
detectChangedBlocksby comparing current values against the previous state
This architecture means the only file you need to modify is your .af definition. The DEFAULT_AGENT_FILE constant in agent_config.ts (line 25) controls which file gets imported by default, but you can override this per-import as shown above.
Key Files for Custom Memory Architectures
Subconscious.af: The baseline agent definition demonstrating the JSON schema for blocks and tags (lines 24-30)scripts/agent_config.ts: ContainsimportDefaultAgent,ensureRequiredAgentTags, andgetAgentIdfor agent managementscripts/conversation_utils.ts: Defines theMemoryBlockandAgenttypes used throughout the sync pipeline (lines 324-334)scripts/sync_letta_memory.ts: Handles full-mode memory synchronization and change detection (lines 108-119)scripts/pretool_sync.ts: Manages mid-tool-use updates to ensure memory changes appear immediately (lines 84-95)
Summary
- Custom architectures are defined by creating new
.afJSON files with your specific block labels and limits - Import logic resides in
importDefaultAgentwithinscripts/agent_config.ts, which POSTs your definition to Letta - Agent identification works via the
LETTA_AGENT_IDenvironment variable or~/.letta/claude-subconscious/config.json - Required tags (
git-memory-enabledandorigin:claude-subconcious) are enforced byensureRequiredAgentTags - Hook compatibility is automatic—
formatMemoryBlocksAsXmlanddetectChangedBlocksadapt to any block structure without modification
Frequently Asked Questions
How do I add a new memory block to an existing custom agent?
Edit your .af file to include a new block object with a unique label, description, value, and limit, then re-import the agent using importDefaultAgent. The detectChangedBlocks function in scripts/sync_letta_memory.ts will recognize the new block structure on the next synchronization cycle.
Can I use completely different block labels than the default Subconscious agent?
Yes. The formatMemoryBlocksAsXml function in scripts/conversation_utils.ts (lines 442-466) processes blocks dynamically based on their label property. The system imposes no restrictions on label names, provided each block includes the required label and value fields.
What happens if my agent is missing the required tags?
The ensureRequiredAgentTags function in scripts/agent_config.ts (lines 155-162) automatically appends git-memory-enabled and origin:claude-subconcious if they are absent. Without these tags, the sync scripts in sync_letta_memory.ts and pretool_sync.ts will skip the agent, treating it as ineligible for memory synchronization.
Is there a performance cost to having many memory blocks?
There is no hardcoded limit on block count in the synchronization logic. However, the limit field on each block controls token allocation, and the total formatted output from formatMemoryBlocksAsXml must fit within Claude’s available context window when injected into CLAUDE.md. Monitor the compiled XML size if defining numerous high-limit blocks.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →