diff --git a/src/agent/prompts/system_prompt_memfs.txt b/src/agent/prompts/system_prompt_memfs.txt index f8b584a..1146500 100644 --- a/src/agent/prompts/system_prompt_memfs.txt +++ b/src/agent/prompts/system_prompt_memfs.txt @@ -4,11 +4,14 @@ You have an advanced memory system that enables you to remember past interactions and continuously improve your own capabilities. ## Memory Filesystem -Your memory is stored in a git repository at `~/.letta/agents//memory/`. This provides full version control, sync with the server, and create worktrees for parallel edits. +Your memory is stored in a git repository at `$MEMORY_DIR` (usually `~/.letta/agents/$AGENT_ID/memory/`). This provides full version control, sync with the server, and create worktrees for parallel edits. Each file contains metadata frontmatter include a `description` (the description of the file's contents), `limit` (the character limit), and optional `metadata`. The filesystem tree is always available in your system prompt, along with the contents of files in the `system/` folder. You also have additional external memory (e.g. your message history) that is accessible and that you can bring into context with tools when needed. +When running in Letta Code, shell tools provide: +- `MEMORY_DIR`: absolute path to this agent's memory repository + ## How It Works 1. Each `.md` file in `memory/system/` is pinned to your system prompt with tags 2. The `memory_filesystem` block renders the current tree view of all available memory files @@ -18,7 +21,7 @@ You also have additional external memory (e.g. your message history) that is acc ## Syncing ```bash -cd ~/.letta/agents//memory +cd "$MEMORY_DIR" # See what changed git status @@ -35,5 +38,5 @@ The system will remind you when your memory has uncommitted changes. Sync when c ## History ```bash -git -C ~/.letta/agents//memory log --oneline +git -C "$MEMORY_DIR" log --oneline ``` diff --git a/src/agent/prompts/system_prompt_memory.txt b/src/agent/prompts/system_prompt_memory.txt index c7b88b7..5fe4cbf 100644 --- a/src/agent/prompts/system_prompt_memory.txt +++ b/src/agent/prompts/system_prompt_memory.txt @@ -6,6 +6,8 @@ Your memory consists of core memory (composed of memory blocks) and external mem - Memory blocks: Each memory block contains a label (title), description (explaining how this block should influence your behavior), and value (the actual content). Memory blocks have size limits. Memory blocks are embedded within your system instructions and are pinned in-context (so they are always visible). - External memory: Additional memory storage that is accessible and that you can bring into context with tools when needed. +When running in Letta Code, shell tools provide `AGENT_ID`: your current agent ID. + Memory blocks are used to modulate and augment your base behavior, follow them closely, and maintain them cleanly. Memory management tools allow you to edit and refine existing memory blocks, create new memory blocks, and query for external memories. Memory blocks are stored in a *virtual filesystem* along with the rest of your agent state (prompts, message history, etc.), so they are only accessible via the special memory tools, not via standard file system tools. diff --git a/src/cli/App.tsx b/src/cli/App.tsx index 4aca584..3edaebb 100644 --- a/src/cli/App.tsx +++ b/src/cli/App.tsx @@ -7843,9 +7843,13 @@ ${recentCommits} ## Memory Filesystem Location Your memory blocks are synchronized with the filesystem at: -\`~/.letta/agents/${agentId}/memory/\` +\`${getMemoryFilesystemRoot(agentId)}\` -Use this path when working with memory files during initialization. +Environment variables available in Letta Code: +- \`AGENT_ID=${agentId}\` +- \`MEMORY_DIR=${getMemoryFilesystemRoot(agentId)}\` + +Use \`$MEMORY_DIR\` when working with memory files during initialization. ` : ""; diff --git a/src/cli/helpers/sessionContext.ts b/src/cli/helpers/sessionContext.ts index 711acd1..2756594 100644 --- a/src/cli/helpers/sessionContext.ts +++ b/src/cli/helpers/sessionContext.ts @@ -3,6 +3,7 @@ import { execSync } from "node:child_process"; import { platform } from "node:os"; +import { getMemoryFilesystemRoot } from "../../agent/memoryFilesystem"; import { LETTA_CLOUD_API_URL } from "../../auth/oauth"; import { SYSTEM_REMINDER_CLOSE, SYSTEM_REMINDER_OPEN } from "../../constants"; import { settingsManager } from "../../settings-manager"; @@ -194,6 +195,17 @@ export function buildSessionContext(options: SessionContextOptions): string { } } + const showMemoryDir = (() => { + try { + return settingsManager.isMemfsEnabled(agentInfo.id); + } catch { + return false; + } + })(); + const memoryDirLine = showMemoryDir + ? `\n- **Memory directory (also stored in \`MEMORY_DIR\` env var)**: \`${getMemoryFilesystemRoot(agentInfo.id)}\`` + : ""; + // Build the context let context = `${SYSTEM_REMINDER_OPEN} This is an automated message providing context about the user's environment. @@ -238,7 +250,7 @@ ${gitInfo.status} // Add agent info context += ` ## Agent Information (i.e. information about you) -- **Agent ID**: ${agentInfo.id} +- **Agent ID (also stored in \`AGENT_ID\` env var)**: ${agentInfo.id}${memoryDirLine} - **Agent name**: ${agentInfo.name || "(unnamed)"} (the user can change this with /rename) - **Agent description**: ${agentInfo.description || "(no description)"} (the user can change this with /description) - **Last message**: ${lastRunInfo} diff --git a/src/tests/session-context.test.ts b/src/tests/session-context.test.ts new file mode 100644 index 0000000..88ded68 --- /dev/null +++ b/src/tests/session-context.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, test } from "bun:test"; +import { getMemoryFilesystemRoot } from "../agent/memoryFilesystem"; +import { buildSessionContext } from "../cli/helpers/sessionContext"; +import { settingsManager } from "../settings-manager"; + +describe("session context reminder", () => { + test("always includes AGENT_ID env var", () => { + const agentId = "agent-test-session-context"; + const context = buildSessionContext({ + agentInfo: { + id: agentId, + name: "Test Agent", + description: "Test description", + lastRunAt: null, + }, + serverUrl: "https://api.letta.com", + }); + + expect(context).toContain( + `- **Agent ID (also stored in \`AGENT_ID\` env var)**: ${agentId}`, + ); + }); + + test("does not include MEMORY_DIR env var when memfs is disabled", () => { + const agentId = "agent-test-session-context-disabled"; + const original = settingsManager.isMemfsEnabled.bind(settingsManager); + ( + settingsManager as unknown as { + isMemfsEnabled: (id: string) => boolean; + } + ).isMemfsEnabled = () => false; + + try { + const context = buildSessionContext({ + agentInfo: { + id: agentId, + name: "Test Agent", + description: "Test description", + lastRunAt: null, + }, + serverUrl: "https://api.letta.com", + }); + + expect(context).not.toContain( + "Memory directory (also stored in `MEMORY_DIR` env var)", + ); + expect(context).not.toContain(getMemoryFilesystemRoot(agentId)); + } finally { + ( + settingsManager as unknown as { + isMemfsEnabled: (id: string) => boolean; + } + ).isMemfsEnabled = original; + } + }); + + test("includes MEMORY_DIR env var when memfs is enabled", () => { + const agentId = "agent-test-session-context-enabled"; + const original = settingsManager.isMemfsEnabled.bind(settingsManager); + ( + settingsManager as unknown as { + isMemfsEnabled: (id: string) => boolean; + } + ).isMemfsEnabled = () => true; + + try { + const context = buildSessionContext({ + agentInfo: { + id: agentId, + name: "Test Agent", + description: "Test description", + lastRunAt: null, + }, + serverUrl: "https://api.letta.com", + }); + + expect(context).toContain( + `- **Memory directory (also stored in \`MEMORY_DIR\` env var)**: \`${getMemoryFilesystemRoot(agentId)}\``, + ); + } finally { + ( + settingsManager as unknown as { + isMemfsEnabled: (id: string) => boolean; + } + ).isMemfsEnabled = original; + } + }); +}); diff --git a/src/tests/tools/shell-env.test.ts b/src/tests/tools/shell-env.test.ts index 70407d5..a256164 100644 --- a/src/tests/tools/shell-env.test.ts +++ b/src/tests/tools/shell-env.test.ts @@ -1,12 +1,38 @@ import { describe, expect, test } from "bun:test"; import { spawnSync } from "node:child_process"; import * as path from "node:path"; +import { getMemoryFilesystemRoot } from "../../agent/memoryFilesystem"; +import { settingsManager } from "../../settings-manager"; import { ensureLettaShimDir, getShellEnv, resolveLettaInvocation, } from "../../tools/impl/shellEnv"; +function withTemporaryAgentEnv(agentId: string, fn: () => T): T { + const originalAgentId = process.env.AGENT_ID; + const originalLettaAgentId = process.env.LETTA_AGENT_ID; + + process.env.AGENT_ID = agentId; + process.env.LETTA_AGENT_ID = agentId; + + try { + return fn(); + } finally { + if (originalAgentId === undefined) { + delete process.env.AGENT_ID; + } else { + process.env.AGENT_ID = originalAgentId; + } + + if (originalLettaAgentId === undefined) { + delete process.env.LETTA_AGENT_ID; + } else { + process.env.LETTA_AGENT_ID = originalLettaAgentId; + } + } +} + describe("shellEnv letta shim", () => { test("resolveLettaInvocation prefers explicit launcher env", () => { const invocation = resolveLettaInvocation( @@ -137,3 +163,74 @@ describe("shellEnv letta shim", () => { } }); }); + +test("getShellEnv injects AGENT_ID aliases", () => { + withTemporaryAgentEnv(`agent-test-${Date.now()}`, () => { + const env = getShellEnv(); + + expect(env.AGENT_ID).toBeTruthy(); + expect(env.LETTA_AGENT_ID).toBe(env.AGENT_ID); + }); +}); + +test("getShellEnv does not inject MEMORY_DIR aliases when memfs is disabled", () => { + withTemporaryAgentEnv(`agent-test-${Date.now()}`, () => { + const originalIsMemfsEnabled = + settingsManager.isMemfsEnabled.bind(settingsManager); + const originalMemoryDir = process.env.MEMORY_DIR; + const originalLettaMemoryDir = process.env.LETTA_MEMORY_DIR; + ( + settingsManager as unknown as { isMemfsEnabled: (id: string) => boolean } + ).isMemfsEnabled = () => false; + process.env.MEMORY_DIR = "/tmp/stale-memory-dir"; + process.env.LETTA_MEMORY_DIR = "/tmp/stale-memory-dir"; + + try { + const env = getShellEnv(); + expect(env.LETTA_MEMORY_DIR).toBeUndefined(); + expect(env.MEMORY_DIR).toBeUndefined(); + } finally { + ( + settingsManager as unknown as { + isMemfsEnabled: (id: string) => boolean; + } + ).isMemfsEnabled = originalIsMemfsEnabled; + + if (originalMemoryDir === undefined) { + delete process.env.MEMORY_DIR; + } else { + process.env.MEMORY_DIR = originalMemoryDir; + } + + if (originalLettaMemoryDir === undefined) { + delete process.env.LETTA_MEMORY_DIR; + } else { + process.env.LETTA_MEMORY_DIR = originalLettaMemoryDir; + } + } + }); +}); + +test("getShellEnv injects MEMORY_DIR aliases when memfs is enabled", () => { + withTemporaryAgentEnv(`agent-test-${Date.now()}`, () => { + const original = settingsManager.isMemfsEnabled.bind(settingsManager); + ( + settingsManager as unknown as { isMemfsEnabled: (id: string) => boolean } + ).isMemfsEnabled = () => true; + + try { + const env = getShellEnv(); + expect(env.AGENT_ID).toBeTruthy(); + const resolvedAgentId = env.AGENT_ID as string; + const expectedMemoryDir = getMemoryFilesystemRoot(resolvedAgentId); + expect(env.LETTA_MEMORY_DIR).toBe(expectedMemoryDir); + expect(env.MEMORY_DIR).toBe(expectedMemoryDir); + } finally { + ( + settingsManager as unknown as { + isMemfsEnabled: (id: string) => boolean; + } + ).isMemfsEnabled = original; + } + }); +}); diff --git a/src/tools/impl/shellEnv.ts b/src/tools/impl/shellEnv.ts index 0a06d16..a390239 100644 --- a/src/tools/impl/shellEnv.ts +++ b/src/tools/impl/shellEnv.ts @@ -11,6 +11,7 @@ import * as path from "node:path"; import { fileURLToPath } from "node:url"; import { getServerUrl } from "../../agent/client"; import { getCurrentAgentId } from "../../agent/context"; +import { getMemoryFilesystemRoot } from "../../agent/memoryFilesystem"; import { settingsManager } from "../../settings-manager"; /** @@ -180,13 +181,43 @@ export function getShellEnv(): NodeJS.ProcessEnv { : pathPrefixes.join(path.delimiter); } - // Add Letta context for skill scripts + // Add Letta context for skill scripts. + // Prefer explicit agent context, but fall back to inherited env values. + let agentId: string | undefined; try { - env.LETTA_AGENT_ID = getCurrentAgentId(); + const resolvedAgentId = getCurrentAgentId(); + if (typeof resolvedAgentId === "string" && resolvedAgentId.trim()) { + agentId = resolvedAgentId.trim(); + } } catch { - // Context not set yet (e.g., during startup), skip + // Context not set yet (e.g., during startup), try env fallback below. } + if (!agentId) { + const fallbackAgentId = env.AGENT_ID || env.LETTA_AGENT_ID; + if (typeof fallbackAgentId === "string" && fallbackAgentId.trim()) { + agentId = fallbackAgentId.trim(); + } + } + + if (agentId) { + env.LETTA_AGENT_ID = agentId; + env.AGENT_ID = agentId; + + try { + if (settingsManager.isMemfsEnabled(agentId)) { + const memoryDir = getMemoryFilesystemRoot(agentId); + env.LETTA_MEMORY_DIR = memoryDir; + env.MEMORY_DIR = memoryDir; + } else { + // Clear inherited/stale memory-dir vars for non-memfs agents. + delete env.LETTA_MEMORY_DIR; + delete env.MEMORY_DIR; + } + } catch { + // Settings may not be initialized in tests/startup; preserve inherited values. + } + } // Inject API key and base URL from settings if not already in env if (!env.LETTA_API_KEY || !env.LETTA_BASE_URL) { try {