From 04e3d8739ec6141785f64f2658cdb9d970a92534 Mon Sep 17 00:00:00 2001 From: Devansh Jain <31609257+devanshrj@users.noreply.github.com> Date: Fri, 20 Feb 2026 17:54:35 -0800 Subject: [PATCH] refactor: split session-context reminder into agent-info + session-context (#1078) Co-authored-by: Letta --- src/cli/helpers/agentInfo.ts | 103 +++++++++++++++++ src/cli/helpers/sessionContext.ts | 95 +--------------- src/reminders/catalog.ts | 13 ++- src/reminders/engine.ts | 33 ++++-- src/reminders/state.ts | 2 + src/tests/agent-info.test.ts | 130 ++++++++++++++++++++++ src/tests/reminders/catalog.test.ts | 4 +- src/tests/reminders/engine-parity.test.ts | 7 +- src/tests/session-context.test.ts | 91 +++------------ 9 files changed, 296 insertions(+), 182 deletions(-) create mode 100644 src/cli/helpers/agentInfo.ts create mode 100644 src/tests/agent-info.test.ts diff --git a/src/cli/helpers/agentInfo.ts b/src/cli/helpers/agentInfo.ts new file mode 100644 index 0000000..a17b6da --- /dev/null +++ b/src/cli/helpers/agentInfo.ts @@ -0,0 +1,103 @@ +// src/cli/helpers/agentInfo.ts +// Generates agent info system reminder (agent identity, server, memory dir) + +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"; + +export interface AgentInfo { + id: string; + name: string | null; + description?: string | null; + lastRunAt?: string | null; +} + +export interface AgentInfoOptions { + agentInfo: AgentInfo; + serverUrl?: string; +} + +/** + * Format relative time from a date string + */ +function getRelativeTime(dateStr: string): string { + const date = new Date(dateStr); + const now = new Date(); + const diffMs = now.getTime() - date.getTime(); + const diffSecs = Math.floor(diffMs / 1000); + const diffMins = Math.floor(diffSecs / 60); + const diffHours = Math.floor(diffMins / 60); + const diffDays = Math.floor(diffHours / 24); + + if (diffDays > 0) { + return `${diffDays} day${diffDays === 1 ? "" : "s"} ago`; + } + if (diffHours > 0) { + return `${diffHours} hour${diffHours === 1 ? "" : "s"} ago`; + } + if (diffMins > 0) { + return `${diffMins} minute${diffMins === 1 ? "" : "s"} ago`; + } + return "just now"; +} + +/** + * Build the agent info system reminder. + * Contains agent identity information (ID, name, description, memory dir, server). + * Returns empty string on any failure (graceful degradation). + */ +export function buildAgentInfo(options: AgentInfoOptions): string { + try { + const { agentInfo, serverUrl } = options; + + // Get server URL + let actualServerUrl = LETTA_CLOUD_API_URL; + try { + const settings = settingsManager.getSettings(); + actualServerUrl = + serverUrl || + process.env.LETTA_BASE_URL || + settings.env?.LETTA_BASE_URL || + LETTA_CLOUD_API_URL; + } catch { + // actualServerUrl stays default + } + + // Format last run info + let lastRunInfo = "No previous messages"; + if (agentInfo.lastRunAt) { + try { + const lastRunDate = new Date(agentInfo.lastRunAt); + const localLastRun = lastRunDate.toLocaleString(); + const relativeTime = getRelativeTime(agentInfo.lastRunAt); + lastRunInfo = `${localLastRun} (${relativeTime})`; + } catch { + lastRunInfo = "(failed to parse last run time)"; + } + } + + 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)}\`` + : ""; + + return `${SYSTEM_REMINDER_OPEN} This is an automated message providing information about you. +- **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} +- **Server location**: ${actualServerUrl} +${SYSTEM_REMINDER_CLOSE}`; + } catch { + // If anything fails catastrophically, return empty string + // This ensures the user's message still gets sent + return ""; + } +} diff --git a/src/cli/helpers/sessionContext.ts b/src/cli/helpers/sessionContext.ts index 2756594..b113258 100644 --- a/src/cli/helpers/sessionContext.ts +++ b/src/cli/helpers/sessionContext.ts @@ -1,26 +1,12 @@ // src/cli/helpers/sessionContext.ts // Generates session context system reminder for the first message of each CLI session +// Contains device/environment information only. Agent metadata is in agentMetadata.ts. 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"; import { getVersion } from "../../version"; -interface AgentInfo { - id: string; - name: string | null; - description?: string | null; - lastRunAt?: string | null; -} - -interface SessionContextOptions { - agentInfo: AgentInfo; - serverUrl?: string; -} - /** * Get the current local time in a human-readable format */ @@ -54,30 +40,6 @@ export function getDeviceType(): string { } } -/** - * Format relative time from a date string - */ -function getRelativeTime(dateStr: string): string { - const date = new Date(dateStr); - const now = new Date(); - const diffMs = now.getTime() - date.getTime(); - const diffSecs = Math.floor(diffMs / 1000); - const diffMins = Math.floor(diffSecs / 60); - const diffHours = Math.floor(diffMins / 60); - const diffDays = Math.floor(diffHours / 24); - - if (diffDays > 0) { - return `${diffDays} day${diffDays === 1 ? "" : "s"} ago`; - } - if (diffHours > 0) { - return `${diffHours} hour${diffHours === 1 ? "" : "s"} ago`; - } - if (diffMins > 0) { - return `${diffMins} minute${diffMins === 1 ? "" : "s"} ago`; - } - return "just now"; -} - /** * Safely execute a git command, returning null on failure */ @@ -137,12 +99,12 @@ function getGitInfo(): { } /** - * Build the full session context system reminder - * Returns empty string on any failure (graceful degradation) + * Build the session context system reminder (device/environment info only). + * Agent metadata is handled separately by buildAgentMetadata(). + * Returns empty string on any failure (graceful degradation). */ -export function buildSessionContext(options: SessionContextOptions): string { +export function buildSessionContext(): string { try { - const { agentInfo, serverUrl } = options; const cwd = process.cwd(); // Gather info with safe fallbacks @@ -169,43 +131,6 @@ export function buildSessionContext(options: SessionContextOptions): string { const gitInfo = getGitInfo(); - // Get server URL - let actualServerUrl = LETTA_CLOUD_API_URL; - try { - const settings = settingsManager.getSettings(); - actualServerUrl = - serverUrl || - process.env.LETTA_BASE_URL || - settings.env?.LETTA_BASE_URL || - LETTA_CLOUD_API_URL; - } catch { - // actualServerUrl stays default - } - - // Format last run info - let lastRunInfo = "No previous messages"; - if (agentInfo.lastRunAt) { - try { - const lastRunDate = new Date(agentInfo.lastRunAt); - const localLastRun = lastRunDate.toLocaleString(); - const relativeTime = getRelativeTime(agentInfo.lastRunAt); - lastRunInfo = `${localLastRun} (${relativeTime})`; - } catch { - lastRunInfo = "(failed to parse last run time)"; - } - } - - 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. @@ -247,15 +172,7 @@ ${gitInfo.status} `; } - // Add agent info - context += ` -## Agent Information (i.e. information about you) -- **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} -- **Server location**: ${actualServerUrl} -${SYSTEM_REMINDER_CLOSE}`; + context += SYSTEM_REMINDER_CLOSE; return context; } catch { diff --git a/src/reminders/catalog.ts b/src/reminders/catalog.ts index b228f32..af6a12e 100644 --- a/src/reminders/catalog.ts +++ b/src/reminders/catalog.ts @@ -6,6 +6,7 @@ export type SharedReminderMode = export type SharedReminderId = | "session-context" + | "agent-info" | "skills" | "permission-mode" | "plan-mode" @@ -24,9 +25,19 @@ export const SHARED_REMINDER_CATALOG: ReadonlyArray = [ { id: "session-context", - description: "First-turn device/agent/git context", + description: "First-turn device/git/cwd context", modes: ["interactive", "headless-one-shot", "headless-bidirectional"], }, + { + id: "agent-info", + description: "Agent identity (ID, name, server, memory dir)", + modes: [ + "interactive", + "headless-one-shot", + "headless-bidirectional", + "subagent", + ], + }, { id: "skills", description: "Available skills system reminder (with reinjection)", diff --git a/src/reminders/engine.ts b/src/reminders/engine.ts index eba40b1..f0f22f9 100644 --- a/src/reminders/engine.ts +++ b/src/reminders/engine.ts @@ -7,6 +7,7 @@ import { SKILLS_DIR, type SkillSource, } from "../agent/skills"; +import { buildAgentInfo } from "../cli/helpers/agentInfo"; import { buildCompactionMemoryReminder, buildMemoryReminder, @@ -58,6 +59,27 @@ type SharedReminderProvider = ( context: SharedReminderContext, ) => Promise; +async function buildAgentInfoReminder( + context: SharedReminderContext, +): Promise { + if (context.state.hasSentAgentInfo) { + return null; + } + + const reminder = buildAgentInfo({ + agentInfo: { + id: context.agent.id, + name: context.agent.name, + description: context.agent.description, + lastRunAt: context.agent.lastRunAt, + }, + serverUrl: context.agent.serverUrl, + }); + + context.state.hasSentAgentInfo = true; + return reminder || null; +} + async function buildSessionContextReminder( context: SharedReminderContext, ): Promise { @@ -72,15 +94,7 @@ async function buildSessionContextReminder( return null; } - const reminder = buildSessionContext({ - agentInfo: { - id: context.agent.id, - name: context.agent.name, - description: context.agent.description, - lastRunAt: context.agent.lastRunAt, - }, - serverUrl: context.agent.serverUrl, - }); + const reminder = buildSessionContext(); context.state.hasSentSessionContext = true; return reminder || null; @@ -347,6 +361,7 @@ export const sharedReminderProviders: Record< SharedReminderId, SharedReminderProvider > = { + "agent-info": buildAgentInfoReminder, "session-context": buildSessionContextReminder, skills: buildSkillsReminder, "permission-mode": buildPermissionModeReminder, diff --git a/src/reminders/state.ts b/src/reminders/state.ts index f3eb444..e6db35d 100644 --- a/src/reminders/state.ts +++ b/src/reminders/state.ts @@ -18,6 +18,7 @@ export interface ToolsetChangeReminder { } export interface SharedReminderState { + hasSentAgentInfo: boolean; hasSentSessionContext: boolean; hasInjectedSkillsReminder: boolean; cachedSkillsReminder: string | null; @@ -32,6 +33,7 @@ export interface SharedReminderState { export function createSharedReminderState(): SharedReminderState { return { + hasSentAgentInfo: false, hasSentSessionContext: false, hasInjectedSkillsReminder: false, cachedSkillsReminder: null, diff --git a/src/tests/agent-info.test.ts b/src/tests/agent-info.test.ts new file mode 100644 index 0000000..37b209f --- /dev/null +++ b/src/tests/agent-info.test.ts @@ -0,0 +1,130 @@ +import { describe, expect, test } from "bun:test"; +import { getMemoryFilesystemRoot } from "../agent/memoryFilesystem"; +import { buildAgentInfo } from "../cli/helpers/agentInfo"; +import { settingsManager } from "../settings-manager"; + +describe("agent info reminder", () => { + test("always includes AGENT_ID env var", () => { + const agentId = "agent-test-agent-info"; + const context = buildAgentInfo({ + 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-agent-info-disabled"; + const original = settingsManager.isMemfsEnabled.bind(settingsManager); + ( + settingsManager as unknown as { + isMemfsEnabled: (id: string) => boolean; + } + ).isMemfsEnabled = () => false; + + try { + const context = buildAgentInfo({ + 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-agent-info-enabled"; + const original = settingsManager.isMemfsEnabled.bind(settingsManager); + ( + settingsManager as unknown as { + isMemfsEnabled: (id: string) => boolean; + } + ).isMemfsEnabled = () => true; + + try { + const context = buildAgentInfo({ + 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; + } + }); + + test("includes agent name and description", () => { + const context = buildAgentInfo({ + agentInfo: { + id: "agent-test", + name: "My Agent", + description: "Does cool stuff", + lastRunAt: null, + }, + serverUrl: "https://api.letta.com", + }); + + expect(context).toContain("**Agent name**: My Agent"); + expect(context).toContain("**Agent description**: Does cool stuff"); + }); + + test("includes server location", () => { + const context = buildAgentInfo({ + agentInfo: { + id: "agent-test", + name: "Test Agent", + lastRunAt: null, + }, + }); + + expect(context).toContain("**Server location**:"); + }); + + test("does not include device information", () => { + const context = buildAgentInfo({ + agentInfo: { + id: "agent-test", + name: "Test Agent", + lastRunAt: null, + }, + serverUrl: "https://api.letta.com", + }); + + expect(context).not.toContain("## Device Information"); + expect(context).not.toContain("Local time"); + expect(context).not.toContain("Git repository"); + }); +}); diff --git a/src/tests/reminders/catalog.test.ts b/src/tests/reminders/catalog.test.ts index 04ccbee..9fa24d8 100644 --- a/src/tests/reminders/catalog.test.ts +++ b/src/tests/reminders/catalog.test.ts @@ -30,11 +30,11 @@ describe("shared reminder catalog", () => { } }); - test("subagent mode has no reminders", () => { + test("subagent mode only has agent-info reminder", () => { const subagentReminders = SHARED_REMINDER_CATALOG.filter((entry) => entry.modes.includes("subagent"), ); - expect(subagentReminders).toEqual([]); + expect(subagentReminders.map((entry) => entry.id)).toEqual(["agent-info"]); }); test("command and toolset reminders are interactive-only", () => { diff --git a/src/tests/reminders/engine-parity.test.ts b/src/tests/reminders/engine-parity.test.ts index 9531e00..5ed9fe0 100644 --- a/src/tests/reminders/engine-parity.test.ts +++ b/src/tests/reminders/engine-parity.test.ts @@ -89,7 +89,7 @@ describe("shared reminder parity", () => { ); }); - test("subagent mode produces no reminders", async () => { + test("subagent mode produces only agent-info reminder", async () => { for (const reminderId of SHARED_REMINDER_IDS) { providerMap[reminderId] = async () => reminderId; } @@ -115,7 +115,8 @@ describe("shared reminder parity", () => { state: createSharedReminderState(), }); - expect(subagent.appliedReminderIds).toEqual([]); - expect(subagent.parts).toEqual([]); + expect(subagent.appliedReminderIds).toEqual(reminderIdsForMode("subagent")); + expect(subagent.appliedReminderIds).toEqual(["agent-info"]); + expect(subagent.parts.map((part) => part.text)).toEqual(["agent-info"]); }); }); diff --git a/src/tests/session-context.test.ts b/src/tests/session-context.test.ts index 88ded68..b426df3 100644 --- a/src/tests/session-context.test.ts +++ b/src/tests/session-context.test.ts @@ -1,88 +1,23 @@ 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", - }); + test("includes device information section", () => { + const context = buildSessionContext(); - expect(context).toContain( - `- **Agent ID (also stored in \`AGENT_ID\` env var)**: ${agentId}`, - ); + expect(context).toContain("## Device Information"); + expect(context).toContain("**Local time**"); + expect(context).toContain("**Device type**"); + expect(context).toContain("**Letta Code version**"); + expect(context).toContain("**Current working directory**"); }); - 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; + test("does not include agent information section", () => { + const context = buildSessionContext(); - 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; - } + expect(context).not.toContain("## Agent Information"); + expect(context).not.toContain("Agent ID"); + expect(context).not.toContain("Agent name"); + expect(context).not.toContain("Server location"); }); });