Files
homeclaw/src/tools/memory.ts
T
kimandClaude Sonnet 5 05976e3f05 v4.3.6: gateway 루트에 남아있던 나머지 20개 파일도 서브디렉토리로 분류
- memory/ (3): fact-store, memory-manager, memory-vector
- hooks/ (2): hooks, hook-loader
- infra/ (4): boot, gpu-detector, ollama-process-manager, mcp-manager
- guards/ (6): link-validator, visual-error-detection, context-injection,
  prompt-gates, retry-strategy, verification-flow
- internal-agent-task.ts(실제 express 라우터), music-renderer.ts(routes-music.ts
  전용 백엔드)는 routes/로 편입
- server.ts/session.ts/skills-manager.ts는 5곳 안팎에서 두루 참조되는 핵심
  모듈이라 루트 유지, orchestrator.ts(export {}뿐인 죽은 레거시 껍데기)도
  분류 대상이 아니라 별도 이슈로 루트에 남김
- 이번에도 import 경로 자동 재계산 스크립트 사용, 41곳 수정 후 빌드/재시작/
  20개 엔드포인트 재검증 완료

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-25 17:10:44 +09:00

141 lines
5.5 KiB
TypeScript

import { ToolResult } from '../types.js';
import { loadMemory, updateMemory } from '../config/soul-loader.js';
import { queryFactRecords } from '../gateway/memory/fact-store.js';
import { sanitizeMemoryText } from './memory-utils.js';
// MEMORY_WRITE: model appends or replaces a bullet in memory.md
export async function executeMemoryWrite(args: { fact: string; action?: 'append' | 'replace_all' | 'upsert'; key?: string; reference?: string; source_tool?: string; source_output?: string; actor?: 'agent' | 'user' | 'system' }): Promise<ToolResult> {
if (!args.fact?.trim()) return { success: false, error: 'fact is required' };
const action = args.action ?? 'append';
try {
const fact = sanitizeMemoryText(args.fact.trim());
const actor = args.actor || 'agent';
const reference = args.reference ? sanitizeMemoryText(args.reference) : undefined;
const source_tool = args.source_tool ? sanitizeMemoryText(args.source_tool) : undefined;
const source_output = args.source_output ? sanitizeMemoryText(args.source_output) : undefined;
const key = args.key ? sanitizeMemoryText(args.key) : undefined;
// Build bullet with metadata
const metaParts: string[] = [];
metaParts.push(`[${actor}]`);
if (key) metaParts.push(`[key=${key}]`);
if (reference) metaParts.push(`[ref=${reference}]`);
if (source_tool) metaParts.push(`[src=${source_tool}]`);
const meta = metaParts.join('');
const bullet = `- ${meta} ${fact}`;
if (action === 'replace_all') {
updateMemory(`# Memory\n\n${bullet}\n`);
} else if (action === 'upsert') {
const current = loadMemory();
const lines = current ? current.split(/\r?\n/) : [];
const hasHeader = lines.some(l => /^#\s*memory\b/i.test(l.trim()));
const keyPattern = key ? new RegExp(`\\[key=${key.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\]`) : null;
const filtered = lines.filter(line => {
const t = line.trim();
if (!t) return true;
if (/^#\s*memory\b/i.test(t)) return true;
if (!t.startsWith('-')) return true;
if (keyPattern && keyPattern.test(t)) return false;
return true;
});
const out = [];
if (hasHeader) out.push(...filtered);
else out.push('# Memory', '', ...filtered.filter(l => l.trim() !== '# Memory'));
if (out.length > 0 && out[out.length - 1].trim() !== '') out.push('');
out.push(bullet);
out.push('');
updateMemory(out.join('\n'));
} else {
const current = loadMemory();
// Remove placeholder line if present
const cleaned = current.replace(/- First run: no facts stored yet\.|\n?/, '').trim();
const bullets = cleaned ? `${cleaned}\n${bullet}\n` : `# Memory\n\n${bullet}\n`;
updateMemory(bullets);
}
return { success: true, stdout: `Memory updated: ${fact}` };
} catch (err: any) {
return { success: false, error: `Memory write failed: ${err.message}` };
}
}
export const memoryWriteTool = {
name: 'memory_write',
description: 'Persist a fact to long-term memory (survives restarts)',
execute: executeMemoryWrite,
schema: {
fact: 'string (required) - The fact to remember (e.g. "User prefers Python 3.12")',
action: 'string (optional) - "append" (default) adds a new bullet, "upsert" replaces bullet with same key, "replace_all" clears and rewrites',
key: 'string (optional) - unique key for upsert (e.g., "fact:us-attorney-general")',
reference: 'string (optional) - job id or session reference to associate with this fact',
source_tool: 'string (optional) - tool that produced this fact (e.g., web_search)',
source_output: 'string (optional) - raw tool output or snippet',
actor: 'string (optional) - who added the fact: agent|user|system'
},
};
// MEMORY_SEARCH: semantic lookup over typed memory facts
export async function executeMemorySearch(args: { query: string; session_id?: string; max?: number }): Promise<ToolResult> {
const query = String(args?.query || '').trim();
if (!query) return { success: false, error: 'query is required' };
try {
const sessionId = String(args?.session_id || '').trim() || undefined;
const maxRaw = Number(args?.max ?? 5);
const max = Number.isFinite(maxRaw) ? Math.min(Math.max(Math.floor(maxRaw), 1), 25) : 5;
const matches = queryFactRecords({
query,
session_id: sessionId,
includeGlobal: true,
max,
includeStale: false,
});
const results = matches.map((m) => ({
key: m.key,
value: m.value,
scope: m.scope,
session_id: m.session_id,
type: m.type,
confidence: m.confidence,
source_tool: m.source_tool,
source_url: m.source_url,
updated_at: m.updated_at,
verified_at: m.verified_at,
expires_at: m.expires_at,
actor: m.actor,
}));
const stdout = results.length
? results.map((r, i) => `${i + 1}. [${r.key}] ${r.value}`).join('\n')
: 'No memory matches found.';
return {
success: true,
stdout,
data: {
query,
session_id: sessionId,
count: results.length,
results,
},
};
} catch (err: any) {
return { success: false, error: `Memory search failed: ${err.message}` };
}
}
export const memorySearchTool = {
name: 'memory_search',
description: 'Search long-term memory for relevant facts',
execute: executeMemorySearch,
schema: {
query: 'string (required) - what to look for',
session_id: 'string (optional) - narrow to session scope',
max: 'number (optional, default 5) - max results',
},
};