import { mapExperienceRow, normalizeExperienceRecordInput, parseExperienceJson, } from './experience-schema.mjs'; export const DEFAULT_REFLECT_MIN_GROUP_SIZE = 3; export function problemFingerprint(problem) { const text = String(problem ?? '') .toLowerCase() .replace(/[^\p{L}\p{N}]+/gu, ' ') .trim() .replace(/\s+/g, ' '); if (!text) return ''; return text.slice(0, 120); } export function environmentMatchBoost(row, terms) { if (!terms.length) return 0; const environment = parseExperienceJson(row.environment_json ?? row.environment, null); if (!environment) return 0; const haystack = [ environment.runtime, ...(Array.isArray(environment.components) ? environment.components : []), environment.region, ] .filter(Boolean) .join(' ') .toLowerCase(); if (!haystack) return 0; let boost = 0; for (const term of terms) { if (haystack.includes(term)) boost += 0.5; } return boost; } export function scoreExperienceRow(row, terms, nowMs, halfLifeMs) { const haystack = [ row.title, row.body, row.problem, row.environment_json ? JSON.stringify(parseExperienceJson(row.environment_json, {})) : '', ] .filter(Boolean) .join('\n') .toLowerCase(); let keywordScore = 0; for (const term of terms) { if (haystack.includes(term)) keywordScore += 1; } keywordScore += environmentMatchBoost(row, terms); if (keywordScore <= 0) return { row, score: 0 }; const ageMs = Math.max(0, nowMs - Number(row.updated_at ?? row.updatedAt ?? 0)); const recency = Math.pow(0.5, ageMs / halfLifeMs); return { row, score: keywordScore * recency }; } export function groupTaskOutcomesByProblem(rows, minGroupSize = DEFAULT_REFLECT_MIN_GROUP_SIZE) { const buckets = new Map(); for (const row of rows) { if (String(row.kind ?? '').toLowerCase() !== 'task_outcome') continue; if (row.parent_experience_id ?? row.parentExperienceId) continue; if (String(row.status ?? 'active').toLowerCase() !== 'active') continue; const fp = problemFingerprint(row.problem || row.title); if (!fp) continue; if (!buckets.has(fp)) buckets.set(fp, []); buckets.get(fp).push(row); } return [...buckets.values()].filter((group) => group.length >= minGroupSize); } export function findExistingExecutionPattern(rows, fingerprint) { for (const row of rows) { if (String(row.kind ?? '').toLowerCase() !== 'execution_pattern') continue; if (String(row.status ?? 'active').toLowerCase() !== 'active') continue; const fp = problemFingerprint(row.problem || row.title); if (fp === fingerprint) return row; } return null; } export function buildExecutionPatternAggregate(rows) { const sorted = [...rows].sort( (a, b) => Number(b.updated_at ?? b.updatedAt ?? 0) - Number(a.updated_at ?? a.updatedAt ?? 0), ); const problem = sorted[0].problem || sorted[0].title; const results = sorted.map((row) => row.result).filter(Boolean); const successCount = results.filter((value) => value === 'success').length; const result = successCount >= results.length / 2 ? 'success' : results.every((value) => value === 'failure') ? 'failure' : 'partial'; const confidences = sorted .map((row) => (row.confidence == null ? null : Number(row.confidence))) .filter((value) => Number.isFinite(value)); const confidence = confidences.length > 0 ? Number((confidences.reduce((sum, value) => sum + value, 0) / confidences.length).toFixed(3)) : null; const components = new Set(); let runtime = null; const steps = new Set(); let executor = null; const sources = []; const seenSourceIds = new Set(); for (const row of sorted) { const environment = parseExperienceJson(row.environment_json ?? row.environment, null); if (environment?.runtime) runtime = environment.runtime; for (const component of environment?.components ?? []) { if (component) components.add(String(component)); } const action = parseExperienceJson(row.action_json ?? row.action, null); if (action?.executor) executor = action.executor; for (const step of action?.steps ?? []) { if (step) steps.add(String(step)); } const evidence = parseExperienceJson(row.evidence_json ?? row.evidence, null); for (const source of evidence?.sources ?? []) { const sourceId = String(source?.source_id ?? '').trim(); if (sourceId && seenSourceIds.has(sourceId)) continue; if (sourceId) seenSourceIds.add(sourceId); sources.push(source); } } const bodyLines = sorted.slice(0, 5).map((row) => `- ${row.title}: ${row.body}`); if (sorted.length > 5) { bodyLines.push(`- … 另有 ${sorted.length - 5} 条同类任务记录`); } return normalizeExperienceRecordInput({ kind: 'execution_pattern', title: String(problem).slice(0, 200), body: `聚合 ${sorted.length} 次同类任务经验:\n${bodyLines.join('\n')}`, problem, result, confidence, environment: { runtime, components: [...components], }, action: { executor, steps: [...steps], }, evidence: { sources, provenance: { aggregated_from: sorted.map((row) => row.id), }, }, tags: ['execution_pattern', 'reflect'], }); } export function planExperienceReflection(rows, { minGroupSize = DEFAULT_REFLECT_MIN_GROUP_SIZE } = {}) { const activeRows = rows.filter((row) => String(row.status ?? 'active').toLowerCase() === 'active'); const groups = groupTaskOutcomesByProblem(activeRows, minGroupSize); const plans = []; for (const group of groups) { const fingerprint = problemFingerprint(group[0].problem || group[0].title); if (!fingerprint) continue; if (findExistingExecutionPattern(activeRows, fingerprint)) continue; plans.push({ fingerprint, sourceIds: group.map((row) => row.id), payload: buildExecutionPatternAggregate(group), }); } return plans; } export function mapReflectResult(savedRow) { return mapExperienceRow(savedRow); }