Files
EJClaw/runners/agent-runner/src/memory-selection.ts
2026-04-11 04:47:16 +09:00

92 lines
2.2 KiB
TypeScript

export interface SelectedCompactMemory {
content: string;
keywords: string[];
memoryKind: string | null;
}
const MAX_SELECTED_MEMORIES = 3;
const MEMORY_PATTERNS: Array<{
kind: string | null;
regex: RegExp;
keyword: string;
}> = [
{
kind: 'room_norm',
regex:
/(규칙|원칙|금지|반드시|하지 않|세션 시작 시에만|새 세션 시작 시에만)/i,
keyword: 'rule',
},
{
kind: 'preference',
regex: /(선호|원함|원한다|원하는|원했다)/i,
keyword: 'preference',
},
{
kind: 'decision',
regex: /(합의|결정|방향|기준|우선|책임지는 방향)/i,
keyword: 'decision',
},
{
kind: 'project_fact',
regex:
/(owner|reviewer|trigger|모드|메모리|기억|세션 리셋|recall|persist|compact)/i,
keyword: 'memory',
},
];
function normalizeSentence(raw: string): string | null {
const normalized = raw
.trim()
.replace(/^[-*]\s*/, '')
.replace(/\s+/g, ' ');
return normalized ? normalized : null;
}
function splitSummaryIntoSentences(summary: string): string[] {
return summary
.split(/\n+|(?<=[.!?。])\s+/)
.map(normalizeSentence)
.filter((value): value is string => Boolean(value));
}
function classifyMemorySentence(content: string): SelectedCompactMemory | null {
if (!content) return null;
const matchedPatterns = MEMORY_PATTERNS.filter(({ regex }) =>
regex.test(content),
);
if (matchedPatterns.length === 0) return null;
const primary = matchedPatterns[0];
const keywords = [
...new Set(
matchedPatterns.map((pattern) => pattern.keyword).filter(Boolean),
),
];
return {
content,
keywords,
memoryKind: primary?.kind ?? null,
};
}
export function selectCompactMemoriesFromSummary(
summary: string,
roomKey: string,
): SelectedCompactMemory[] {
if (!summary.trim()) return [];
const selected = splitSummaryIntoSentences(summary)
.map((sentence) => classifyMemorySentence(sentence))
.filter((value): value is SelectedCompactMemory => Boolean(value))
.slice(0, MAX_SELECTED_MEMORIES)
.map((entry) => ({
...entry,
keywords: [...new Set([roomKey, ...entry.keywords])],
}));
return selected;
}