fix(FN-000): process agent memory dreams
This commit is contained in:
@@ -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 ──────────────────────────────────────────────────────
|
||||
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user