fix(FN-000): process agent memory dreams

This commit is contained in:
gsxdsm
2026-04-17 10:03:46 -07:00
parent d5d5c13781
commit 0515882da1
6 changed files with 402 additions and 36 deletions

View File

@@ -484,15 +484,21 @@ export { MemoryBackendError } from "./memory-backend.js";
export type { MemoryBackendCapabilities, MemoryFileInfo, MemoryGetOptions, MemoryGetResult, MemorySearchOptions, MemorySearchResult } from "./memory-backend.js";
export {
agentDailyMemoryPath,
agentMemoryDreamsPath,
agentMemoryLongTermPath,
agentMemoryWorkspacePath,
buildDreamProcessingPrompt,
createMemoryDreamsAutomation,
DEFAULT_MEMORY_DREAMS_SCHEDULE,
ensureAgentMemoryFiles,
extractDreamProcessorResult,
MEMORY_DREAMS_SCHEDULE_NAME,
processAgentMemoryDreams,
processMemoryDreams,
syncMemoryDreamsAutomation,
} from "./memory-dreams.js";
export type { DreamProcessorResult, DreamPromptExecutor } from "./memory-dreams.js";
export type { AgentDreamProcessorResult, DreamProcessorResult, DreamPromptExecutor } from "./memory-dreams.js";
// ── Project Insights ──────────────────────────────────────────────────────

View File

@@ -1,8 +1,16 @@
import { describe, expect, it, vi } from "vitest";
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import {
agentDailyMemoryPath,
agentMemoryDreamsPath,
agentMemoryLongTermPath,
createMemoryDreamsAutomation,
DEFAULT_MEMORY_DREAMS_SCHEDULE,
ensureAgentMemoryFiles,
MEMORY_DREAMS_SCHEDULE_NAME,
processAgentMemoryDreams,
syncMemoryDreamsAutomation,
} from "./memory-dreams.js";
@@ -16,6 +24,8 @@ describe("memory-dreams automation", () => {
expect(automation.steps![0].id).toBe("memory-dream-processor");
expect(automation.steps![0].prompt).toContain(".fusion/memory/DREAMS.md");
expect(automation.steps![0].prompt).toContain(".fusion/memory/MEMORY.md");
expect(automation.steps![0].prompt).toContain(".fusion/agent-memory/{agentId}/");
expect(automation.steps![0].prompt).toContain("Keep agent memory separate from workspace memory");
});
it("uses custom schedule and model when provided", () => {
@@ -54,4 +64,69 @@ describe("memory-dreams automation", () => {
);
expect(result?.id).toBe("dreams-1");
});
it("creates agent long-term, daily, and dreams memory files", async () => {
const rootDir = await mkdtemp(join(tmpdir(), "agent-dreams-"));
try {
const date = new Date("2026-04-17T12:00:00.000Z");
await ensureAgentMemoryFiles(rootDir, {
id: "ceo-agent",
name: "CEO",
memory: "Prioritize roadmap sequencing.",
} as any, date);
await expect(readFile(agentMemoryLongTermPath(rootDir, "ceo-agent"), "utf-8"))
.resolves.toContain("Prioritize roadmap sequencing");
await expect(readFile(agentMemoryDreamsPath(rootDir, "ceo-agent"), "utf-8"))
.resolves.toContain("Agent Memory Dreams");
await expect(readFile(agentDailyMemoryPath(rootDir, "ceo-agent", date), "utf-8"))
.resolves.toContain("Agent Daily Memory 2026-04-17");
} finally {
await rm(rootDir, { recursive: true, force: true });
}
});
it("processes agent daily memory into agent dreams and long-term updates", async () => {
const rootDir = await mkdtemp(join(tmpdir(), "agent-dreams-process-"));
try {
const date = new Date("2026-04-17T12:00:00.000Z");
const agent = {
id: "ceo-agent",
name: "CEO",
role: "executor",
state: "idle",
memory: "Existing CEO preference.",
metadata: {},
createdAt: date.toISOString(),
updatedAt: date.toISOString(),
} as any;
await ensureAgentMemoryFiles(rootDir, agent, date);
await writeFile(
agentDailyMemoryPath(rootDir, "ceo-agent", date),
"# Agent Daily Memory 2026-04-17\n\n- CEO should delegate implementation after sequencing.",
"utf-8",
);
const result = await processAgentMemoryDreams(rootDir, [agent], async (prompt) => {
expect(prompt).toContain("private memory for agent CEO");
expect(prompt).toContain("delegate implementation");
return "## DREAMS\n\nDelegation after sequencing is recurring.\n\n## LONG_TERM_UPDATES\n\n- Delegate implementation after roadmap sequencing.";
}, date);
expect(result).toEqual([{
agentId: "ceo-agent",
dreams: "Delegation after sequencing is recurring.",
longTermUpdates: "- Delegate implementation after roadmap sequencing.",
}]);
await expect(readFile(agentMemoryDreamsPath(rootDir, "ceo-agent"), "utf-8"))
.resolves.toContain("Delegation after sequencing is recurring");
await expect(readFile(agentMemoryLongTermPath(rootDir, "ceo-agent"), "utf-8"))
.resolves.toContain("Delegate implementation after roadmap sequencing");
await expect(readFile(agentDailyMemoryPath(rootDir, "ceo-agent", date), "utf-8"))
.resolves.toContain("Processed into dreams");
} finally {
await rm(rootDir, { recursive: true, force: true });
}
});
});

View File

@@ -1,5 +1,6 @@
import { appendFile, readFile, writeFile } from "node:fs/promises";
import { appendFile, mkdir, readFile, readdir, stat, writeFile } from "node:fs/promises";
import { existsSync } from "node:fs";
import { join } from "node:path";
import {
dailyMemoryPath,
ensureOpenClawMemoryFiles,
@@ -7,7 +8,8 @@ import {
memoryLongTermPath,
} from "./memory-backend.js";
import type { ScheduledTaskCreateInput } from "./automation.js";
import type { ProjectSettings } from "./types.js";
import type { Agent, ProjectSettings } from "./types.js";
import { isEphemeralAgent } from "./types.js";
export const MEMORY_DREAMS_SCHEDULE_NAME = "Memory Dreams";
export const DEFAULT_MEMORY_DREAMS_SCHEDULE = "0 4 * * *";
@@ -17,8 +19,59 @@ export interface DreamProcessorResult {
longTermUpdates: string;
}
export interface AgentDreamProcessorResult extends DreamProcessorResult {
agentId: string;
}
export type DreamPromptExecutor = (prompt: string) => Promise<string>;
const AGENT_MEMORY_ROOT = ".fusion/agent-memory";
const AGENT_MEMORY_FILENAME = "MEMORY.md";
const AGENT_DREAMS_FILENAME = "DREAMS.md";
const DAILY_AGENT_MEMORY_RE = /^\d{4}-\d{2}-\d{2}\.md$/;
export function agentMemoryWorkspacePath(rootDir: string, agentId: string): string {
const safeAgentId = agentId.trim().replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "") || "agent";
return join(rootDir, AGENT_MEMORY_ROOT, safeAgentId);
}
export function agentMemoryLongTermPath(rootDir: string, agentId: string): string {
return join(agentMemoryWorkspacePath(rootDir, agentId), AGENT_MEMORY_FILENAME);
}
export function agentMemoryDreamsPath(rootDir: string, agentId: string): string {
return join(agentMemoryWorkspacePath(rootDir, agentId), AGENT_DREAMS_FILENAME);
}
export function agentDailyMemoryPath(rootDir: string, agentId: string, date = new Date()): string {
return join(agentMemoryWorkspacePath(rootDir, agentId), `${date.toISOString().slice(0, 10)}.md`);
}
export async function ensureAgentMemoryFiles(rootDir: string, agent: Pick<Agent, "id" | "name" | "memory">, date = new Date()): Promise<void> {
const workspacePath = agentMemoryWorkspacePath(rootDir, agent.id);
await mkdir(workspacePath, { recursive: true });
const longTermPath = agentMemoryLongTermPath(rootDir, agent.id);
if (!existsSync(longTermPath)) {
const title = agent.name?.trim() ? `# Agent Memory: ${agent.name.trim()}` : "# Agent Memory";
await writeFile(
longTermPath,
`${title}\n\n<!-- Per-agent memory. Keep separate from workspace Project Memory. -->\n\n${agent.memory?.trim() ?? ""}\n`,
"utf-8",
);
}
const dreamsPath = agentMemoryDreamsPath(rootDir, agent.id);
if (!existsSync(dreamsPath)) {
await writeFile(dreamsPath, "# Agent Memory Dreams\n\n<!-- Synthesized patterns from this agent's daily notes. -->\n", "utf-8");
}
const dailyPath = agentDailyMemoryPath(rootDir, agent.id, date);
if (!existsSync(dailyPath)) {
await writeFile(dailyPath, `# Agent Daily Memory ${date.toISOString().slice(0, 10)}\n\n<!-- Running observations for this agent. -->\n`, "utf-8");
}
}
export function buildDreamProcessingPrompt(input: {
date: string;
longTermMemory: string;
@@ -104,6 +157,70 @@ export async function processMemoryDreams(
return result;
}
async function readAgentDailyNotes(rootDir: string, agentId: string, date: Date): Promise<string> {
const workspacePath = agentMemoryWorkspacePath(rootDir, agentId);
const dateKey = date.toISOString().slice(0, 10);
const dailyPath = agentDailyMemoryPath(rootDir, agentId, date);
if (existsSync(dailyPath)) {
return readFile(dailyPath, "utf-8");
}
const files = await readdir(workspacePath).catch(() => [] as string[]);
const chunks: string[] = [];
for (const file of files) {
if (!DAILY_AGENT_MEMORY_RE.test(file)) continue;
if (!file.startsWith(dateKey)) continue;
const absPath = join(workspacePath, file);
if ((await stat(absPath)).isFile()) {
chunks.push(await readFile(absPath, "utf-8"));
}
}
return chunks.join("\n\n");
}
export async function processAgentMemoryDreams(
rootDir: string,
agents: Agent[],
executePrompt: DreamPromptExecutor,
date = new Date(),
): Promise<AgentDreamProcessorResult[]> {
const dateKey = date.toISOString().slice(0, 10);
const results: AgentDreamProcessorResult[] = [];
for (const agent of agents) {
if (isEphemeralAgent(agent)) {
continue;
}
await ensureAgentMemoryFiles(rootDir, agent, date);
const longTermPath = agentMemoryLongTermPath(rootDir, agent.id);
const dreamsPath = agentMemoryDreamsPath(rootDir, agent.id);
const dailyPath = agentDailyMemoryPath(rootDir, agent.id, date);
const prompt = buildDreamProcessingPrompt({
date: dateKey,
longTermMemory: await readIfExists(longTermPath),
previousDreams: await readIfExists(dreamsPath),
dailyMemory: await readAgentDailyNotes(rootDir, agent.id, date),
}).replace(
"You are processing project memory in an OpenClaw-style memory system.",
`You are processing private memory for agent ${agent.name} (${agent.id}) in an OpenClaw-style memory system.`,
);
const result = extractDreamProcessorResult(await executePrompt(prompt));
if (result.dreams) {
await appendFile(dreamsPath, `\n## ${dateKey}\n\n${result.dreams}\n`, "utf-8");
}
if (result.longTermUpdates) {
await appendFile(longTermPath, `\n## Dream Updates ${dateKey}\n\n${result.longTermUpdates}\n`, "utf-8");
}
await writeFile(dailyPath, `# Agent Daily Memory ${dateKey}\n\n<!-- Processed into dreams on ${new Date().toISOString()} -->\n`, "utf-8");
results.push({ agentId: agent.id, ...result });
}
return results;
}
export function createMemoryDreamsAutomation(
settings: Partial<ProjectSettings>,
modelProvider?: string,
@@ -120,13 +237,20 @@ export function createMemoryDreamsAutomation(
4. Append a dated synthesis to \`.fusion/memory/DREAMS.md\` with patterns, open loops, contradictions, and emerging themes.
5. Append only durable conventions, decisions, pitfalls, or constraints to \`.fusion/memory/MEMORY.md\`.
6. Reset today's daily note to a short processed marker after successful synthesis.
7. For every persisted non-ephemeral agent in \`.fusion/agents/*.json\`, repeat the same process for that agent's private memory workspace at \`.fusion/agent-memory/{agentId}/\`:
- Read \`MEMORY.md\`, \`DREAMS.md\`, and today's \`YYYY-MM-DD.md\`.
- If the agent workspace is missing, create it and seed \`MEMORY.md\` from the agent JSON \`memory\` field when present.
- Append agent-specific synthesis to that agent's \`DREAMS.md\`.
- Promote only durable agent-specific operating preferences, habits, or constraints to that agent's \`MEMORY.md\`.
- Reset that agent's daily note after successful synthesis.
## Rules
- Do not copy task logs or changelog entries into long-term memory.
- Do not invent facts.
- Keep dreams useful for future agents, not a transcript of the day.
- Preserve the three-layer model: daily notes are raw, DREAMS.md is synthesis, MEMORY.md is curated durable knowledge.`;
- Preserve the three-layer model for both workspace and agent memory: daily notes are raw, DREAMS.md is synthesis, MEMORY.md is curated durable knowledge.
- Keep agent memory separate from workspace memory. Do not promote private agent operating notes into project memory unless they are useful to every agent in the workspace.`;
return {
name: MEMORY_DREAMS_SCHEDULE_NAME,