LLM-driven 设计,添加学习审计和会话历史存储至目录的功能

This commit is contained in:
2026-05-15 11:50:20 +08:00
parent f150c602e5
commit eebf802e31
15 changed files with 1557 additions and 133 deletions
+78 -3
View File
@@ -6,6 +6,7 @@ import {
atomicWriteFileWithHistory,
ensureDirectory,
readTextFile,
toStableId,
} from "../utils/fileStore.js";
export type MemoryScope = "user" | "workspace";
@@ -13,6 +14,7 @@ export type MemoryEntrySource = "review" | "tool";
export type MemoryEntry = {
content: string;
id: string;
};
export type MemoryDraft = {
@@ -64,7 +66,10 @@ export class MemoryStore {
return { changed: false, entry: existing };
}
const entry: MemoryEntry = { content };
const entry: MemoryEntry = {
content,
id: toStableId(scope, key, content.toLowerCase()),
};
entries.unshift(entry);
// 每次覆盖 memory 文件前先保留上一版,写入失败时由底层工具恢复。
await atomicWriteFileWithHistory(
@@ -79,6 +84,62 @@ export class MemoryStore {
});
}
async list(scope: MemoryScope, key: string) {
return await this.readEntries(scope, key);
}
async replace(scope: MemoryScope, key: string, targetId: string, draft: MemoryDraft) {
return this.serializeWrite(async () => {
const content = normalizeMemoryContent(draft.content);
if (!content) {
return { changed: false, detail: "content rejected by persistence policy" };
}
const entries = await this.readEntries(scope, key);
const index = entries.findIndex((entry) => entry.id === targetId.trim());
if (index === -1) {
return { changed: false, detail: "memory entry not found" };
}
const duplicate = entries.find(
(entry, currentIndex) => currentIndex !== index && entry.content === content,
);
if (duplicate) {
return { changed: false, detail: "replacement would duplicate an existing memory" };
}
entries[index] = {
content,
id: entries[index]?.id ?? toStableId(scope, key, content.toLowerCase()),
};
await atomicWriteFileWithHistory(
this.filePath(scope, key),
renderMemoryMarkdown(scope, entries),
{
historyDir: this.historyDir,
rootDir: this.baseDir,
},
);
return { changed: true, detail: "memory replaced" };
});
}
async remove(scope: MemoryScope, key: string, targetId: string) {
return this.serializeWrite(async () => {
const entries = await this.readEntries(scope, key);
const next = entries.filter((entry) => entry.id !== targetId.trim());
if (next.length === entries.length) {
return { changed: false, detail: "memory entry not found" };
}
await atomicWriteFileWithHistory(
this.filePath(scope, key),
renderMemoryMarkdown(scope, next),
{
historyDir: this.historyDir,
rootDir: this.baseDir,
},
);
return { changed: true, detail: "memory removed" };
});
}
async buildPromptSnapshot(context: MemoryContext) {
const [userMemory, workspaceMemory] = await Promise.all([
this.readEntries("user", context.actorKey),
@@ -158,11 +219,25 @@ const parseMemoryMarkdown = (content: string): MemoryEntry[] =>
.split("\n")
.map((line) => line.trim())
.filter((line) => line.startsWith("- "))
.map((line) => ({ content: normalizeMemoryContent(line.slice(2)) }))
.map((line) => line.slice(2).trim())
.map((line) => {
const match = line.match(/^\[([a-z0-9]{8,})\]\s+(.*)$/i);
if (match) {
return {
content: normalizeMemoryContent(match[2]),
id: match[1],
};
}
const normalized = normalizeMemoryContent(line);
return {
content: normalized,
id: normalized ? toStableId("memory-entry", normalized.toLowerCase()) : "",
};
})
.filter((entry) => entry.content);
const renderMemoryMarkdown = (scope: MemoryScope, entries: MemoryEntry[]) => {
const title = scope === "user" ? "# User Memory" : "# Workspace Memory";
const bullets = entries.map((entry) => `- ${entry.content}`);
const bullets = entries.map((entry) => `- [${entry.id}] ${entry.content}`);
return [title, "", ...bullets, ""].join("\n");
};