Files
connectai/src/intelligence/reflectionStore.ts
T
koriweb 2afd1ac589 feat: Self-Evolving Digital Employee OS P0~P6 + 캘린더 충돌 게이트
신뢰성 코어 (P1~P2):
- Requirement Graph: 업무 유형(회의록/시장조사/업무조사/일정) 필수 요소 주입 + 커버리지 hook
- Confidence Engine(0~100 결정론적) / Escalation Engine(검토 요청) / Epistemic Guard(모름·추정·확실 3분류)
- Provenance: citationTrace 에 출처 수정일·오래됨 경고
- Critic Loop: 문제 신호 turn 만 LLM 검수 1회 + 보완 카드

성장 루프 (P3):
- Gap Detector(Requirement-Knowledge) / Need Engine(30/25/20/15/10 공식) / Knowledge Inventory
- Learning Queue(proposed 전용 병합 — 승인은 사람만) / Decision Journal / Reflection 기록
- 반복 누락 요소(3회+)는 다음 turn 체크리스트에 자동 강조 (T5 루프)

지식 운영 (P4) + 기억 (P5) + 학습 실행 (P6):
- Knowledge Validation + Belief Revision(중복 reject·충돌 시 update/add 권고)
- Knowledge Decay(분야별 반감기 감사) / Knowledge Debt(blocked x impact)
- Organizational Memory(.astra/organization.md 상시 주입)
- Research Agent(approved 큐 -> 조사 브리프+추정 라벨 초안+Validation 게이트 -> proposals/)
- Skill Score(전/후반 추세) + Success Pattern DB(전요소충족+확신도90+ 자동 적재)

병렬 트랙:
- 캘린더 충돌 게이트: conflictCheck + 구조화 이벤트 캐시 + create_calendar_event 차단(force 는 사용자 승인 후)
- Task Eval Harness: 회의록 골든셋 자동 채점 명령 + 성장 리포트/학습 큐/노후 점검 명령

신규 모듈 17종(src/intelligence/), VS Code 명령 5종, 설정 11종, 테스트 +89건(전체 508 통과).
설계 문서: docs/SELF_EVOLVING_OS_MASTER_PLAN.md

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-11 13:42:09 +09:00

163 lines
6.7 KiB
TypeScript

/**
* Reflection Store — 업무 turn 회고 기록 + Failure Pattern 집계.
*
* Self-Evolving OS 마스터 플랜 Phase 1 / Track 2-4 (Reflection Engine v1) +
* Phase 3 / Track 3-6 (Failure Pattern DB v1 시드). 신뢰 조건 T5
* "같은 실수를 반복하지 않는다" 의 데이터 기반.
*
* v1 은 결정론적 신호만 기록 (LLM 회고 질문은 후속 증분):
* 업무 turn 종료 → {업무유형, 확신도, 누락 요소, 에스컬레이션 여부, Critic 이슈 수}
* 를 <brain>/.astra/growth/reflections.jsonl 에 append.
*
* 이 파일이 쌓이면:
* - summarizeFailurePatterns() → "회의록·기한 누락 N회" 류 반복 실수 집계
* - formatGrowthReport() → 기간별 확신도/누락률 추이 = *성장세 그래프의 원천*
*/
import * as fs from 'fs';
import * as path from 'path';
export const REFLECTIONS_REL_PATH = path.join('.astra', 'growth', 'reflections.jsonl');
export interface ReflectionRecord {
/** ISO timestamp. */
ts: string;
taskId: string | null;
taskLabel: string | null;
confidenceScore: number;
confidenceBand: string;
/** 커버리지 누락 요소 label 목록. */
missing: string[];
escalated: boolean;
/** Critic 검수가 돌았으면 발견 이슈 수, 안 돌았으면 null. */
criticIssues: number | null;
/** 요청 미리보기 (디버그·회고용, 120자). */
promptPreview: string;
// ── Decision Journal v1 (Track 3-7) — "왜 이 확신도/판단이었나" 역추적 필드 ──
/** 확신도 기여 요인 label 목록 (confidenceEngine factors). */
factors?: string[];
/** 답변에 쓰인 상위 출처 title (citation/selfCheckSources 기준). */
usedSources?: string[];
// ── Gap Detector v1 (Track 3-2) — Need Engine 입력 신호 ──
/** 검색 그라운딩: 청크 수·최고 score. */
retrieval?: { chunkCount: number; topScore: number };
/** 검색 근거 없이/약하게 수행한 업무 turn (지식 갭 신호). */
weakGrounding?: boolean;
/** 갭 심각도 (none/low/medium/high). */
gapSeverity?: string;
}
/** 회고 1건 append — 실패해도 throw 하지 않음 (회고가 turn 을 막으면 안 됨). */
export function appendReflection(brainPath: string, record: ReflectionRecord): boolean {
try {
if (!brainPath) return false;
const file = path.join(brainPath, REFLECTIONS_REL_PATH);
fs.mkdirSync(path.dirname(file), { recursive: true });
fs.appendFileSync(file, JSON.stringify(record) + '\n', 'utf8');
return true;
} catch {
return false;
}
}
/** 회고 로드 — 깨진 줄은 무시. limit 은 *최근* N건. */
export function loadReflections(brainPath: string, limit?: number): ReflectionRecord[] {
try {
const file = path.join(brainPath, REFLECTIONS_REL_PATH);
if (!fs.existsSync(file)) return [];
const lines = fs.readFileSync(file, 'utf8').split('\n').filter((l) => l.trim());
const records: ReflectionRecord[] = [];
for (const line of lines) {
try {
const obj = JSON.parse(line);
if (obj && typeof obj.ts === 'string') records.push(obj as ReflectionRecord);
} catch { /* skip broken line */ }
}
return limit && limit > 0 ? records.slice(-limit) : records;
} catch {
return [];
}
}
export interface FailurePattern {
taskId: string;
taskLabel: string;
element: string;
count: number;
}
/**
* Failure Pattern 집계 — (업무유형 × 누락 요소) 별 반복 횟수, 많은 순.
* "시장규모 누락 27회" 류의 반복 실수를 수치로 노출 (설계서 12장).
*/
export function summarizeFailurePatterns(records: ReflectionRecord[]): FailurePattern[] {
const counts = new Map<string, FailurePattern>();
for (const r of records) {
if (!r.taskId) continue;
for (const el of r.missing || []) {
const key = `${r.taskId}::${el}`;
const cur = counts.get(key);
if (cur) cur.count++;
else counts.set(key, { taskId: r.taskId, taskLabel: r.taskLabel || r.taskId, element: el, count: 1 });
}
}
return Array.from(counts.values()).sort((a, b) => b.count - a.count);
}
/**
* 반복 실수 경고 — 같은 (업무 × 요소) 누락이 threshold 회 이상이면 해당 요소를
* 시스템 프롬프트 강조 대상으로 반환. Requirement Graph 블록이 이걸 받아
* "특히 자주 누락되는 요소" 로 표시 (T5 루프의 첫 닫힘).
*/
export function recurrentMisses(records: ReflectionRecord[], taskId: string, threshold = 3): string[] {
return summarizeFailurePatterns(records)
.filter((p) => p.taskId === taskId && p.count >= threshold)
.map((p) => p.element);
}
/** 기간(주) 단위 성장 리포트 — 확신도 평균·누락률 추이. */
export function formatGrowthReport(records: ReflectionRecord[]): string {
if (records.length === 0) return '# 성장 리포트\n\n기록 없음 — 업무 turn 이 쌓이면 추이가 표시됩니다.\n';
// 주 단위 버킷 (ISO week 근사 — ts 앞 10자의 날짜 기준 7일 묶음).
const byWeek = new Map<string, ReflectionRecord[]>();
for (const r of records) {
const d = new Date(r.ts);
if (isNaN(d.getTime())) continue;
const weekStart = new Date(d);
weekStart.setDate(d.getDate() - d.getDay()); // 일요일 기준
const key = weekStart.toISOString().slice(0, 10);
const arr = byWeek.get(key) || [];
arr.push(r);
byWeek.set(key, arr);
}
const lines: string[] = [];
lines.push('# ASTRA 성장 리포트 (Reflection 기반)');
lines.push('');
lines.push(`총 업무 turn: ${records.length}`);
lines.push('');
lines.push('| 주 (시작일) | 업무 수 | 평균 확신도 | 요소 누락률 | 에스컬레이션 |');
lines.push('|---|---|---|---|---|');
const weeks = Array.from(byWeek.keys()).sort();
for (const w of weeks) {
const rs = byWeek.get(w)!;
const avgConf = rs.reduce((s, r) => s + (r.confidenceScore || 0), 0) / rs.length;
const missRate = rs.filter((r) => (r.missing || []).length > 0).length / rs.length;
const escCount = rs.filter((r) => r.escalated).length;
lines.push(`| ${w} | ${rs.length} | ${avgConf.toFixed(0)} | ${(missRate * 100).toFixed(0)}% | ${escCount} |`);
}
lines.push('');
lines.push('## 반복 실수 Top (Failure Patterns)');
const patterns = summarizeFailurePatterns(records).slice(0, 10);
if (patterns.length === 0) {
lines.push('- 없음');
} else {
for (const p of patterns) lines.push(`- ${p.taskLabel} · **${p.element}** 누락 ${p.count}`);
}
lines.push('');
return lines.join('\n');
}