# How to Set Up a Custom Letta Agent with a Different Memory Block Architecture

> Learn to set up a custom Letta agent with a unique memory block architecture. Define new block labels and import them easily without modifying hooks.

- Repository: [Letta/claude-subconscious](https://github.com/letta-ai/claude-subconscious)
- Tags: how-to-guide
- Published: 2026-03-26

---

**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`](https://github.com/letta-ai/claude-subconscious/blob/main/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`](https://github.com/letta-ai/claude-subconscious/blob/main/scripts/conversation_utils.ts) (lines 441-447) loads whatever blocks exist, `formatMemoryBlocksAsXml` (lines 442-466) converts them to XML for injection into [`CLAUDE.md`](https://github.com/letta-ai/claude-subconscious/blob/main/CLAUDE.md), and `detectChangedBlocks` in [`scripts/sync_letta_memory.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/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.

```json
{
  "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`](https://github.com/letta-ai/claude-subconscious/blob/main/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.

```typescript
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_ID` environment variable, **or**
- Saving it to `~/.letta/claude-subconscious/config.json`

The validation logic in [`agent_config.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/agent_config.ts) (lines 33-42) checks both locations when resolving which agent to synchronize.

```bash
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`](https://github.com/letta-ai/claude-subconscious/blob/main/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`](https://github.com/letta-ai/claude-subconscious/blob/main/session_start.ts) (line 185), [`sync_letta_memory.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/sync_letta_memory.ts), and [`pretool_sync.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/pretool_sync.ts)—will automatically read your custom blocks and inject them into [`CLAUDE.md`](https://github.com/letta-ai/claude-subconscious/blob/main/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`](https://github.com/letta-ai/claude-subconscious/blob/main/sync_letta_memory.ts) executes, it:
1. Fetches the agent via `fetchAgent` in [`conversation_utils.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/conversation_utils.ts)
2. Iterates over `agent.blocks` regardless of label names
3. Formats each block using `formatMemoryBlocksAsXml` into XML tags like `<project_health>...</project_health>`
4. Detects changes via `detectChangedBlocks` by 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`](https://github.com/letta-ai/claude-subconscious/blob/main/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`](https://github.com/letta-ai/claude-subconscious/blob/main/scripts/agent_config.ts)**: Contains `importDefaultAgent`, `ensureRequiredAgentTags`, and `getAgentId` for agent management
- **[`scripts/conversation_utils.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/scripts/conversation_utils.ts)**: Defines the `MemoryBlock` and `Agent` types used throughout the sync pipeline (lines 324-334)
- **[`scripts/sync_letta_memory.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/scripts/sync_letta_memory.ts)**: Handles full-mode memory synchronization and change detection (lines 108-119)
- **[`scripts/pretool_sync.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/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 `.af` JSON files with your specific block labels and limits
- **Import logic** resides in `importDefaultAgent` within [`scripts/agent_config.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/scripts/agent_config.ts), which POSTs your definition to Letta
- **Agent identification** works via the `LETTA_AGENT_ID` environment variable or `~/.letta/claude-subconscious/config.json`
- **Required tags** (`git-memory-enabled` and `origin:claude-subconcious`) are enforced by `ensureRequiredAgentTags`
- **Hook compatibility** is automatic—`formatMemoryBlocksAsXml` and `detectChangedBlocks` adapt 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`](https://github.com/letta-ai/claude-subconscious/blob/main/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`](https://github.com/letta-ai/claude-subconscious/blob/main/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`](https://github.com/letta-ai/claude-subconscious/blob/main/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`](https://github.com/letta-ai/claude-subconscious/blob/main/sync_letta_memory.ts) and [`pretool_sync.ts`](https://github.com/letta-ai/claude-subconscious/blob/main/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`](https://github.com/letta-ai/claude-subconscious/blob/main/CLAUDE.md). Monitor the compiled XML size if defining numerous high-limit blocks.