更新memory读取机制,新增前需要先list阅读已有的内容

This commit is contained in:
2026-06-04 15:35:01 +08:00
parent 0188240d62
commit 8a1785c244
7 changed files with 67 additions and 129 deletions
+34 -1
View File
@@ -240,9 +240,10 @@ export class LearningOrchestrator {
traceId: input.requestContext.traceId,
});
try {
const existingMemory = await this.loadMemoryContext(input.requestContext);
await this.runtime.prompt(
reviewSession.id,
buildReviewPrompt({ focus, recentTurns }),
buildReviewPrompt({ existingMemory, focus, recentTurns }),
toRuntimeModel(input.model),
);
const messages = await this.runtime.messages(reviewSession.id, 20);
@@ -312,6 +313,14 @@ export class LearningOrchestrator {
});
}
private async loadMemoryContext(context: ChatRequestContext) {
const [userMemory, workspaceMemory] = await Promise.all([
this.memoryStore.list("user", context.actorKey),
this.memoryStore.list("workspace", context.projectKey),
]);
return { userMemory, workspaceMemory };
}
private async applyMemoryProposal(
input: TurnReviewInput,
proposal: ReviewResult["memories"][number],
@@ -440,9 +449,14 @@ const buildGatePrompt = ({ recentTurns }: { recentTurns: SessionTurnRecord[] })
};
const buildReviewPrompt = ({
existingMemory,
focus,
recentTurns,
}: {
existingMemory: {
userMemory: Array<{ content: string; id: string }>;
workspaceMemory: Array<{ content: string; id: string }>;
};
focus: GateResult["focus"];
recentTurns: SessionTurnRecord[];
}) => {
@@ -452,6 +466,16 @@ const buildReviewPrompt = ({
`Turn ${index + 1}\nUser: ${turn.userMessage}\nAssistant: ${turn.assistantMessage}\nTool calls: ${turn.toolCallCount}`,
)
.join("\n\n");
const userMemoryBlock =
existingMemory.userMemory.length > 0
? existingMemory.userMemory.map((entry) => `- [${entry.id}] ${entry.content}`).join("\n")
: "(empty)";
const workspaceMemoryBlock =
existingMemory.workspaceMemory.length > 0
? existingMemory.workspaceMemory
.map((entry) => `- [${entry.id}] ${entry.content}`)
.join("\n")
: "(empty)";
return [
"You are doing an internal self-improvement review for TJWaterAgent.",
"Do NOT call any tools. Return JSON only. Do NOT wrap in markdown fences.",
@@ -462,8 +486,17 @@ const buildReviewPrompt = ({
"- Keep only stable user preferences, durable constraints, or stable workspace facts.",
"- Use scope='user' for user preferences and constraints.",
"- Use scope='workspace' for project or environment facts.",
"- Read the existing memories first before proposing changes.",
"- If a new lesson overlaps, refines, or supersedes an existing memory, prefer replace/remove using target_id instead of add.",
"- Use add only when the lesson is genuinely new after reviewing the existing memory list.",
"- Do not store one-off task outcomes, temporary facts, or speculative conclusions.",
"",
"Current persisted memories:",
"[User memory]",
userMemoryBlock,
"[Workspace memory]",
workspaceMemoryBlock,
"",
"Skill rules:",
"- Save only reusable workflows, methods, or pitfalls that will help in future similar tasks.",
"- Prefer append_pattern for concise reusable lessons.",
-105
View File
@@ -61,7 +61,6 @@ export class MemoryStore {
changed: false,
detail: "content rejected by persistence policy",
entry: null as MemoryEntry | null,
similar: null as MemoryEntry | null,
};
}
@@ -72,17 +71,6 @@ export class MemoryStore {
changed: false,
detail: "memory already existed",
entry: existing,
similar: existing,
};
}
const similar = findSimilarMemory(entries, content);
if (similar) {
return {
changed: false,
detail: "similar memory already exists",
entry: similar,
similar,
};
}
@@ -104,7 +92,6 @@ export class MemoryStore {
changed: true,
detail: "memory stored",
entry,
similar: null as MemoryEntry | null,
};
});
}
@@ -130,13 +117,6 @@ export class MemoryStore {
if (duplicate) {
return { changed: false, detail: "replacement would duplicate an existing memory" };
}
const similar = findSimilarMemory(entries, content, entries[index]?.id);
if (similar) {
return {
changed: false,
detail: "replacement would overlap with a similar existing memory",
};
}
entries[index] = {
content,
id: entries[index]?.id ?? toStableId(scope, key, content.toLowerCase()),
@@ -246,91 +226,6 @@ const normalizeMemoryContent = (content: string) => {
return normalized;
};
const findSimilarMemory = (
entries: MemoryEntry[],
content: string,
excludeId?: string,
) =>
entries.find(
(entry) => entry.id !== excludeId && areSimilarMemoryContents(entry.content, content),
) ?? null;
const areSimilarMemoryContents = (left: string, right: string) => {
const normalizedLeft = normalizeComparableMemory(left);
const normalizedRight = normalizeComparableMemory(right);
if (!normalizedLeft || !normalizedRight) {
return false;
}
if (normalizedLeft === normalizedRight) {
return true;
}
const [shorter, longer] =
normalizedLeft.length <= normalizedRight.length
? [normalizedLeft, normalizedRight]
: [normalizedRight, normalizedLeft];
if (shorter.length >= 12 && longer.includes(shorter)) {
return true;
}
if (shorter.length < 8) {
return false;
}
if (
longestCommonSubsequenceLength(normalizedLeft, normalizedRight) / shorter.length >= 0.5
) {
return true;
}
return (
diceCoefficient(buildCharacterBigrams(normalizedLeft), buildCharacterBigrams(normalizedRight)) >=
0.72
);
};
const normalizeComparableMemory = (content: string) =>
normalizeMemoryContent(content)
.toLowerCase()
.replace(/[^\p{L}\p{N}]+/gu, "");
const buildCharacterBigrams = (content: string) => {
const grams = new Set<string>();
for (let index = 0; index < content.length - 1; index += 1) {
grams.add(content.slice(index, index + 2));
}
return grams;
};
const diceCoefficient = (left: Set<string>, right: Set<string>) => {
if (left.size === 0 || right.size === 0) {
return 0;
}
let overlap = 0;
for (const item of left) {
if (right.has(item)) {
overlap += 1;
}
}
return (2 * overlap) / (left.size + right.size);
};
const longestCommonSubsequenceLength = (left: string, right: string) => {
const previous = new Array(right.length + 1).fill(0);
const current = new Array(right.length + 1).fill(0);
for (let leftIndex = 1; leftIndex <= left.length; leftIndex += 1) {
for (let rightIndex = 1; rightIndex <= right.length; rightIndex += 1) {
current[rightIndex] =
left[leftIndex - 1] === right[rightIndex - 1]
? previous[rightIndex - 1] + 1
: Math.max(previous[rightIndex], current[rightIndex - 1]);
}
for (let rightIndex = 0; rightIndex <= right.length; rightIndex += 1) {
previous[rightIndex] = current[rightIndex];
current[rightIndex] = 0;
}
}
return previous[right.length];
};
const parseMemoryMarkdown = (content: string): MemoryEntry[] =>
content
.split("\n")
+1
View File
@@ -14,6 +14,7 @@ export type SessionRuntimeContext = {
allowLearningWrite?: boolean;
clientSessionId: string;
learningMode?: "interactive" | "review";
memoryListReadScopes?: Partial<Record<"user" | "workspace", boolean>>;
projectId?: string;
projectKey: string;
sessionId: string;