feat: recipe-based system prompt management (#1293)

This commit is contained in:
Devansh Jain
2026-03-09 17:20:11 -07:00
committed by GitHub
parent c55a1fbd22
commit f148beee5d
8 changed files with 470 additions and 295 deletions

View File

@@ -12,7 +12,6 @@ import { getModelContextWindow } from "./available-models";
import { getClient, getServerUrl } from "./client";
import { getLettaCodeHeaders } from "./http-headers";
import { getDefaultMemoryBlocks } from "./memory";
import { type MemoryPromptMode, reconcileMemoryPrompt } from "./memoryPrompt";
import {
formatAvailableModels,
getDefaultModel,
@@ -20,7 +19,12 @@ import {
resolveModel,
} from "./model";
import { updateAgentLLMConfig } from "./modify";
import { resolveSystemPrompt } from "./promptAssets";
import {
isKnownPreset,
type MemoryPromptMode,
resolveAndBuildSystemPrompt,
swapMemoryAddon,
} from "./promptAssets";
import { SLEEPTIME_MEMORY_PERSONA } from "./prompts/sleeptime";
/**
@@ -351,21 +355,11 @@ export async function createAgent(
(modelUpdateArgs?.context_window as number | undefined) ??
(await getModelContextWindow(modelHandle));
// Resolve system prompt content:
// 1. If systemPromptCustom is provided, use it as-is
// 2. Otherwise, resolve systemPromptPreset to content
// 3. Reconcile to the selected managed memory mode
let systemPromptContent: string;
if (options.systemPromptCustom) {
systemPromptContent = options.systemPromptCustom;
} else {
systemPromptContent = await resolveSystemPrompt(options.systemPromptPreset);
}
systemPromptContent = reconcileMemoryPrompt(
systemPromptContent,
options.memoryPromptMode ?? "standard",
);
// Resolve system prompt content
const memMode: MemoryPromptMode = options.memoryPromptMode ?? "standard";
const systemPromptContent = options.systemPromptCustom
? swapMemoryAddon(options.systemPromptCustom, memMode)
: await resolveAndBuildSystemPrompt(options.systemPromptPreset, memMode);
// Create agent with inline memory blocks (LET-7101: single API call instead of N+1)
// - memory_blocks: new blocks to create inline
@@ -462,6 +456,20 @@ export async function createAgent(
}
}
// Persist system prompt preset — only for non-subagents and known presets or custom.
// Guarded by isReady since settings may not be initialized in direct/test callers.
if (!isSubagent && settingsManager.isReady) {
if (options.systemPromptCustom) {
settingsManager.setSystemPromptPreset(fullAgent.id, "custom");
} else if (isKnownPreset(options.systemPromptPreset ?? "default")) {
settingsManager.setSystemPromptPreset(
fullAgent.id,
options.systemPromptPreset ?? "default",
);
}
// Subagent names: don't persist (no reproducible recipe)
}
// Build provenance info
const provenance: AgentProvenance = {
isNew: true,

View File

@@ -1,201 +0,0 @@
import {
SYSTEM_PROMPT_MEMFS_ADDON,
SYSTEM_PROMPT_MEMORY_ADDON,
} from "./promptAssets";
export type MemoryPromptMode = "standard" | "memfs";
export interface MemoryPromptDrift {
code:
| "legacy_memory_language_with_memfs"
| "memfs_language_with_standard_mode"
| "orphan_memfs_fragment";
message: string;
}
interface Heading {
level: number;
title: string;
startOffset: number;
}
function normalizeNewlines(text: string): string {
return text.replace(/\r\n/g, "\n");
}
function scanHeadingsOutsideFences(text: string): Heading[] {
const lines = text.split("\n");
const headings: Heading[] = [];
let inFence = false;
let fenceToken = "";
let offset = 0;
for (const line of lines) {
const trimmed = line.trimStart();
const fenceMatch = trimmed.match(/^(```+|~~~+)/);
if (fenceMatch) {
const token = fenceMatch[1] ?? fenceMatch[0] ?? "";
const tokenChar = token.startsWith("`") ? "`" : "~";
if (!inFence) {
inFence = true;
fenceToken = tokenChar;
} else if (fenceToken === tokenChar) {
inFence = false;
fenceToken = "";
}
}
if (!inFence) {
const headingMatch = line.match(/^\s*(#{1,6})\s+(.+?)\s*$/);
if (headingMatch) {
const hashes = headingMatch[1] ?? "";
const rawTitle = headingMatch[2] ?? "";
if (hashes && rawTitle) {
const level = hashes.length;
const title = rawTitle.replace(/\s+#*$/, "").trim();
headings.push({
level,
title,
startOffset: offset,
});
}
}
}
offset += line.length + 1;
}
return headings;
}
function stripHeadingSections(
text: string,
shouldStrip: (heading: Heading) => boolean,
): string {
let current = text;
while (true) {
const headings = scanHeadingsOutsideFences(current);
const target = headings.find(shouldStrip);
if (!target) {
return current;
}
const nextHeading = headings.find(
(heading) =>
heading.startOffset > target.startOffset &&
heading.level <= target.level,
);
const end = nextHeading ? nextHeading.startOffset : current.length;
current = `${current.slice(0, target.startOffset)}${current.slice(end)}`;
}
}
function getMemfsTailFragment(): string {
const tailAnchor = "# See what changed";
const start = SYSTEM_PROMPT_MEMFS_ADDON.indexOf(tailAnchor);
if (start === -1) return "";
return SYSTEM_PROMPT_MEMFS_ADDON.slice(start).trim();
}
function stripExactAddon(text: string, addon: string): string {
const trimmedAddon = addon.trim();
if (!trimmedAddon) return text;
let current = text;
while (current.includes(trimmedAddon)) {
current = current.replace(trimmedAddon, "");
}
return current;
}
function stripOrphanMemfsTail(text: string): string {
const tail = getMemfsTailFragment();
if (!tail) return text;
let current = text;
while (current.includes(tail)) {
current = current.replace(tail, "");
}
return current;
}
function compactBlankLines(text: string): string {
return text.replace(/\n{3,}/g, "\n\n").trimEnd();
}
export function stripManagedMemorySections(systemPrompt: string): string {
let current = normalizeNewlines(systemPrompt);
// Strip exact current addons first (fast path).
current = stripExactAddon(current, SYSTEM_PROMPT_MEMORY_ADDON);
current = stripExactAddon(current, SYSTEM_PROMPT_MEMFS_ADDON);
// Strip known orphan fragment produced by the old regex bug.
current = stripOrphanMemfsTail(current);
// Strip legacy/variant memory sections by markdown heading parsing.
current = stripHeadingSections(
current,
(heading) => heading.title === "Memory",
);
current = stripHeadingSections(current, (heading) =>
heading.title.startsWith("Memory Filesystem"),
);
return compactBlankLines(current);
}
export function reconcileMemoryPrompt(
systemPrompt: string,
mode: MemoryPromptMode,
): string {
const base = stripManagedMemorySections(systemPrompt).trimEnd();
const addon =
mode === "memfs"
? SYSTEM_PROMPT_MEMFS_ADDON.trimStart()
: SYSTEM_PROMPT_MEMORY_ADDON.trimStart();
return `${base}\n\n${addon}`.trim();
}
export function detectMemoryPromptDrift(
systemPrompt: string,
expectedMode: MemoryPromptMode,
): MemoryPromptDrift[] {
const prompt = normalizeNewlines(systemPrompt);
const drifts: MemoryPromptDrift[] = [];
const hasLegacyMemoryLanguage = prompt.includes(
"Your memory consists of core memory (composed of memory blocks)",
);
const hasMemfsLanguage =
prompt.includes("## Memory Filesystem") ||
prompt.includes("Your memory is stored in a git repository at");
const hasOrphanFragment =
prompt.includes("# See what changed") &&
prompt.includes("git add system/") &&
prompt.includes('git commit -m "<type>: <what changed>"');
if (expectedMode === "memfs" && hasLegacyMemoryLanguage) {
drifts.push({
code: "legacy_memory_language_with_memfs",
message:
"System prompt contains legacy memory-block language while memfs is enabled.",
});
}
if (expectedMode === "standard" && hasMemfsLanguage) {
drifts.push({
code: "memfs_language_with_standard_mode",
message:
"System prompt contains Memory Filesystem language while memfs is disabled.",
});
}
if (hasOrphanFragment && !hasMemfsLanguage) {
drifts.push({
code: "orphan_memfs_fragment",
message:
"System prompt contains orphaned memfs sync fragment without a full memfs section.",
});
}
return drifts;
}

View File

@@ -343,25 +343,21 @@ export async function updateAgentSystemPrompt(
systemPromptId: string,
): Promise<UpdateSystemPromptResult> {
try {
const { resolveSystemPrompt } = await import("./promptAssets");
const { detectMemoryPromptDrift, reconcileMemoryPrompt } = await import(
"./memoryPrompt"
const { isKnownPreset, resolveAndBuildSystemPrompt } = await import(
"./promptAssets"
);
const { settingsManager } = await import("../settings-manager");
const client = await getClient();
const currentAgent = await client.agents.retrieve(agentId);
const baseContent = await resolveSystemPrompt(systemPromptId);
const settingIndicatesMemfs = settingsManager.isMemfsEnabled(agentId);
const promptIndicatesMemfs = detectMemoryPromptDrift(
currentAgent.system || "",
"standard",
).some((drift) => drift.code === "memfs_language_with_standard_mode");
const memoryMode =
settingIndicatesMemfs || promptIndicatesMemfs ? "memfs" : "standard";
const systemPromptContent = reconcileMemoryPrompt(baseContent, memoryMode);
settingsManager.isReady && settingsManager.isMemfsEnabled(agentId)
? "memfs"
: "standard";
const systemPromptContent = await resolveAndBuildSystemPrompt(
systemPromptId,
memoryMode,
);
const updateResult = await updateAgentSystemPromptRaw(
agentId,
@@ -375,6 +371,15 @@ export async function updateAgentSystemPrompt(
};
}
// Persist preset for known presets; clear stale preset for subagent/unknown
if (settingsManager.isReady) {
if (isKnownPreset(systemPromptId)) {
settingsManager.setSystemPromptPreset(agentId, systemPromptId);
} else {
settingsManager.clearSystemPromptPreset(agentId);
}
}
// Re-fetch agent to get updated state
const agent = await client.agents.retrieve(agentId);
@@ -407,15 +412,26 @@ export async function updateAgentSystemPromptMemfs(
enableMemfs: boolean,
): Promise<SystemPromptUpdateResult> {
try {
const client = await getClient();
const agent = await client.agents.retrieve(agentId);
const { reconcileMemoryPrompt } = await import("./memoryPrompt");
const nextSystemPrompt = reconcileMemoryPrompt(
agent.system || "",
enableMemfs ? "memfs" : "standard",
const { settingsManager } = await import("../settings-manager");
const { isKnownPreset, buildSystemPrompt, swapMemoryAddon } = await import(
"./promptAssets"
);
const newMode = enableMemfs ? "memfs" : "standard";
const storedPreset = settingsManager.isReady
? settingsManager.getSystemPromptPreset(agentId)
: undefined;
let nextSystemPrompt: string;
if (storedPreset && isKnownPreset(storedPreset)) {
nextSystemPrompt = buildSystemPrompt(storedPreset, newMode);
} else {
const client = await getClient();
const agent = await client.agents.retrieve(agentId);
nextSystemPrompt = swapMemoryAddon(agent.system || "", newMode);
}
const client = await getClient();
await client.agents.update(agentId, {
system: nextSystemPrompt,
});

View File

@@ -114,6 +114,140 @@ export const SYSTEM_PROMPTS: SystemPromptOption[] = [
},
];
export type MemoryPromptMode = "standard" | "memfs";
// --- Heading-aware section stripping (for legacy/custom prompts) ---
interface Heading {
level: number;
title: string;
startOffset: number;
}
function scanHeadingsOutsideFences(text: string): Heading[] {
const lines = text.split("\n");
const headings: Heading[] = [];
let inFence = false;
let fenceToken = "";
let offset = 0;
for (const line of lines) {
const trimmed = line.trimStart();
const fenceMatch = trimmed.match(/^(```+|~~~+)/);
if (fenceMatch) {
const token = fenceMatch[1] ?? fenceMatch[0] ?? "";
const tokenChar = token.startsWith("`") ? "`" : "~";
if (!inFence) {
inFence = true;
fenceToken = tokenChar;
} else if (fenceToken === tokenChar) {
inFence = false;
fenceToken = "";
}
}
if (!inFence) {
const headingMatch = line.match(/^\s*(#{1,6})\s+(.+?)\s*$/);
if (headingMatch) {
const hashes = headingMatch[1] ?? "";
const rawTitle = headingMatch[2] ?? "";
if (hashes && rawTitle) {
const level = hashes.length;
const title = rawTitle.replace(/\s+#*$/, "").trim();
headings.push({ level, title, startOffset: offset });
}
}
}
offset += line.length + 1;
}
return headings;
}
function stripHeadingSections(
text: string,
shouldStrip: (heading: Heading) => boolean,
): string {
let current = text;
while (true) {
const headings = scanHeadingsOutsideFences(current);
const target = headings.find(shouldStrip);
if (!target) return current;
const nextHeading = headings.find(
(h) => h.startOffset > target.startOffset && h.level <= target.level,
);
const end = nextHeading ? nextHeading.startOffset : current.length;
current = `${current.slice(0, target.startOffset)}${current.slice(end)}`;
}
}
/**
* Check if a preset ID exists in SYSTEM_PROMPTS.
*/
export function isKnownPreset(id: string): boolean {
return SYSTEM_PROMPTS.some((p) => p.id === id);
}
/**
* Deterministic rebuild of a system prompt from a known preset + memory mode.
* Throws on unknown preset (prevents stale/renamed presets from silently rewriting prompts).
*/
export function buildSystemPrompt(
presetId: string,
memoryMode: MemoryPromptMode,
): string {
const preset = SYSTEM_PROMPTS.find((p) => p.id === presetId);
if (!preset) {
throw new Error(
`Unknown preset "${presetId}" — cannot rebuild system prompt`,
);
}
const addon =
memoryMode === "memfs"
? SYSTEM_PROMPT_MEMFS_ADDON
: SYSTEM_PROMPT_MEMORY_ADDON;
return `${preset.content.trimEnd()}\n\n${addon.trimStart()}`.trim();
}
/**
* Swap the memory addon on a custom/subagent/legacy prompt.
* Strips all existing addons (handles duplicates) and orphan memfs tail fragments,
* then appends the target addon.
*/
export function swapMemoryAddon(
systemPrompt: string,
mode: MemoryPromptMode,
): string {
let result = systemPrompt;
// Strip all existing addons (replaceAll handles duplicates)
for (const addon of [
SYSTEM_PROMPT_MEMORY_ADDON.trim(),
SYSTEM_PROMPT_MEMFS_ADDON.trim(),
]) {
result = result.replaceAll(addon, "");
}
// Strip orphan memfs tail fragment (from old drift bugs)
const tailAnchor = "# See what changed";
const tailStart = SYSTEM_PROMPT_MEMFS_ADDON.indexOf(tailAnchor);
if (tailStart !== -1) {
const orphanTail = SYSTEM_PROMPT_MEMFS_ADDON.slice(tailStart).trim();
result = result.replaceAll(orphanTail, "");
}
// Strip legacy/variant memory sections by markdown heading parsing
// (handles edited or older ## Memory / ## Memory Filesystem sections)
result = stripHeadingSections(result, (h) => h.title === "Memory");
result = stripHeadingSections(result, (h) =>
h.title.startsWith("Memory Filesystem"),
);
// Compact blank lines and append target addon
result = result.replace(/\n{3,}/g, "\n\n").trimEnd();
const target =
mode === "memfs" ? SYSTEM_PROMPT_MEMFS_ADDON : SYSTEM_PROMPT_MEMORY_ADDON;
return `${result}\n\n${target.trimStart()}`.trim();
}
/**
* Validate a system prompt preset ID.
*
@@ -145,6 +279,23 @@ export async function validateSystemPromptPreset(
);
}
/**
* Resolve a prompt ID and build the full system prompt with memory addon.
* Known presets are rebuilt deterministically; unknown IDs (subagent names)
* are resolved async and have the addon swapped in.
*/
export async function resolveAndBuildSystemPrompt(
promptId: string | undefined,
memoryMode: MemoryPromptMode,
): Promise<string> {
const id = promptId ?? "default";
if (isKnownPreset(id)) {
return buildSystemPrompt(id, memoryMode);
}
const resolved = await resolveSystemPrompt(id);
return swapMemoryAddon(resolved, memoryMode);
}
/**
* Resolve a system prompt ID to its content.
*