feat: add session context system reminder on first message (#229)
Co-authored-by: Letta <noreply@letta.com>
This commit is contained in:
@@ -360,6 +360,8 @@ export default function App({
|
||||
>(null);
|
||||
const [llmConfig, setLlmConfig] = useState<LlmConfig | null>(null);
|
||||
const [agentName, setAgentName] = useState<string | null>(null);
|
||||
const [agentDescription, setAgentDescription] = useState<string | null>(null);
|
||||
const [agentLastRunAt, setAgentLastRunAt] = useState<string | null>(null);
|
||||
const currentModelLabel =
|
||||
llmConfig?.model_endpoint_type && llmConfig?.model
|
||||
? `${llmConfig.model_endpoint_type}/${llmConfig.model}`
|
||||
@@ -391,6 +393,9 @@ export default function App({
|
||||
// Session stats tracking
|
||||
const sessionStatsRef = useRef(new SessionStats());
|
||||
|
||||
// Track if we've sent the session context for this CLI session
|
||||
const hasSentSessionContextRef = useRef(false);
|
||||
|
||||
// Show exit stats on exit
|
||||
const [showExitStats, setShowExitStats] = useState(false);
|
||||
|
||||
@@ -617,6 +622,11 @@ export default function App({
|
||||
const agent = await client.agents.retrieve(agentId);
|
||||
setLlmConfig(agent.llm_config);
|
||||
setAgentName(agent.name);
|
||||
setAgentDescription(agent.description ?? null);
|
||||
// Get last message timestamp from agent state if available
|
||||
const lastRunCompletion = (agent as { last_run_completion?: string })
|
||||
.last_run_completion;
|
||||
setAgentLastRunAt(lastRunCompletion ?? null);
|
||||
|
||||
// Detect current toolset from attached tools
|
||||
const { detectToolsetFromAgent } = await import("../tools/toolset");
|
||||
@@ -2418,8 +2428,29 @@ ${recentCommits}
|
||||
// Prepend skill unload reminder if skills are loaded (using cached flag)
|
||||
const skillUnloadReminder = getSkillUnloadReminder();
|
||||
|
||||
// Combine reminders with content (plan mode first, then skill unload)
|
||||
const allReminders = planModeReminder + skillUnloadReminder;
|
||||
// Prepend session context on first message of CLI session (if enabled)
|
||||
let sessionContextReminder = "";
|
||||
const sessionContextEnabled = settingsManager.getSetting(
|
||||
"sessionContextEnabled",
|
||||
);
|
||||
if (!hasSentSessionContextRef.current && sessionContextEnabled) {
|
||||
const { buildSessionContext } = await import(
|
||||
"./helpers/sessionContext"
|
||||
);
|
||||
sessionContextReminder = buildSessionContext({
|
||||
agentInfo: {
|
||||
id: agentId,
|
||||
name: agentName,
|
||||
description: agentDescription,
|
||||
lastRunAt: agentLastRunAt,
|
||||
},
|
||||
});
|
||||
hasSentSessionContextRef.current = true;
|
||||
}
|
||||
|
||||
// Combine reminders with content (session context first, then plan mode, then skill unload)
|
||||
const allReminders =
|
||||
sessionContextReminder + planModeReminder + skillUnloadReminder;
|
||||
const messageContent =
|
||||
allReminders && typeof contentParts === "string"
|
||||
? allReminders + contentParts
|
||||
@@ -2553,6 +2584,8 @@ ${recentCommits}
|
||||
refreshDerived,
|
||||
agentId,
|
||||
agentName,
|
||||
agentDescription,
|
||||
agentLastRunAt,
|
||||
handleExit,
|
||||
isExecutingTool,
|
||||
queuedApprovalResults,
|
||||
|
||||
243
src/cli/helpers/sessionContext.ts
Normal file
243
src/cli/helpers/sessionContext.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
// src/cli/helpers/sessionContext.ts
|
||||
// Generates session context system reminder for the first message of each CLI session
|
||||
|
||||
import { execSync } from "node:child_process";
|
||||
import { platform } from "node:os";
|
||||
import { LETTA_CLOUD_API_URL } from "../../auth/oauth";
|
||||
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
|
||||
*/
|
||||
function getLocalTime(): string {
|
||||
const now = new Date();
|
||||
return now.toLocaleString(undefined, {
|
||||
weekday: "long",
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
timeZoneName: "short",
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get device type based on platform
|
||||
*/
|
||||
function getDeviceType(): string {
|
||||
const p = platform();
|
||||
switch (p) {
|
||||
case "darwin":
|
||||
return "macOS";
|
||||
case "win32":
|
||||
return "Windows";
|
||||
case "linux":
|
||||
return "Linux";
|
||||
default:
|
||||
return p;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
function safeGitExec(command: string, cwd: string): string | null {
|
||||
try {
|
||||
return execSync(command, { cwd, encoding: "utf-8", stdio: "pipe" }).trim();
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gather git information if in a git repository
|
||||
* Returns truncated commits (3) and status (20 lines)
|
||||
* Each field is gathered independently with fallbacks
|
||||
*/
|
||||
function getGitInfo(): {
|
||||
isGitRepo: boolean;
|
||||
branch?: string;
|
||||
recentCommits?: string;
|
||||
status?: string;
|
||||
} {
|
||||
const cwd = process.cwd();
|
||||
|
||||
try {
|
||||
// Check if we're in a git repo
|
||||
execSync("git rev-parse --git-dir", { cwd, stdio: "pipe" });
|
||||
|
||||
// Get current branch (with fallback)
|
||||
const branch = safeGitExec("git branch --show-current", cwd) ?? "(unknown)";
|
||||
|
||||
// Get recent commits (3 commits with author, with fallback)
|
||||
const recentCommits =
|
||||
safeGitExec('git log --format="%h %s (%an)" -3', cwd) ??
|
||||
"(failed to get commits)";
|
||||
|
||||
// Get git status (truncate to 20 lines, with fallback)
|
||||
const fullStatus =
|
||||
safeGitExec("git status --short", cwd) ?? "(failed to get status)";
|
||||
const statusLines = fullStatus.split("\n");
|
||||
let status = fullStatus;
|
||||
if (statusLines.length > 20) {
|
||||
status =
|
||||
statusLines.slice(0, 20).join("\n") +
|
||||
`\n... and ${statusLines.length - 20} more files`;
|
||||
}
|
||||
|
||||
return {
|
||||
isGitRepo: true,
|
||||
branch,
|
||||
recentCommits,
|
||||
status: status || "(clean working tree)",
|
||||
};
|
||||
} catch {
|
||||
return { isGitRepo: false };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the full session context system reminder
|
||||
* Returns empty string on any failure (graceful degradation)
|
||||
*/
|
||||
export function buildSessionContext(options: SessionContextOptions): string {
|
||||
try {
|
||||
const { agentInfo, serverUrl } = options;
|
||||
const cwd = process.cwd();
|
||||
|
||||
// Gather info with safe fallbacks
|
||||
let version = "unknown";
|
||||
try {
|
||||
version = getVersion();
|
||||
} catch {
|
||||
// version stays "unknown"
|
||||
}
|
||||
|
||||
let deviceType = "unknown";
|
||||
try {
|
||||
deviceType = getDeviceType();
|
||||
} catch {
|
||||
// deviceType stays "unknown"
|
||||
}
|
||||
|
||||
let localTime = "unknown";
|
||||
try {
|
||||
localTime = getLocalTime();
|
||||
} catch {
|
||||
// localTime stays "unknown"
|
||||
}
|
||||
|
||||
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)";
|
||||
}
|
||||
}
|
||||
|
||||
// Build the context
|
||||
let context = `<system-reminder>
|
||||
This is an automated message providing context about the user's environment.
|
||||
The user has just initiated a new connection via the [Letta Code CLI client](https://docs.letta.com/letta-code/index.md).
|
||||
|
||||
## Device Information
|
||||
- **Local time**: ${localTime}
|
||||
- **Device type**: ${deviceType}
|
||||
- **Letta Code version**: ${version}
|
||||
- **Current working directory**: ${cwd}
|
||||
`;
|
||||
|
||||
// Add git info if available
|
||||
if (gitInfo.isGitRepo) {
|
||||
context += `- **Git repository**: Yes (branch: ${gitInfo.branch})
|
||||
|
||||
### Recent Commits
|
||||
\`\`\`
|
||||
${gitInfo.recentCommits}
|
||||
\`\`\`
|
||||
|
||||
### Git Status
|
||||
\`\`\`
|
||||
${gitInfo.status}
|
||||
\`\`\`
|
||||
`;
|
||||
} else {
|
||||
context += `- **Git repository**: No
|
||||
`;
|
||||
}
|
||||
|
||||
// Add agent info
|
||||
context += `
|
||||
## Agent Information (i.e. information about you)
|
||||
- **Agent ID**: ${agentInfo.id}
|
||||
- **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>`;
|
||||
|
||||
return context;
|
||||
} catch {
|
||||
// If anything fails catastrophically, return empty string
|
||||
// This ensures the user's message still gets sent
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ export interface Settings {
|
||||
lastAgent: string | null;
|
||||
tokenStreaming: boolean;
|
||||
enableSleeptime: boolean;
|
||||
sessionContextEnabled: boolean; // Send device/agent context on first message of each session
|
||||
globalSharedBlockIds: Record<string, string>; // DEPRECATED: kept for backwards compat
|
||||
profiles?: Record<string, string>; // DEPRECATED: old format, kept for migration
|
||||
pinnedAgents?: string[]; // Array of agent IDs pinned globally
|
||||
@@ -38,6 +39,7 @@ const DEFAULT_SETTINGS: Settings = {
|
||||
lastAgent: null,
|
||||
tokenStreaming: false,
|
||||
enableSleeptime: false,
|
||||
sessionContextEnabled: true,
|
||||
globalSharedBlockIds: {},
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user