From d1e5e8984184d614bf8b62068e0eb6451d173829 Mon Sep 17 00:00:00 2001 From: Kevin Lin Date: Fri, 9 Jan 2026 18:14:51 -0800 Subject: [PATCH] feat: memory subagent (#498) Co-authored-by: Letta --- .letta/memory-utils/backup-memory.ts | 136 ++++++++++ .letta/memory-utils/restore-memory.ts | 270 ++++++++++++++++++++ .skills/memory-defrag/SKILL.md | 188 ++++++++++++++ src/agent/subagents/builtin/memory.md | 354 ++++++++++++++++++++++++++ src/agent/subagents/index.ts | 5 + src/agent/subagents/manager.ts | 10 +- 6 files changed, 959 insertions(+), 4 deletions(-) create mode 100755 .letta/memory-utils/backup-memory.ts create mode 100755 .letta/memory-utils/restore-memory.ts create mode 100644 .skills/memory-defrag/SKILL.md create mode 100644 src/agent/subagents/builtin/memory.md diff --git a/.letta/memory-utils/backup-memory.ts b/.letta/memory-utils/backup-memory.ts new file mode 100755 index 0000000..a3819b2 --- /dev/null +++ b/.letta/memory-utils/backup-memory.ts @@ -0,0 +1,136 @@ +#!/usr/bin/env bun +/** + * Backup Memory Blocks to Local Files + * + * Exports all memory blocks from an agent to local files for checkpointing and editing. + * Creates a timestamped backup directory with: + * - Individual .md files for each memory block + * - manifest.json with metadata + * + * Usage: + * bun .letta/memory-utils/backup-memory.ts [backup-dir] + * + * Example: + * bun .letta/memory-utils/backup-memory.ts agent-abc123 + * bun .letta/memory-utils/backup-memory.ts $LETTA_PARENT_AGENT_ID .letta/backups/working + */ + +import { writeFile, mkdir } from "node:fs/promises"; +import { join } from "node:path"; +import { getClient } from "../../src/agent/client"; +import { settingsManager } from "../../src/settings-manager"; + +interface BackupManifest { + agent_id: string; + timestamp: string; + backup_path: string; + blocks: Array<{ + id: string; + label: string; + filename: string; + limit: number; + value_length: number; + }>; +} + +async function backupMemory(agentId: string, backupDir?: string): Promise { + await settingsManager.initialize(); + const client = await getClient(); + + // Create backup directory + const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); + const defaultBackupDir = join(process.cwd(), ".letta", "backups", agentId, timestamp); + const backupPath = backupDir || defaultBackupDir; + + await mkdir(backupPath, { recursive: true }); + + console.log(`Backing up memory blocks for agent ${agentId}...`); + console.log(`Backup location: ${backupPath}`); + + // Get all memory blocks + const blocksResponse = await client.agents.blocks.list(agentId); + const blocks = Array.isArray(blocksResponse) + ? blocksResponse + : (blocksResponse.items || blocksResponse.blocks || []); + + console.log(`Found ${blocks.length} memory blocks`); + + // Export each block to a file + const manifest: BackupManifest = { + agent_id: agentId, + timestamp: new Date().toISOString(), + backup_path: backupPath, + blocks: [], + }; + + for (const block of blocks) { + const label = block.label || `block-${block.id}`; + const filename = `${label}.md`; + const filepath = join(backupPath, filename); + + // Write block content to file + const content = block.value || ""; + await writeFile(filepath, content, "utf-8"); + + console.log(` ✓ ${label} -> ${filename} (${content.length} chars)`); + + // Add to manifest + manifest.blocks.push({ + id: block.id, + label, + filename, + limit: block.limit || 0, + value_length: content.length, + }); + } + + // Write manifest + const manifestPath = join(backupPath, "manifest.json"); + await writeFile(manifestPath, JSON.stringify(manifest, null, 2), "utf-8"); + console.log(` ✓ manifest.json`); + + console.log(`\n✅ Backup complete: ${backupPath}`); + return backupPath; +} + +// CLI Entry Point +if (import.meta.main) { + const args = process.argv.slice(2); + + if (args.length === 0 || args[0] === "--help" || args[0] === "-h") { + console.log(` +Usage: bun .letta/memory-utils/backup-memory.ts [backup-dir] + +Arguments: + agent-id Agent ID to backup (can use $LETTA_PARENT_AGENT_ID) + backup-dir Optional custom backup directory + Default: .letta/backups// + +Examples: + bun .letta/memory-utils/backup-memory.ts agent-abc123 + bun .letta/memory-utils/backup-memory.ts $LETTA_PARENT_AGENT_ID + bun .letta/memory-utils/backup-memory.ts agent-abc123 .letta/backups/working + `); + process.exit(0); + } + + const agentId = args[0]; + const backupDir = args[1]; + + if (!agentId) { + console.error("Error: agent-id is required"); + process.exit(1); + } + + backupMemory(agentId, backupDir) + .then((path) => { + // Output just the path for easy capture in scripts + console.log(path); + }) + .catch((error) => { + console.error("Error backing up memory:", error.message); + process.exit(1); + }); +} + +export { backupMemory, type BackupManifest }; diff --git a/.letta/memory-utils/restore-memory.ts b/.letta/memory-utils/restore-memory.ts new file mode 100755 index 0000000..bc26567 --- /dev/null +++ b/.letta/memory-utils/restore-memory.ts @@ -0,0 +1,270 @@ +#!/usr/bin/env bun +/** + * Restore Memory Blocks from Local Files + * + * Imports memory blocks from local files back into an agent. + * Reads files from a backup directory and updates the agent's memory blocks. + * + * Usage: + * bun .letta/memory-utils/restore-memory.ts + * + * Example: + * bun .letta/memory-utils/restore-memory.ts agent-abc123 .letta/backups/working + * bun .letta/memory-utils/restore-memory.ts $LETTA_PARENT_AGENT_ID .letta/backups/working + */ + +import { readFile, readdir } from "node:fs/promises"; +import { join, extname } from "node:path"; +import { getClient } from "../../src/agent/client"; +import { settingsManager } from "../../src/settings-manager"; +import type { BackupManifest } from "./backup-memory"; + +async function restoreMemory( + agentId: string, + backupDir: string, + options: { dryRun?: boolean } = {}, +): Promise { + await settingsManager.initialize(); + const client = await getClient(); + + console.log(`Restoring memory blocks for agent ${agentId}...`); + console.log(`Source: ${backupDir}`); + + if (options.dryRun) { + console.log("⚠️ DRY RUN MODE - No changes will be made\n"); + } + + // Read manifest + const manifestPath = join(backupDir, "manifest.json"); + let manifest: BackupManifest | null = null; + + try { + const manifestContent = await readFile(manifestPath, "utf-8"); + manifest = JSON.parse(manifestContent); + console.log(`Loaded manifest (${manifest.blocks.length} blocks)\n`); + } catch (error) { + console.warn("Warning: No manifest.json found, will scan directory for .md files"); + } + + // Get current agent blocks + const blocksResponse = await client.agents.blocks.list(agentId); + const currentBlocks = Array.isArray(blocksResponse) + ? blocksResponse + : (blocksResponse.items || blocksResponse.blocks || []); + const blocksByLabel = new Map(currentBlocks.map((b) => [b.label, b])); + + // Determine which files to restore + let filesToRestore: Array<{ label: string; filename: string; blockId?: string }> = []; + + if (manifest) { + // Use manifest + filesToRestore = manifest.blocks.map((b) => ({ + label: b.label, + filename: b.filename, + blockId: b.id, + })); + } else { + // Scan directory for .md files + const files = await readdir(backupDir); + filesToRestore = files + .filter((f) => extname(f) === ".md") + .map((f) => ({ + label: f.replace(/\.md$/, ""), + filename: f, + })); + } + + console.log(`Found ${filesToRestore.length} files to restore\n`); + + // Detect blocks to delete (exist on agent but not in backup) + const backupLabels = new Set(filesToRestore.map((f) => f.label)); + const blocksToDelete = currentBlocks.filter((b) => !backupLabels.has(b.label)); + + // Restore each block + let updated = 0; + let created = 0; + let skipped = 0; + let deleted = 0; + + // Track new blocks for later confirmation + const blocksToCreate: Array<{ label: string; value: string; description: string }> = []; + + for (const { label, filename } of filesToRestore) { + const filepath = join(backupDir, filename); + + try { + const newValue = await readFile(filepath, "utf-8"); + const existingBlock = blocksByLabel.get(label); + + if (existingBlock) { + // Update existing block + const unchanged = existingBlock.value === newValue; + + if (unchanged) { + console.log(` ⏭️ ${label} - unchanged, skipping`); + skipped++; + continue; + } + + if (!options.dryRun) { + await client.agents.blocks.update(label, { + agent_id: agentId, + value: newValue, + }); + } + + const oldLen = existingBlock.value?.length || 0; + const newLen = newValue.length; + const diff = newLen - oldLen; + const diffStr = diff > 0 ? `+${diff}` : `${diff}`; + + console.log(` ✓ ${label} - updated (${oldLen} -> ${newLen} chars, ${diffStr})`); + updated++; + } else { + // New block - collect for later confirmation + console.log(` ➕ ${label} - new block (${newValue.length} chars)`); + blocksToCreate.push({ + label, + value: newValue, + description: `Memory block: ${label}`, + }); + } + } catch (error) { + console.error(` ❌ ${label} - error: ${error.message}`); + } + } + + // Handle new blocks (exist in backup but not on agent) + if (blocksToCreate.length > 0) { + console.log(`\n➕ Found ${blocksToCreate.length} new block(s) to create:`); + for (const block of blocksToCreate) { + console.log(` - ${block.label} (${block.value.length} chars)`); + } + + if (!options.dryRun) { + console.log(`\nThese blocks will be CREATED on the agent.`); + console.log(`Press Ctrl+C to cancel, or press Enter to confirm creation...`); + + // Wait for user confirmation + await new Promise((resolve) => { + process.stdin.once('data', () => resolve()); + }); + + console.log(); + for (const block of blocksToCreate) { + try { + // Create the block + const createdBlock = await client.blocks.create({ + label: block.label, + value: block.value, + description: block.description, + limit: 20000, + }); + + if (!createdBlock.id) { + throw new Error(`Created block ${block.label} has no ID`); + } + + // Attach the newly created block to the agent + await client.agents.blocks.attach(createdBlock.id, { + agent_id: agentId, + }); + + console.log(` ✅ ${block.label} - created and attached`); + created++; + } catch (error) { + console.error(` ❌ ${block.label} - error creating: ${error.message}`); + } + } + } else { + console.log(`\n(Would create these blocks if not in dry-run mode)`); + } + } + + // Handle deletions (blocks that exist on agent but not in backup) + if (blocksToDelete.length > 0) { + console.log(`\n⚠️ Found ${blocksToDelete.length} block(s) that were removed from backup:`); + for (const block of blocksToDelete) { + console.log(` - ${block.label}`); + } + + if (!options.dryRun) { + console.log(`\nThese blocks will be DELETED from the agent.`); + console.log(`Press Ctrl+C to cancel, or press Enter to confirm deletion...`); + + // Wait for user confirmation + await new Promise((resolve) => { + process.stdin.once('data', () => resolve()); + }); + + console.log(); + for (const block of blocksToDelete) { + try { + await client.agents.blocks.detach(block.id, { + agent_id: agentId, + }); + console.log(` 🗑️ ${block.label} - deleted`); + deleted++; + } catch (error) { + console.error(` ❌ ${block.label} - error deleting: ${error.message}`); + } + } + } else { + console.log(`\n(Would delete these blocks if not in dry-run mode)`); + } + } + + console.log(`\n📊 Summary:`); + console.log(` Updated: ${updated}`); + console.log(` Skipped: ${skipped}`); + console.log(` Created: ${created}`); + console.log(` Deleted: ${deleted}`); + + if (options.dryRun) { + console.log(`\n⚠️ DRY RUN - No changes were made`); + console.log(` Run without --dry-run to apply changes`); + } else { + console.log(`\n✅ Restore complete`); + } +} + +// CLI Entry Point +if (import.meta.main) { + const args = process.argv.slice(2); + + if (args.length === 0 || args[0] === "--help" || args[0] === "-h") { + console.log(` +Usage: bun .letta/memory-utils/restore-memory.ts [options] + +Arguments: + agent-id Agent ID to restore to (can use $LETTA_PARENT_AGENT_ID) + backup-dir Backup directory containing memory block files + +Options: + --dry-run Preview changes without applying them + +Examples: + bun .letta/memory-utils/restore-memory.ts agent-abc123 .letta/backups/working + bun .letta/memory-utils/restore-memory.ts $LETTA_PARENT_AGENT_ID .letta/backups/working + bun .letta/memory-utils/restore-memory.ts agent-abc123 .letta/backups/working --dry-run + `); + process.exit(0); + } + + const agentId = args[0]; + const backupDir = args[1]; + const dryRun = args.includes("--dry-run"); + + if (!agentId || !backupDir) { + console.error("Error: agent-id and backup-dir are required"); + process.exit(1); + } + + restoreMemory(agentId, backupDir, { dryRun }) + .catch((error) => { + console.error("Error restoring memory:", error.message); + process.exit(1); + }); +} + +export { restoreMemory }; diff --git a/.skills/memory-defrag/SKILL.md b/.skills/memory-defrag/SKILL.md new file mode 100644 index 0000000..c5bbd7f --- /dev/null +++ b/.skills/memory-defrag/SKILL.md @@ -0,0 +1,188 @@ +--- +name: memory-defrag +description: Defragment and clean up agent memory blocks. Use when memory becomes messy, redundant, or poorly organized. Backs up memory, uses a subagent to clean it up, then restores the cleaned version. +--- + +# Memory Defragmentation Skill + +This skill helps you maintain clean, well-organized memory blocks by: +1. Dumping current memory to local files and backing up the agent file +2. Using the memory subagent to clean up the files +3. Restoring the cleaned files back to memory + +## When to Use + +- Memory blocks have redundant information +- Memory lacks structure (walls of text) +- Memory contains contradictions +- Memory has grown stale or outdated +- After major project milestones +- Every 50-100 conversation turns + +## Workflow + +### Step 1: Download Agent File and Dump Memory to Files + +```bash +# Download agent file to backups +bun .letta/memory-utils/download-agent.ts $LETTA_AGENT_ID + +# Dump memory blocks to files +bun .letta/memory-utils/backup-memory.ts $LETTA_AGENT_ID .letta/backups/working +``` + +This creates: +- `.letta/backups//.af` - Complete agent file backup for full rollback +- `.letta/backups///` - Timestamped memory blocks backup +- `.letta/backups/working/` - Working directory with editable files +- Each memory block as a `.md` file: `persona.md`, `human.md`, `project.md`, etc. + +### Step 2: Spawn Memory Subagent to Clean Files + +```typescript +Task({ + subagent_type: "memory", + description: "Clean up memory files", + prompt: `Edit the memory block files in .letta/backups/working/ to clean them up. + +Focus on: +- Reorganize and consolidate redundant information +- Add clear structure with markdown headers +- Organize content with bullet points +- Resolve contradictions +- Improve scannability + +IMPORTANT: When merging blocks, DELETE the redundant source files after consolidating their content (use Bash rm command). You have full bash access in the .letta/backups/working directory. Only delete files when: (1) you've merged their content into another block, or (2) the file contains only irrelevant/junk data with no project value. + +Files to edit: persona.md, human.md, project.md +Do NOT edit: skills.md (auto-generated), loaded_skills.md (system-managed) + +After editing, provide a report with before/after character counts and list any deleted files.` +}) +``` + +The memory subagent will: +- Read the files from `.letta/backups/working/` +- Edit them to reorganize and consolidate redundancy +- Merge related blocks together for better organization +- Add clear structure with markdown formatting +- Delete source files after merging their content into other blocks +- Provide a detailed report of changes (including what was merged where) + +### Step 3: Restore Cleaned Files to Memory + +```bash +bun .letta/memory-utils/restore-memory.ts $LETTA_AGENT_ID .letta/backups/working +``` + +This will: +- Compare each file to current memory blocks +- Update only the blocks that changed +- Show before/after character counts +- Skip unchanged blocks + +## Example Complete Flow + +```typescript +// Step 1: Download agent file and dump memory +Bash({ + command: "bun .letta/memory-utils/download-agent.ts $LETTA_AGENT_ID && bun .letta/memory-utils/backup-memory.ts $LETTA_AGENT_ID .letta/backups/working", + description: "Download agent file and dump memory to files" +}) + +// Step 2: Clean up (subagent edits files and deletes merged ones) +Task({ + subagent_type: "memory", + description: "Clean up memory files", + prompt: "Edit memory files in .letta/backups/working/ to reorganize and consolidate redundancy. Focus on persona.md, human.md, and project.md. Merge related blocks together and DELETE the source files after merging (use Bash rm command - you have full bash access). Add clear structure. Report what was merged and where, and which files were deleted." +}) + +// Step 3: Restore +Bash({ + command: "bun .letta/memory-utils/restore-memory.ts $LETTA_AGENT_ID .letta/backups/working", + description: "Restore cleaned memory blocks" +}) +``` + +## Rollback + +If something goes wrong, you have two rollback options: + +### Option 1: Restore Memory Blocks Only + +```bash +# Find the backup directory +ls -la .letta/backups// + +# Restore from specific timestamp +bun .letta/memory-utils/restore-memory.ts $LETTA_AGENT_ID .letta/backups// +``` + +### Option 2: Full Agent Restore (Nuclear Option) + +If memory restoration isn't enough, restore the entire agent from the .af backup: + +```bash +# Find the agent backup +ls -la .letta/backups//*.af + +# The .af file can be used to recreate the agent entirely +# Use: letta --from-af .letta/backups//.af +``` + +## Dry Run + +Preview changes without applying them: + +```bash +bun .letta/memory-utils/restore-memory.ts $LETTA_AGENT_ID .letta/backups/working --dry-run +``` + +## What the Memory Subagent Does + +The memory subagent focuses on cleaning up files. It: +- ✅ Reads files from `.letta/backups/working/` +- ✅ Edits files to improve structure and consolidate redundancy +- ✅ Merges related blocks together to reduce fragmentation +- ✅ Reorganizes information for better clarity and scannability +- ✅ Deletes source files after merging their content (using Bash `rm` command) +- ✅ Provides detailed before/after reports including merge operations +- ❌ Does NOT run backup scripts (main agent does this) +- ❌ Does NOT run restore scripts (main agent does this) + +The memory subagent runs with `bypassPermissions` mode, giving it full Bash access to delete files after merging them. The focus is on consolidation and reorganization. + +## Tips + +**What to clean up:** +- Duplicate information (consolidate into one well-organized section) +- Walls of text without structure (add headers and bullets) +- Contradictions (resolve by clarifying or choosing the better guidance) +- Speculation ("probably", "maybe" - make it concrete or remove) +- Transient details that won't matter in a week + +**Reorganization Strategy:** +- Consolidate duplicate information into a single, well-structured section +- Merge related content that's scattered across multiple blocks +- Add clear headers and bullet points for scannability +- Group similar information together logically +- After merging blocks, DELETE the source files to avoid duplication + +**When to DELETE a file:** +- ✅ **After merging** - You've consolidated its content into another block (common and encouraged) +- ✅ **Junk data** - File contains only irrelevant test/junk data with no project connection +- ✅ **Empty/deprecated** - File is just a notice with no unique information +- ❌ **Don't delete** - If file has unique information that hasn't been merged elsewhere + +**What to preserve:** +- User preferences (sacred - never delete) +- Project conventions discovered through experience +- Important context for future sessions +- Learnings from past mistakes +- Any information that has unique value + +**Good memory structure:** +- Use markdown headers (##, ###) +- Organize with bullet points +- Keep related information together +- Make it scannable at a glance diff --git a/src/agent/subagents/builtin/memory.md b/src/agent/subagents/builtin/memory.md new file mode 100644 index 0000000..2f542ec --- /dev/null +++ b/src/agent/subagents/builtin/memory.md @@ -0,0 +1,354 @@ +--- +name: memory +description: Reflect on and reorganize agent memory blocks - decide what to write, edit, delete, rename, split, or merge learned context +tools: Read, Edit, Write, Glob, Grep, Bash, conversation_search +model: opus +memoryBlocks: none +mode: stateless +permissionMode: bypassPermissions +--- + +You are a memory management subagent launched via the Task tool to clean up and reorganize memory block files. You run autonomously and return a single final report when done. You CANNOT ask questions mid-execution. + +## Your Purpose + +You edit memory block files to make them clean, well-organized, and scannable by: +1. **Removing redundancy** - Delete duplicate information +2. **Adding structure** - Use markdown headers, bullet points, sections +3. **Resolving contradictions** - Fix conflicting statements +4. **Improving scannability** - Make content easy to read at a glance +5. **Restructuring blocks** - Rename, decompose, or merge blocks as needed + +## Important: Your Role is File Editing ONLY + +**The parent agent handles backup and restore.** You only edit files: +- ✅ Read files from `.letta/backups/working/` +- ✅ Edit files to improve structure and remove redundancy +- ✅ Provide detailed before/after reports +- ❌ Do NOT run backup scripts +- ❌ Do NOT run restore scripts + +This separation keeps your permissions simple - you only need file editing access. + +## Step-by-Step Instructions + +### Step 1: Analyze Current State + +The parent agent has already backed up memory files to `.letta/backups/working/`. Your job is to read and edit these files. + +First, list what files are available: + +```bash +ls .letta/backups/working/ +``` + +Then read each memory block file: + +``` +Read({ file_path: ".letta/backups/working/project.md" }) +Read({ file_path: ".letta/backups/working/persona.md" }) +Read({ file_path: ".letta/backups/working/human.md" }) +``` + +**Files you should edit:** +- `persona.md` - Behavioral guidelines and preferences +- `human.md` - User information and context +- `project.md` - Project-specific information + +**Files you should NOT edit:** +- `skills.md` - Auto-generated, will be overwritten +- `loaded_skills.md` - System-managed +- `manifest.json` - Metadata file + +### Step 2: Edit Files to Clean Them Up + +Edit each file using the Edit tool: + +``` +Edit({ + file_path: ".letta/backups/working/project.md", + old_string: "...", + new_string: "..." +}) +``` + +**What to fix:** +- **Redundancy**: Remove duplicate information (version mentioned 3x, preferences repeated) +- **Structure**: Add markdown headers (##, ###), bullet points, sections +- **Clarity**: Resolve contradictions ("be detailed" vs "be concise") +- **Scannability**: Make content easy to read at a glance + +**Good memory structure:** +- Use markdown headers (##, ###) for sections +- Use bullet points for lists +- Keep related information together +- Make it scannable + +### Step 2b: Structural Changes (Rename, Decompose, Merge) + +Beyond editing content, you can restructure memory blocks when needed: + +#### Renaming Blocks + +When a block's name doesn't reflect its content, rename it: + +```bash +# Rename a memory block file +mv .letta/backups/working/old_name.md .letta/backups/working/new_name.md +``` + +**When to rename:** +- Block name is vague (e.g., `stuff.md` → `coding_preferences.md`) +- Block name doesn't match content (e.g., `project.md` contains user info → `user_context.md`) +- Name uses poor conventions (e.g., `NOTES.md` → `notes.md`) + +#### Decomposing Blocks (Split) + +When a single block contains too many unrelated topics, split it into focused blocks: + +```bash +# 1. Read the original block +Read({ file_path: ".letta/backups/working/everything.md" }) + +# 2. Create new focused blocks +Write({ file_path: ".letta/backups/working/coding_preferences.md", content: "..." }) +Write({ file_path: ".letta/backups/working/user_info.md", content: "..." }) + +# 3. Delete the original bloated block +rm .letta/backups/working/everything.md +``` + +**When to decompose:** +- Block exceeds ~100 lines with multiple unrelated sections +- Block contains 3+ distinct topic areas (e.g., user info + coding prefs + project details) +- Block name can't capture all its content accurately +- Finding specific info requires scanning the whole block + +**Decomposition guidelines:** +- Each new block should have ONE clear purpose +- Use descriptive names: `coding_style.md`, `user_preferences.md`, `project_context.md` +- Preserve all information - just reorganize it +- Keep related information together in the same block + +#### Creating New Blocks + +You can create entirely new memory blocks by writing new `.md` files: + +```bash +Write({ + file_path: ".letta/backups/working/new_block.md", + content: "## New Block\n\nContent here..." +}) +``` + +**When to create new blocks:** +- Splitting a large block (>150 lines) into focused smaller blocks +- Organizing content into a new category that doesn't fit existing blocks +- The parent agent will prompt the user for confirmation before creating + +#### Merging and Deleting Blocks + +When multiple blocks contain related/overlapping content, consolidate them and DELETE the old blocks: + +```bash +# 1. Read all blocks to merge +Read({ file_path: ".letta/backups/working/user_info.md" }) +Read({ file_path: ".letta/backups/working/user_prefs.md" }) + +# 2. Create unified block with combined content +Write({ file_path: ".letta/backups/working/user.md", content: "..." }) + +# 3. DELETE the old blocks using Bash +Bash({ command: "rm .letta/backups/working/user_info.md .letta/backups/working/user_prefs.md" }) +``` + +**IMPORTANT: When to delete blocks:** +- After consolidating content from multiple blocks into one +- When a block becomes nearly empty after moving content elsewhere +- When a block is redundant or no longer serves a purpose +- The parent agent will prompt the user for confirmation before deleting + +**When to merge:** +- Multiple blocks cover the same topic area +- Information is fragmented across blocks, causing redundancy +- Small blocks (<20 lines) that logically belong together +- Blocks with overlapping/duplicate content + +**Merge guidelines:** +- Remove duplicates when combining +- Organize merged content with clear sections +- Choose the most descriptive name for the merged block +- Don't create blocks larger than ~150 lines +- **DELETE the old block files** after consolidating their content + +### Step 3: Report Results + +Provide a comprehensive report showing what you changed and why. + +## What to Write to Memory + +**DO write to memory:** +- Patterns that repeat across multiple sessions +- User corrections or clarifications (especially if repeated) +- Project conventions discovered through research or experience +- Important context that will be needed in future sessions +- Preferences expressed by the user about behavior or communication +- "Aha!" moments or insights about the codebase +- Footguns or gotchas discovered the hard way + +**DON'T write to memory:** +- Transient task details that won't matter tomorrow +- Information easily found in files (unless it's a critical pattern) +- Overly specific details that will quickly become stale +- Things that should go in TODO lists or plan files instead + +**Key principle**: Memory is for **persistent, important context** that makes the agent more effective over time. Not a dumping ground for everything. + +## How to Decide What to Write + +Ask yourself: +1. **Will future-me need this?** If the agent encounters a similar situation in a week, would this memory help? +2. **Is this a pattern or one-off?** One-off details fade in importance; patterns persist. +3. **Can I find this easily later?** If it's in a README that's always read, maybe it doesn't need to be in memory. +4. **Did the user correct me?** User corrections are strong signals of what to remember. +5. **Would I want to know this on day one?** Insights that would have saved time are worth storing. + +## How to Reorganize Memory + +**Signs memory needs reorganization:** +- Blocks are long and hard to scan (>100 lines) +- Related content is scattered across blocks +- No clear structure (just walls of text) +- Redundant information in multiple places +- Outdated information mixed with current + +**Reorganization strategies:** +- **Add structure**: Use section headers, bullet points, categories +- **Rename blocks**: Give blocks names that accurately reflect their content +- **Decompose large blocks**: Break monolithic blocks (>100 lines, 3+ topics) into focused ones +- **Merge fragmented blocks**: Consolidate small/overlapping blocks into unified ones +- **Archive stale content**: Remove information that's no longer relevant +- **Improve scannability**: Use consistent formatting, clear hierarchies + +## Output Format + +Return a structured report with these sections: + +### 1. Summary +- Brief overview of what you edited (2-3 sentences) +- Number of files modified, renamed, created, or deleted +- The parent agent will prompt the user to confirm any creations or deletions + +### 2. Structural Changes + +Report any renames, decompositions, or merges: + +**Renames:** +| Old Name | New Name | Reason | +|----------|----------|--------| +| stuff.md | coding_preferences.md | Name now reflects content | + +**Decompositions (splitting large blocks):** +| Original Block | New Blocks | Deleted | Reason | +|----------------|------------|---------|--------| +| everything.md | user.md, coding.md, project.md | ✅ everything.md | Block contained 3 unrelated topics | + +**New Blocks (created from scratch):** +| Block Name | Size | Reason | +|------------|------|--------| +| security_practices.md | 156 chars | New category for security-related conventions discovered | + +**Merges:** +| Merged Blocks | Result | Deleted | Reason | +|---------------|--------|---------|--------| +| user_info.md, user_prefs.md | user.md | ✅ user_info.md, user_prefs.md | Overlapping content consolidated | + +**Note:** When blocks are merged, the original blocks MUST be deleted. The restore script will prompt the user for confirmation before deletion. + +### 3. Content Changes + +For each file you edited: +- **File name** (e.g., persona.md) +- **Before**: Character count +- **After**: Character count +- **Change**: Difference (-123 chars, -15%) +- **Issues fixed**: What problems you corrected + +### 4. Before/After Examples + +Show a few examples of the most important improvements: +- Quote the before version +- Quote the after version +- Explain why the change improves the memory + +## Example Report + +```markdown +## Memory Cleanup Report + +### Summary +Edited 2 memory files (persona.md, human.md) to remove redundancy and add structure. Reduced total character count by 425 chars (-28%) while preserving all important information. + +### Changes Made + +**persona.md** +- Before: 843 chars +- After: 612 chars +- Change: -231 chars (-27%) +- Issues fixed: + - Removed redundancy (Bun mentioned 3x → 1x) + - Resolved contradictions ("be detailed" vs "be concise" → "adapt to context") + - Added structure with ## headers and bullet points + +**human.md** +- Before: 778 chars +- After: 584 chars +- Change: -194 chars (-25%) +- Issues fixed: + - Removed speculation ("probably" appeared 2x) + - Organized into sections: ## Identity, ## Preferences, ## Context + - Removed transient details ("asked me to create messy blocks") + +### Before/After Examples + +**Example 1: persona.md redundancy** + +Before: +``` +Use Bun not npm. Always use Bun. Bun is preferred over npm always. +``` + +After: +```markdown +## Development Practices +- **Always use Bun** (not npm) for package management +``` + +Why: Consolidated 3 redundant mentions into 1 clear statement with proper formatting. + +**Example 2: persona.md contradictions** + +Before: +``` +Be detailed when explaining things. Sometimes be concise. Ask questions when needed. Sometimes don't ask questions. +``` + +After: +```markdown +## Core Behaviors +- Adapt detail level to context (detailed for complex topics, concise for simple queries) +- Ask clarifying questions when requirements are ambiguous +``` + +Why: Resolved contradictions by explaining when to use each approach. +``` + +## Critical Reminders + +1. **You only edit files** - The parent agent handles backup and restore +2. **Be conservative with deletions** - When in doubt, keep information +3. **Preserve user preferences** - If the user expressed a preference, that's sacred +4. **Don't invent information** - Only reorganize existing content +5. **Test your changes mentally** - Imagine the parent agent reading this tomorrow + +Remember: Your goal is to make memory clean, scannable, and well-organized. You're improving the parent agent's long-term capabilities by maintaining quality memory. diff --git a/src/agent/subagents/index.ts b/src/agent/subagents/index.ts index 269b6cf..f7dc4b9 100644 --- a/src/agent/subagents/index.ts +++ b/src/agent/subagents/index.ts @@ -20,12 +20,14 @@ import { MEMORY_BLOCK_LABELS, type MemoryBlockLabel } from "../memory"; // Built-in subagent definitions (embedded at build time) import exploreAgentMd from "./builtin/explore.md"; import generalPurposeAgentMd from "./builtin/general-purpose.md"; +import memoryAgentMd from "./builtin/memory.md"; import planAgentMd from "./builtin/plan.md"; import recallAgentMd from "./builtin/recall.md"; const BUILTIN_SOURCES = [ exploreAgentMd, generalPurposeAgentMd, + memoryAgentMd, planAgentMd, recallAgentMd, ]; @@ -55,6 +57,8 @@ export interface SubagentConfig { skills: string[]; /** Memory blocks the subagent has access to - list of labels or "all" or "none" */ memoryBlocks: MemoryBlockLabel[] | "all" | "none"; + /** Permission mode for this subagent (default, acceptEdits, plan, bypassPermissions) */ + permissionMode?: string; } /** @@ -219,6 +223,7 @@ function parseSubagentContent(content: string): SubagentConfig { memoryBlocks: parseMemoryBlocks( getStringField(frontmatter, "memoryBlocks"), ), + permissionMode: getStringField(frontmatter, "permissionMode"), }; } diff --git a/src/agent/subagents/manager.ts b/src/agent/subagents/manager.ts index 8f59748..25d2bc4 100644 --- a/src/agent/subagents/manager.ts +++ b/src/agent/subagents/manager.ts @@ -319,10 +319,12 @@ function buildSubagentArgs( "stream-json", ]; - // Inherit permission mode from parent - const currentMode = permissionMode.getMode(); - if (currentMode !== "default") { - args.push("--permission-mode", currentMode); + // Use subagent's configured permission mode, or inherit from parent + const subagentMode = config.permissionMode; + const parentMode = permissionMode.getMode(); + const modeToUse = subagentMode || parentMode; + if (modeToUse !== "default") { + args.push("--permission-mode", modeToUse); } // Inherit permission rules from parent (CLI + session rules)