fix(reflection): given reflection agents the parent memory (#1376)

Co-authored-by: Letta Code <noreply@letta.com>
This commit is contained in:
Sarah Wooders
2026-03-15 11:12:38 -07:00
committed by GitHub
parent 3015450120
commit c60363a25d
3 changed files with 273 additions and 7 deletions

View File

@@ -260,6 +260,7 @@ import { resolveReasoningTabToggleCommand } from "./helpers/reasoningTabToggle";
import {
appendTranscriptDeltaJsonl,
buildAutoReflectionPayload,
buildParentMemorySnapshot,
buildReflectionSubagentPrompt,
finalizeAutoReflectionPayload,
} from "./helpers/reflectionTranscript";
@@ -9435,10 +9436,12 @@ export default function App({
}
const memoryDir = getMemoryFilesystemRoot(agentId);
const parentMemory = await buildParentMemorySnapshot(memoryDir);
const reflectionPrompt = buildReflectionSubagentPrompt({
transcriptPath: autoPayload.payloadPath,
memoryDir,
cwd: process.cwd(),
parentMemory,
});
const { spawnBackgroundSubagentTask } = await import(
@@ -9799,10 +9802,12 @@ ${SYSTEM_REMINDER_CLOSE}
}
const memoryDir = getMemoryFilesystemRoot(agentId);
const parentMemory = await buildParentMemorySnapshot(memoryDir);
const reflectionPrompt = buildReflectionSubagentPrompt({
transcriptPath: autoPayload.payloadPath,
memoryDir,
cwd: process.cwd(),
parentMemory,
});
const { spawnBackgroundSubagentTask } = await import(

View File

@@ -1,6 +1,15 @@
import { appendFile, mkdir, readFile, writeFile } from "node:fs/promises";
import type { Dirent } from "node:fs";
import {
appendFile,
mkdir,
readdir,
readFile,
writeFile,
} from "node:fs/promises";
import { homedir, tmpdir } from "node:os";
import { join } from "node:path";
import { MEMORY_SYSTEM_DIR } from "../../agent/memoryFilesystem";
import { parseFrontmatter } from "../../utils/frontmatter";
import { type Line, linesToTranscript } from "./accumulator";
const TRANSCRIPT_ROOT_ENV = "LETTA_TRANSCRIPT_ROOT";
@@ -43,19 +52,198 @@ export interface ReflectionPromptInput {
transcriptPath: string;
memoryDir: string;
cwd?: string;
parentMemory?: string;
}
export function buildReflectionSubagentPrompt(
input: ReflectionPromptInput,
): string {
const lines = [
"Review the conversation transcript and update memory files.",
`The current conversation transcript has been saved to: ${input.transcriptPath}`,
`The primary agent's memory filesystem is located at: ${input.memoryDir}`,
];
const lines: string[] = [];
if (input.cwd) {
lines.push(`Your current working directory is: ${input.cwd}`);
lines.push("");
}
lines.push(
`Review the conversation transcript and update memory files. The current conversation transcript has been saved to: ${input.transcriptPath}`,
"",
`The primary agent's memory filesystem is located at: ${input.memoryDir}`,
"In-context memory (in the parent agent's system prompt) is stored in the `system/` folder and are rendered in <memory> tags below. Modification to files in `system/` will edit the parent agent's system prompt.",
"Additional memory files (such as skills and external memory) may also be read and modified.",
"",
);
if (input.parentMemory) {
lines.push(input.parentMemory);
}
return lines.join("\n");
}
interface ParentMemoryFile {
relativePath: string;
content: string;
description?: string;
}
function isSystemMemoryFile(relativePath: string): boolean {
return relativePath.startsWith(`${MEMORY_SYSTEM_DIR}/`);
}
async function collectParentMemoryFiles(
memoryDir: string,
): Promise<ParentMemoryFile[]> {
const files: ParentMemoryFile[] = [];
const walk = async (currentDir: string, relativeDir: string) => {
let entries: Dirent[] = [];
try {
entries = await readdir(currentDir, { withFileTypes: true });
} catch {
return;
}
const sortedEntries = entries
.filter((entry) => !entry.name.startsWith("."))
.sort((a, b) => {
if (a.isDirectory() !== b.isDirectory()) {
return a.isDirectory() ? -1 : 1;
}
return a.name.localeCompare(b.name);
});
for (const entry of sortedEntries) {
const entryPath = join(currentDir, entry.name);
const relativePath = relativeDir
? `${relativeDir}/${entry.name}`
: entry.name;
if (entry.isDirectory()) {
await walk(entryPath, relativePath);
continue;
}
if (!entry.isFile() || !entry.name.endsWith(".md")) {
continue;
}
try {
const content = await readFile(entryPath, "utf-8");
const { frontmatter } = parseFrontmatter(content);
const description =
typeof frontmatter.description === "string"
? frontmatter.description
: undefined;
files.push({
relativePath: relativePath.replace(/\\/g, "/"),
content,
description,
});
} catch {
// Skip unreadable files.
}
}
};
await walk(memoryDir, "");
return files;
}
function buildParentMemoryTree(files: ParentMemoryFile[]): string {
type TreeNode = {
children: Map<string, TreeNode>;
isFile: boolean;
description?: string;
};
const makeNode = (): TreeNode => ({ children: new Map(), isFile: false });
const root = makeNode();
for (const file of files) {
const normalizedPath = file.relativePath.replace(/\\/g, "/");
const parts = normalizedPath.split("/");
let current = root;
for (const [index, part] of parts.entries()) {
if (!current.children.has(part)) {
current.children.set(part, makeNode());
}
current = current.children.get(part) as TreeNode;
if (index === parts.length - 1) {
current.isFile = true;
if (file.description && !isSystemMemoryFile(normalizedPath)) {
current.description = file.description;
}
}
}
}
if (!root.children.has(MEMORY_SYSTEM_DIR)) {
root.children.set(MEMORY_SYSTEM_DIR, makeNode());
}
const sortedEntries = (node: TreeNode) =>
Array.from(node.children.entries()).sort(
([nameA, nodeA], [nameB, nodeB]) => {
if (nodeA.isFile !== nodeB.isFile) {
return nodeA.isFile ? 1 : -1;
}
return nameA.localeCompare(nameB);
},
);
const lines: string[] = ["/memory/"];
const render = (node: TreeNode, prefix: string) => {
const entries = sortedEntries(node);
for (const [index, [name, child]] of entries.entries()) {
const isLast = index === entries.length - 1;
const branch = isLast ? "└──" : "├──";
const suffix = child.isFile ? "" : "/";
const description = child.description ? ` (${child.description})` : "";
lines.push(`${prefix}${branch} ${name}${suffix}${description}`);
if (child.children.size > 0) {
const nextPrefix = `${prefix}${isLast ? " " : "│ "}`;
render(child, nextPrefix);
}
}
};
render(root, "");
return lines.join("\n");
}
export async function buildParentMemorySnapshot(
memoryDir: string,
): Promise<string> {
const files = await collectParentMemoryFiles(memoryDir);
const tree = buildParentMemoryTree(files);
const systemFiles = files.filter((file) =>
isSystemMemoryFile(file.relativePath),
);
const lines = [
"<parent_memory>",
"<memory_filesystem>",
tree,
"</memory_filesystem>",
];
if (files.length === 0) {
lines.push("(no memory markdown files found)");
} else {
for (const file of systemFiles) {
const normalizedPath = file.relativePath.replace(/\\/g, "/");
const absolutePath = `${memoryDir.replace(/\\/g, "/")}/${normalizedPath}`;
lines.push("<memory>");
lines.push(`<path>${absolutePath}</path>`);
lines.push(file.content);
lines.push("</memory>");
}
}
lines.push("</parent_memory>");
return lines.join("\n");
}
@@ -272,6 +460,7 @@ export async function buildAutoReflectionPayload(
}
const snapshotLines = lines.slice(cursorLine);
const entries = snapshotLines
.map((line) => parseJsonLine<TranscriptEntry>(line))
.filter((entry): entry is TranscriptEntry => entry !== null);

View File

@@ -1,11 +1,13 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { existsSync } from "node:fs";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
appendTranscriptDeltaJsonl,
buildAutoReflectionPayload,
buildParentMemorySnapshot,
buildReflectionSubagentPrompt,
finalizeAutoReflectionPayload,
getReflectionTranscriptPaths,
} from "../../cli/helpers/reflectionTranscript";
@@ -128,4 +130,74 @@ describe("reflectionTranscript helper", () => {
const payloadText = await readFile(secondAttempt.payloadPath, "utf-8");
expect(payloadText).toContain("<assistant>second</assistant>");
});
test("buildParentMemorySnapshot renders tree descriptions and system <memory> blocks", async () => {
const memoryDir = join(testRoot, "memory");
await mkdir(join(memoryDir, "system"), { recursive: true });
await mkdir(join(memoryDir, "reference"), { recursive: true });
await mkdir(join(memoryDir, "skills", "bird"), { recursive: true });
await writeFile(
join(memoryDir, "system", "human.md"),
"---\ndescription: User context\n---\nDr. Wooders prefers direct answers.\n",
"utf-8",
);
await writeFile(
join(memoryDir, "reference", "project.md"),
"---\ndescription: Project notes\n---\nletta-code CLI details\n",
"utf-8",
);
await writeFile(
join(memoryDir, "skills", "bird", "SKILL.md"),
"---\nname: bird\ndescription: X/Twitter CLI for posting\n---\nThis body should not be inlined into parent memory.\n",
"utf-8",
);
const snapshot = await buildParentMemorySnapshot(memoryDir);
expect(snapshot).toContain("<parent_memory>");
expect(snapshot).toContain("<memory_filesystem>");
expect(snapshot).toContain("/memory/");
expect(snapshot).toContain("system/");
expect(snapshot).toContain("reference/");
expect(snapshot).toContain("skills/");
expect(snapshot).toContain("project.md (Project notes)");
expect(snapshot).toContain("SKILL.md (X/Twitter CLI for posting)");
expect(snapshot).toContain("<memory>");
expect(snapshot).toContain(`<path>${memoryDir}/system/human.md</path>`);
expect(snapshot).toContain("Dr. Wooders prefers direct answers.");
expect(snapshot).toContain("</memory>");
expect(snapshot).not.toContain(
`<path>${memoryDir}/reference/project.md</path>`,
);
expect(snapshot).not.toContain("letta-code CLI details");
expect(snapshot).not.toContain(
"This body should not be inlined into parent memory.",
);
expect(snapshot).toContain("</parent_memory>");
});
test("buildReflectionSubagentPrompt uses expanded reflection instructions", () => {
const prompt = buildReflectionSubagentPrompt({
transcriptPath: "/tmp/transcript.txt",
memoryDir: "/tmp/memory",
cwd: "/tmp/work",
parentMemory: "<parent_memory>snapshot</parent_memory>",
});
expect(prompt).toContain("Review the conversation transcript");
expect(prompt).toContain("Your current working directory is: /tmp/work");
expect(prompt).toContain(
"The current conversation transcript has been saved",
);
expect(prompt).toContain(
"In-context memory (in the parent agent's system prompt) is stored in the `system/` folder and are rendered in <memory> tags below.",
);
expect(prompt).toContain(
"Additional memory files (such as skills and external memory) may also be read and modified.",
);
expect(prompt).toContain("<parent_memory>snapshot</parent_memory>");
});
});