fix: memory defrag skill location (#518)
Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
@@ -1,136 +0,0 @@
|
|||||||
#!/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 <agent-id> [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<string> {
|
|
||||||
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 <agent-id> [backup-dir]
|
|
||||||
|
|
||||||
Arguments:
|
|
||||||
agent-id Agent ID to backup (can use $LETTA_PARENT_AGENT_ID)
|
|
||||||
backup-dir Optional custom backup directory
|
|
||||||
Default: .letta/backups/<agent-id>/<timestamp>
|
|
||||||
|
|
||||||
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 };
|
|
||||||
@@ -21,18 +21,13 @@ This skill helps you maintain clean, well-organized memory blocks by:
|
|||||||
|
|
||||||
## Workflow
|
## Workflow
|
||||||
|
|
||||||
### Step 1: Download Agent File and Dump Memory to Files
|
### Step 1: Backup Memory to Files
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Download agent file to backups
|
npx tsx <SKILL_DIR>/scripts/backup-memory.ts $LETTA_AGENT_ID .letta/backups/working
|
||||||
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:
|
This creates:
|
||||||
- `.letta/backups/<agent-id>/<timestamp>.af` - Complete agent file backup for full rollback
|
|
||||||
- `.letta/backups/<agent-id>/<timestamp>/` - Timestamped memory blocks backup
|
- `.letta/backups/<agent-id>/<timestamp>/` - Timestamped memory blocks backup
|
||||||
- `.letta/backups/working/` - Working directory with editable files
|
- `.letta/backups/working/` - Working directory with editable files
|
||||||
- Each memory block as a `.md` file: `persona.md`, `human.md`, `project.md`, etc.
|
- Each memory block as a `.md` file: `persona.md`, `human.md`, `project.md`, etc.
|
||||||
@@ -72,7 +67,7 @@ The memory subagent will:
|
|||||||
### Step 3: Restore Cleaned Files to Memory
|
### Step 3: Restore Cleaned Files to Memory
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun .letta/memory-utils/restore-memory.ts $LETTA_AGENT_ID .letta/backups/working
|
npx tsx <SKILL_DIR>/scripts/restore-memory.ts $LETTA_AGENT_ID .letta/backups/working
|
||||||
```
|
```
|
||||||
|
|
||||||
This will:
|
This will:
|
||||||
@@ -84,10 +79,10 @@ This will:
|
|||||||
## Example Complete Flow
|
## Example Complete Flow
|
||||||
|
|
||||||
```typescript
|
```typescript
|
||||||
// Step 1: Download agent file and dump memory
|
// Step 1: Backup memory to files
|
||||||
Bash({
|
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",
|
command: "npx tsx <SKILL_DIR>/scripts/backup-memory.ts $LETTA_AGENT_ID .letta/backups/working",
|
||||||
description: "Download agent file and dump memory to files"
|
description: "Backup memory to files"
|
||||||
})
|
})
|
||||||
|
|
||||||
// Step 2: Clean up (subagent edits files and deletes merged ones)
|
// Step 2: Clean up (subagent edits files and deletes merged ones)
|
||||||
@@ -99,35 +94,21 @@ Task({
|
|||||||
|
|
||||||
// Step 3: Restore
|
// Step 3: Restore
|
||||||
Bash({
|
Bash({
|
||||||
command: "bun .letta/memory-utils/restore-memory.ts $LETTA_AGENT_ID .letta/backups/working",
|
command: "npx tsx <SKILL_DIR>/scripts/restore-memory.ts $LETTA_AGENT_ID .letta/backups/working",
|
||||||
description: "Restore cleaned memory blocks"
|
description: "Restore cleaned memory blocks"
|
||||||
})
|
})
|
||||||
```
|
```
|
||||||
|
|
||||||
## Rollback
|
## Rollback
|
||||||
|
|
||||||
If something goes wrong, you have two rollback options:
|
If something goes wrong, restore from a previous backup:
|
||||||
|
|
||||||
### Option 1: Restore Memory Blocks Only
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Find the backup directory
|
# Find the backup directory
|
||||||
ls -la .letta/backups/<agent-id>/
|
ls -la .letta/backups/<agent-id>/
|
||||||
|
|
||||||
# Restore from specific timestamp
|
# Restore from specific timestamp
|
||||||
bun .letta/memory-utils/restore-memory.ts $LETTA_AGENT_ID .letta/backups/<agent-id>/<timestamp>
|
npx tsx <SKILL_DIR>/scripts/restore-memory.ts $LETTA_AGENT_ID .letta/backups/<agent-id>/<timestamp>
|
||||||
```
|
|
||||||
|
|
||||||
### 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/<agent-id>/*.af
|
|
||||||
|
|
||||||
# The .af file can be used to recreate the agent entirely
|
|
||||||
# Use: letta --from-af .letta/backups/<agent-id>/<timestamp>.af
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Dry Run
|
## Dry Run
|
||||||
@@ -135,20 +116,20 @@ ls -la .letta/backups/<agent-id>/*.af
|
|||||||
Preview changes without applying them:
|
Preview changes without applying them:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
bun .letta/memory-utils/restore-memory.ts $LETTA_AGENT_ID .letta/backups/working --dry-run
|
npx tsx <SKILL_DIR>/scripts/restore-memory.ts $LETTA_AGENT_ID .letta/backups/working --dry-run
|
||||||
```
|
```
|
||||||
|
|
||||||
## What the Memory Subagent Does
|
## What the Memory Subagent Does
|
||||||
|
|
||||||
The memory subagent focuses on cleaning up files. It:
|
The memory subagent focuses on cleaning up files. It:
|
||||||
- ✅ Reads files from `.letta/backups/working/`
|
- Reads files from `.letta/backups/working/`
|
||||||
- ✅ Edits files to improve structure and consolidate redundancy
|
- Edits files to improve structure and consolidate redundancy
|
||||||
- ✅ Merges related blocks together to reduce fragmentation
|
- Merges related blocks together to reduce fragmentation
|
||||||
- ✅ Reorganizes information for better clarity and scannability
|
- Reorganizes information for better clarity and scannability
|
||||||
- ✅ Deletes source files after merging their content (using Bash `rm` command)
|
- Deletes source files after merging their content (using Bash `rm` command)
|
||||||
- ✅ Provides detailed before/after reports including merge operations
|
- Provides detailed before/after reports including merge operations
|
||||||
- ❌ Does NOT run backup scripts (main agent does this)
|
- Does NOT run backup scripts (main agent does this)
|
||||||
- ❌ Does NOT run restore 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.
|
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.
|
||||||
|
|
||||||
@@ -169,10 +150,10 @@ The memory subagent runs with `bypassPermissions` mode, giving it full Bash acce
|
|||||||
- After merging blocks, DELETE the source files to avoid duplication
|
- After merging blocks, DELETE the source files to avoid duplication
|
||||||
|
|
||||||
**When to DELETE a file:**
|
**When to DELETE a file:**
|
||||||
- ✅ **After merging** - You've consolidated its content into another block (common and encouraged)
|
- 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
|
- Junk data - File contains only irrelevant test/junk data with no project connection
|
||||||
- ✅ **Empty/deprecated** - File is just a notice with no unique information
|
- 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
|
- Don't delete - If file has unique information that hasn't been merged elsewhere
|
||||||
|
|
||||||
**What to preserve:**
|
**What to preserve:**
|
||||||
- User preferences (sacred - never delete)
|
- User preferences (sacred - never delete)
|
||||||
198
src/skills/builtin/memory-defrag/scripts/backup-memory.ts
Normal file
198
src/skills/builtin/memory-defrag/scripts/backup-memory.ts
Normal file
@@ -0,0 +1,198 @@
|
|||||||
|
#!/usr/bin/env npx tsx
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
|
*
|
||||||
|
* This script is standalone and can be run outside the CLI process.
|
||||||
|
* It reads auth from LETTA_API_KEY env var or ~/.letta/settings.json.
|
||||||
|
*
|
||||||
|
* Usage:
|
||||||
|
* npx tsx backup-memory.ts <agent-id> [backup-dir]
|
||||||
|
*
|
||||||
|
* Example:
|
||||||
|
* npx tsx backup-memory.ts agent-abc123
|
||||||
|
* npx tsx backup-memory.ts $LETTA_AGENT_ID .letta/backups/working
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||||
|
import { createRequire } from "node:module";
|
||||||
|
import { homedir } from "node:os";
|
||||||
|
import { join } from "node:path";
|
||||||
|
|
||||||
|
// Use createRequire for @letta-ai/letta-client so NODE_PATH is respected
|
||||||
|
// (ES module imports don't respect NODE_PATH, but require does)
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
const Letta = require("@letta-ai/letta-client")
|
||||||
|
.default as typeof import("@letta-ai/letta-client").default;
|
||||||
|
type LettaClient = InstanceType<typeof Letta>;
|
||||||
|
|
||||||
|
export interface BackupManifest {
|
||||||
|
agent_id: string;
|
||||||
|
timestamp: string;
|
||||||
|
backup_path: string;
|
||||||
|
blocks: Array<{
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
filename: string;
|
||||||
|
limit: number;
|
||||||
|
value_length: number;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get API key from env var or settings file
|
||||||
|
*/
|
||||||
|
function getApiKey(): string {
|
||||||
|
if (process.env.LETTA_API_KEY) {
|
||||||
|
return process.env.LETTA_API_KEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingsPath = join(homedir(), ".letta", "settings.json");
|
||||||
|
try {
|
||||||
|
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
||||||
|
if (settings.env?.LETTA_API_KEY) {
|
||||||
|
return settings.env.LETTA_API_KEY;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Settings file doesn't exist or is invalid
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
"No LETTA_API_KEY found. Set the env var or run the Letta CLI to authenticate.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a Letta client with auth from env/settings
|
||||||
|
*/
|
||||||
|
function createClient(): LettaClient {
|
||||||
|
return new Letta({ apiKey: getApiKey() });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backup memory blocks to local files
|
||||||
|
*/
|
||||||
|
async function backupMemory(
|
||||||
|
agentId: string,
|
||||||
|
backupDir?: string,
|
||||||
|
): Promise<string> {
|
||||||
|
const client = createClient();
|
||||||
|
|
||||||
|
// Create backup directory
|
||||||
|
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||||
|
const defaultBackupDir = join(
|
||||||
|
process.cwd(),
|
||||||
|
".letta",
|
||||||
|
"backups",
|
||||||
|
agentId,
|
||||||
|
timestamp,
|
||||||
|
);
|
||||||
|
const backupPath = backupDir || defaultBackupDir;
|
||||||
|
|
||||||
|
mkdirSync(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 as { items?: unknown[] }).items ||
|
||||||
|
(blocksResponse as { blocks?: unknown[] }).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 as Array<{
|
||||||
|
id: string;
|
||||||
|
label?: string;
|
||||||
|
value?: string;
|
||||||
|
limit?: number;
|
||||||
|
}>) {
|
||||||
|
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 || "";
|
||||||
|
writeFileSync(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");
|
||||||
|
writeFileSync(manifestPath, JSON.stringify(manifest, null, 2), "utf-8");
|
||||||
|
console.log(` ✓ manifest.json`);
|
||||||
|
|
||||||
|
console.log(`\n✅ Backup complete: ${backupPath}`);
|
||||||
|
return backupPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLI Entry Point - check if this file is being run directly
|
||||||
|
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
||||||
|
if (isMainModule) {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
||||||
|
console.log(`
|
||||||
|
Usage: npx tsx backup-memory.ts <agent-id> [backup-dir]
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
agent-id Agent ID to backup (can use $LETTA_AGENT_ID)
|
||||||
|
backup-dir Optional custom backup directory
|
||||||
|
Default: .letta/backups/<agent-id>/<timestamp>
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
npx tsx backup-memory.ts agent-abc123
|
||||||
|
npx tsx backup-memory.ts $LETTA_AGENT_ID
|
||||||
|
npx tsx 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 instanceof Error ? error.message : String(error),
|
||||||
|
);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export { backupMemory };
|
||||||
242
.letta/memory-utils/restore-memory.ts → src/skills/builtin/memory-defrag/scripts/restore-memory.ts
Executable file → Normal file
242
.letta/memory-utils/restore-memory.ts → src/skills/builtin/memory-defrag/scripts/restore-memory.ts
Executable file → Normal file
@@ -1,61 +1,116 @@
|
|||||||
#!/usr/bin/env bun
|
#!/usr/bin/env npx tsx
|
||||||
/**
|
/**
|
||||||
* Restore Memory Blocks from Local Files
|
* Restore Memory Blocks from Local Files
|
||||||
*
|
*
|
||||||
* Imports memory blocks from local files back into an agent.
|
* Imports memory blocks from local files back into an agent.
|
||||||
* Reads files from a backup directory and updates the agent's memory blocks.
|
* Reads files from a backup directory and updates the agent's memory blocks.
|
||||||
*
|
*
|
||||||
|
* This script is standalone and can be run outside the CLI process.
|
||||||
|
* It reads auth from LETTA_API_KEY env var or ~/.letta/settings.json.
|
||||||
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* bun .letta/memory-utils/restore-memory.ts <agent-id> <backup-dir>
|
* npx tsx restore-memory.ts <agent-id> <backup-dir> [options]
|
||||||
*
|
*
|
||||||
* Example:
|
* Example:
|
||||||
* bun .letta/memory-utils/restore-memory.ts agent-abc123 .letta/backups/working
|
* npx tsx restore-memory.ts agent-abc123 .letta/backups/working
|
||||||
* bun .letta/memory-utils/restore-memory.ts $LETTA_PARENT_AGENT_ID .letta/backups/working
|
* npx tsx restore-memory.ts $LETTA_AGENT_ID .letta/backups/working --dry-run
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { readFile, readdir } from "node:fs/promises";
|
import { readdirSync, readFileSync } from "node:fs";
|
||||||
import { join, extname } from "node:path";
|
import { createRequire } from "node:module";
|
||||||
import { getClient } from "../../src/agent/client";
|
import { homedir } from "node:os";
|
||||||
import { settingsManager } from "../../src/settings-manager";
|
import { extname, join } from "node:path";
|
||||||
|
|
||||||
import type { BackupManifest } from "./backup-memory";
|
import type { BackupManifest } from "./backup-memory";
|
||||||
|
|
||||||
|
// Use createRequire for @letta-ai/letta-client so NODE_PATH is respected
|
||||||
|
// (ES module imports don't respect NODE_PATH, but require does)
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
const Letta = require("@letta-ai/letta-client")
|
||||||
|
.default as typeof import("@letta-ai/letta-client").default;
|
||||||
|
type LettaClient = InstanceType<typeof Letta>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get API key from env var or settings file
|
||||||
|
*/
|
||||||
|
function getApiKey(): string {
|
||||||
|
if (process.env.LETTA_API_KEY) {
|
||||||
|
return process.env.LETTA_API_KEY;
|
||||||
|
}
|
||||||
|
|
||||||
|
const settingsPath = join(homedir(), ".letta", "settings.json");
|
||||||
|
try {
|
||||||
|
const settings = JSON.parse(readFileSync(settingsPath, "utf-8"));
|
||||||
|
if (settings.env?.LETTA_API_KEY) {
|
||||||
|
return settings.env.LETTA_API_KEY;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Settings file doesn't exist or is invalid
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new Error(
|
||||||
|
"No LETTA_API_KEY found. Set the env var or run the Letta CLI to authenticate.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a Letta client with auth from env/settings
|
||||||
|
*/
|
||||||
|
function createClient(): LettaClient {
|
||||||
|
return new Letta({ apiKey: getApiKey() });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restore memory blocks from local files
|
||||||
|
*/
|
||||||
async function restoreMemory(
|
async function restoreMemory(
|
||||||
agentId: string,
|
agentId: string,
|
||||||
backupDir: string,
|
backupDir: string,
|
||||||
options: { dryRun?: boolean } = {},
|
options: { dryRun?: boolean } = {},
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
await settingsManager.initialize();
|
const client = createClient();
|
||||||
const client = await getClient();
|
|
||||||
|
|
||||||
console.log(`Restoring memory blocks for agent ${agentId}...`);
|
console.log(`Restoring memory blocks for agent ${agentId}...`);
|
||||||
console.log(`Source: ${backupDir}`);
|
console.log(`Source: ${backupDir}`);
|
||||||
|
|
||||||
if (options.dryRun) {
|
if (options.dryRun) {
|
||||||
console.log("⚠️ DRY RUN MODE - No changes will be made\n");
|
console.log("⚠️ DRY RUN MODE - No changes will be made\n");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read manifest
|
// Read manifest
|
||||||
const manifestPath = join(backupDir, "manifest.json");
|
const manifestPath = join(backupDir, "manifest.json");
|
||||||
let manifest: BackupManifest | null = null;
|
let manifest: BackupManifest | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const manifestContent = await readFile(manifestPath, "utf-8");
|
const manifestContent = readFileSync(manifestPath, "utf-8");
|
||||||
manifest = JSON.parse(manifestContent);
|
manifest = JSON.parse(manifestContent);
|
||||||
console.log(`Loaded manifest (${manifest.blocks.length} blocks)\n`);
|
console.log(`Loaded manifest (${manifest?.blocks.length} blocks)\n`);
|
||||||
} catch (error) {
|
} catch {
|
||||||
console.warn("Warning: No manifest.json found, will scan directory for .md files");
|
console.warn(
|
||||||
|
"Warning: No manifest.json found, will scan directory for .md files",
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get current agent blocks
|
// Get current agent blocks
|
||||||
const blocksResponse = await client.agents.blocks.list(agentId);
|
const blocksResponse = await client.agents.blocks.list(agentId);
|
||||||
const currentBlocks = Array.isArray(blocksResponse)
|
const currentBlocks = Array.isArray(blocksResponse)
|
||||||
? blocksResponse
|
? blocksResponse
|
||||||
: (blocksResponse.items || blocksResponse.blocks || []);
|
: (blocksResponse as { items?: unknown[] }).items ||
|
||||||
const blocksByLabel = new Map(currentBlocks.map((b) => [b.label, b]));
|
(blocksResponse as { blocks?: unknown[] }).blocks ||
|
||||||
|
[];
|
||||||
|
const blocksByLabel = new Map(
|
||||||
|
(currentBlocks as Array<{ label: string; id: string; value?: string }>).map(
|
||||||
|
(b) => [b.label, b],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
// Determine which files to restore
|
// Determine which files to restore
|
||||||
let filesToRestore: Array<{ label: string; filename: string; blockId?: string }> = [];
|
let filesToRestore: Array<{
|
||||||
|
label: string;
|
||||||
|
filename: string;
|
||||||
|
blockId?: string;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
if (manifest) {
|
if (manifest) {
|
||||||
// Use manifest
|
// Use manifest
|
||||||
filesToRestore = manifest.blocks.map((b) => ({
|
filesToRestore = manifest.blocks.map((b) => ({
|
||||||
@@ -65,7 +120,7 @@ async function restoreMemory(
|
|||||||
}));
|
}));
|
||||||
} else {
|
} else {
|
||||||
// Scan directory for .md files
|
// Scan directory for .md files
|
||||||
const files = await readdir(backupDir);
|
const files = readdirSync(backupDir);
|
||||||
filesToRestore = files
|
filesToRestore = files
|
||||||
.filter((f) => extname(f) === ".md")
|
.filter((f) => extname(f) === ".md")
|
||||||
.map((f) => ({
|
.map((f) => ({
|
||||||
@@ -73,52 +128,60 @@ async function restoreMemory(
|
|||||||
filename: f,
|
filename: f,
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`Found ${filesToRestore.length} files to restore\n`);
|
console.log(`Found ${filesToRestore.length} files to restore\n`);
|
||||||
|
|
||||||
// Detect blocks to delete (exist on agent but not in backup)
|
// Detect blocks to delete (exist on agent but not in backup)
|
||||||
const backupLabels = new Set(filesToRestore.map((f) => f.label));
|
const backupLabels = new Set(filesToRestore.map((f) => f.label));
|
||||||
const blocksToDelete = currentBlocks.filter((b) => !backupLabels.has(b.label));
|
const blocksToDelete = (
|
||||||
|
currentBlocks as Array<{ label: string; id: string }>
|
||||||
|
).filter((b) => !backupLabels.has(b.label));
|
||||||
|
|
||||||
// Restore each block
|
// Restore each block
|
||||||
let updated = 0;
|
let updated = 0;
|
||||||
let created = 0;
|
let created = 0;
|
||||||
let skipped = 0;
|
let skipped = 0;
|
||||||
let deleted = 0;
|
let deleted = 0;
|
||||||
|
|
||||||
// Track new blocks for later confirmation
|
// Track new blocks for later confirmation
|
||||||
const blocksToCreate: Array<{ label: string; value: string; description: string }> = [];
|
const blocksToCreate: Array<{
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
description: string;
|
||||||
|
}> = [];
|
||||||
|
|
||||||
for (const { label, filename } of filesToRestore) {
|
for (const { label, filename } of filesToRestore) {
|
||||||
const filepath = join(backupDir, filename);
|
const filepath = join(backupDir, filename);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const newValue = await readFile(filepath, "utf-8");
|
const newValue = readFileSync(filepath, "utf-8");
|
||||||
const existingBlock = blocksByLabel.get(label);
|
const existingBlock = blocksByLabel.get(label);
|
||||||
|
|
||||||
if (existingBlock) {
|
if (existingBlock) {
|
||||||
// Update existing block
|
// Update existing block
|
||||||
const unchanged = existingBlock.value === newValue;
|
const unchanged = existingBlock.value === newValue;
|
||||||
|
|
||||||
if (unchanged) {
|
if (unchanged) {
|
||||||
console.log(` ⏭️ ${label} - unchanged, skipping`);
|
console.log(` ⏭️ ${label} - unchanged, skipping`);
|
||||||
skipped++;
|
skipped++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options.dryRun) {
|
if (!options.dryRun) {
|
||||||
await client.agents.blocks.update(label, {
|
await client.agents.blocks.update(label, {
|
||||||
agent_id: agentId,
|
agent_id: agentId,
|
||||||
value: newValue,
|
value: newValue,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const oldLen = existingBlock.value?.length || 0;
|
const oldLen = existingBlock.value?.length || 0;
|
||||||
const newLen = newValue.length;
|
const newLen = newValue.length;
|
||||||
const diff = newLen - oldLen;
|
const diff = newLen - oldLen;
|
||||||
const diffStr = diff > 0 ? `+${diff}` : `${diff}`;
|
const diffStr = diff > 0 ? `+${diff}` : `${diff}`;
|
||||||
|
|
||||||
console.log(` ✓ ${label} - updated (${oldLen} -> ${newLen} chars, ${diffStr})`);
|
console.log(
|
||||||
|
` ✓ ${label} - updated (${oldLen} -> ${newLen} chars, ${diffStr})`,
|
||||||
|
);
|
||||||
updated++;
|
updated++;
|
||||||
} else {
|
} else {
|
||||||
// New block - collect for later confirmation
|
// New block - collect for later confirmation
|
||||||
@@ -130,26 +193,30 @@ async function restoreMemory(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(` ❌ ${label} - error: ${error.message}`);
|
console.error(
|
||||||
|
` ❌ ${label} - error: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle new blocks (exist in backup but not on agent)
|
// Handle new blocks (exist in backup but not on agent)
|
||||||
if (blocksToCreate.length > 0) {
|
if (blocksToCreate.length > 0) {
|
||||||
console.log(`\n➕ Found ${blocksToCreate.length} new block(s) to create:`);
|
console.log(`\n➕ Found ${blocksToCreate.length} new block(s) to create:`);
|
||||||
for (const block of blocksToCreate) {
|
for (const block of blocksToCreate) {
|
||||||
console.log(` - ${block.label} (${block.value.length} chars)`);
|
console.log(` - ${block.label} (${block.value.length} chars)`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options.dryRun) {
|
if (!options.dryRun) {
|
||||||
console.log(`\nThese blocks will be CREATED on the agent.`);
|
console.log(`\nThese blocks will be CREATED on the agent.`);
|
||||||
console.log(`Press Ctrl+C to cancel, or press Enter to confirm creation...`);
|
console.log(
|
||||||
|
`Press Ctrl+C to cancel, or press Enter to confirm creation...`,
|
||||||
|
);
|
||||||
|
|
||||||
// Wait for user confirmation
|
// Wait for user confirmation
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
process.stdin.once('data', () => resolve());
|
process.stdin.once("data", () => resolve());
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log();
|
console.log();
|
||||||
for (const block of blocksToCreate) {
|
for (const block of blocksToCreate) {
|
||||||
try {
|
try {
|
||||||
@@ -160,43 +227,49 @@ async function restoreMemory(
|
|||||||
description: block.description,
|
description: block.description,
|
||||||
limit: 20000,
|
limit: 20000,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!createdBlock.id) {
|
if (!createdBlock.id) {
|
||||||
throw new Error(`Created block ${block.label} has no ID`);
|
throw new Error(`Created block ${block.label} has no ID`);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attach the newly created block to the agent
|
// Attach the newly created block to the agent
|
||||||
await client.agents.blocks.attach(createdBlock.id, {
|
await client.agents.blocks.attach(createdBlock.id, {
|
||||||
agent_id: agentId,
|
agent_id: agentId,
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log(` ✅ ${block.label} - created and attached`);
|
console.log(` ✅ ${block.label} - created and attached`);
|
||||||
created++;
|
created++;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(` ❌ ${block.label} - error creating: ${error.message}`);
|
console.error(
|
||||||
|
` ❌ ${block.label} - error creating: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(`\n(Would create these blocks if not in dry-run mode)`);
|
console.log(`\n(Would create these blocks if not in dry-run mode)`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle deletions (blocks that exist on agent but not in backup)
|
// Handle deletions (blocks that exist on agent but not in backup)
|
||||||
if (blocksToDelete.length > 0) {
|
if (blocksToDelete.length > 0) {
|
||||||
console.log(`\n⚠️ Found ${blocksToDelete.length} block(s) that were removed from backup:`);
|
console.log(
|
||||||
|
`\n⚠️ Found ${blocksToDelete.length} block(s) that were removed from backup:`,
|
||||||
|
);
|
||||||
for (const block of blocksToDelete) {
|
for (const block of blocksToDelete) {
|
||||||
console.log(` - ${block.label}`);
|
console.log(` - ${block.label}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options.dryRun) {
|
if (!options.dryRun) {
|
||||||
console.log(`\nThese blocks will be DELETED from the agent.`);
|
console.log(`\nThese blocks will be DELETED from the agent.`);
|
||||||
console.log(`Press Ctrl+C to cancel, or press Enter to confirm deletion...`);
|
console.log(
|
||||||
|
`Press Ctrl+C to cancel, or press Enter to confirm deletion...`,
|
||||||
|
);
|
||||||
|
|
||||||
// Wait for user confirmation
|
// Wait for user confirmation
|
||||||
await new Promise<void>((resolve) => {
|
await new Promise<void>((resolve) => {
|
||||||
process.stdin.once('data', () => resolve());
|
process.stdin.once("data", () => resolve());
|
||||||
});
|
});
|
||||||
|
|
||||||
console.log();
|
console.log();
|
||||||
for (const block of blocksToDelete) {
|
for (const block of blocksToDelete) {
|
||||||
try {
|
try {
|
||||||
@@ -206,20 +279,22 @@ async function restoreMemory(
|
|||||||
console.log(` 🗑️ ${block.label} - deleted`);
|
console.log(` 🗑️ ${block.label} - deleted`);
|
||||||
deleted++;
|
deleted++;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(` ❌ ${block.label} - error deleting: ${error.message}`);
|
console.error(
|
||||||
|
` ❌ ${block.label} - error deleting: ${error instanceof Error ? error.message : String(error)}`,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
console.log(`\n(Would delete these blocks if not in dry-run mode)`);
|
console.log(`\n(Would delete these blocks if not in dry-run mode)`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`\n📊 Summary:`);
|
console.log(`\n📊 Summary:`);
|
||||||
console.log(` Updated: ${updated}`);
|
console.log(` Updated: ${updated}`);
|
||||||
console.log(` Skipped: ${skipped}`);
|
console.log(` Skipped: ${skipped}`);
|
||||||
console.log(` Created: ${created}`);
|
console.log(` Created: ${created}`);
|
||||||
console.log(` Deleted: ${deleted}`);
|
console.log(` Deleted: ${deleted}`);
|
||||||
|
|
||||||
if (options.dryRun) {
|
if (options.dryRun) {
|
||||||
console.log(`\n⚠️ DRY RUN - No changes were made`);
|
console.log(`\n⚠️ DRY RUN - No changes were made`);
|
||||||
console.log(` Run without --dry-run to apply changes`);
|
console.log(` Run without --dry-run to apply changes`);
|
||||||
@@ -228,43 +303,46 @@ async function restoreMemory(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// CLI Entry Point
|
// CLI Entry Point - check if this file is being run directly
|
||||||
if (import.meta.main) {
|
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
|
||||||
|
if (isMainModule) {
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
if (args.length === 0 || args[0] === "--help" || args[0] === "-h") {
|
||||||
console.log(`
|
console.log(`
|
||||||
Usage: bun .letta/memory-utils/restore-memory.ts <agent-id> <backup-dir> [options]
|
Usage: npx tsx restore-memory.ts <agent-id> <backup-dir> [options]
|
||||||
|
|
||||||
Arguments:
|
Arguments:
|
||||||
agent-id Agent ID to restore to (can use $LETTA_PARENT_AGENT_ID)
|
agent-id Agent ID to restore to (can use $LETTA_AGENT_ID)
|
||||||
backup-dir Backup directory containing memory block files
|
backup-dir Backup directory containing memory block files
|
||||||
|
|
||||||
Options:
|
Options:
|
||||||
--dry-run Preview changes without applying them
|
--dry-run Preview changes without applying them
|
||||||
|
|
||||||
Examples:
|
Examples:
|
||||||
bun .letta/memory-utils/restore-memory.ts agent-abc123 .letta/backups/working
|
npx tsx restore-memory.ts agent-abc123 .letta/backups/working
|
||||||
bun .letta/memory-utils/restore-memory.ts $LETTA_PARENT_AGENT_ID .letta/backups/working
|
npx tsx restore-memory.ts $LETTA_AGENT_ID .letta/backups/working
|
||||||
bun .letta/memory-utils/restore-memory.ts agent-abc123 .letta/backups/working --dry-run
|
npx tsx restore-memory.ts agent-abc123 .letta/backups/working --dry-run
|
||||||
`);
|
`);
|
||||||
process.exit(0);
|
process.exit(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
const agentId = args[0];
|
const agentId = args[0];
|
||||||
const backupDir = args[1];
|
const backupDir = args[1];
|
||||||
const dryRun = args.includes("--dry-run");
|
const dryRun = args.includes("--dry-run");
|
||||||
|
|
||||||
if (!agentId || !backupDir) {
|
if (!agentId || !backupDir) {
|
||||||
console.error("Error: agent-id and backup-dir are required");
|
console.error("Error: agent-id and backup-dir are required");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
restoreMemory(agentId, backupDir, { dryRun })
|
restoreMemory(agentId, backupDir, { dryRun }).catch((error) => {
|
||||||
.catch((error) => {
|
console.error(
|
||||||
console.error("Error restoring memory:", error.message);
|
"Error restoring memory:",
|
||||||
process.exit(1);
|
error instanceof Error ? error.message : String(error),
|
||||||
});
|
);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export { restoreMemory };
|
export { restoreMemory };
|
||||||
Reference in New Issue
Block a user