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>
This commit is contained in:
@@ -0,0 +1,174 @@
|
||||
/**
|
||||
* Critic Agent + Debate Loop (v1) — 제출된 업무 산출물의 LLM 검수.
|
||||
*
|
||||
* Self-Evolving OS 마스터 플랜 Phase 1 / Track 2-3. 신뢰 조건 T3 의 LLM 계층:
|
||||
* Requirement Coverage(결정론적, 정규식) 가 "요소가 *언급* 됐는가" 만 보면,
|
||||
* Critic 은 "내용이 *충실* 한가 + 결정/미결 구분이 맞는가 + 근거 없는 단정이
|
||||
* 없는가" 를 본다.
|
||||
*
|
||||
* Debate Loop 원형은 작성→비판→재작성→재검토지만, 로컬 Gemma 의 latency 비용
|
||||
* 때문에 v1 은 *조건부 1-pass 검수* — 결정론적 검사(커버리지 누락 또는 확신도
|
||||
* <70)가 문제를 신호할 때만 Critic LLM 1회 호출, 결과를 답변 아래 보완 카드로
|
||||
* 표시. 전면 다회전 debate 는 config knob(maxRounds) 만 준비해 두고 후속 증분.
|
||||
*
|
||||
* 모든 LLM 의존은 주입(critique caller) — 모듈 자체는 순수, 테스트 가능.
|
||||
*/
|
||||
|
||||
import type { TaskRequirement } from './requirementGraph';
|
||||
|
||||
export interface CriticIssue {
|
||||
severity: 'major' | 'minor';
|
||||
description: string;
|
||||
}
|
||||
|
||||
export interface CritiqueResult {
|
||||
/** true = 검수 통과 (보완 불필요). */
|
||||
pass: boolean;
|
||||
issues: CriticIssue[];
|
||||
/** 누락 요소를 보완하는 추가 섹션 제안 (Critic 이 생성 가능했을 때만). */
|
||||
supplement: string;
|
||||
/** 디버그용 원문 (파싱 실패 분석). */
|
||||
raw?: string;
|
||||
}
|
||||
|
||||
/** 주입형 LLM caller — agent.ts 의 callNonStreaming 또는 평가 하니스의 단순 호출. */
|
||||
export type CritiqueLlmCall = (system: string, user: string, maxTokens: number) => Promise<string>;
|
||||
|
||||
export interface CriticOptions {
|
||||
/** 검수 대상 초안 최대 길이 (chars) — 초과분 잘라서 전달. 기본 12000. */
|
||||
maxDraftChars: number;
|
||||
/** Critic 응답 max tokens. 기본 700. */
|
||||
maxTokens: number;
|
||||
}
|
||||
|
||||
export const DEFAULT_CRITIC_OPTIONS: CriticOptions = {
|
||||
maxDraftChars: 12000,
|
||||
maxTokens: 700,
|
||||
};
|
||||
|
||||
export function buildCritiquePrompt(
|
||||
userPrompt: string,
|
||||
draft: string,
|
||||
requirement: TaskRequirement | null,
|
||||
missingLabels: string[],
|
||||
opts: CriticOptions = DEFAULT_CRITIC_OPTIONS,
|
||||
): { system: string; user: string } {
|
||||
const reqSection = requirement
|
||||
? [
|
||||
`업무 유형: ${requirement.label}`,
|
||||
'필수 요소:',
|
||||
...requirement.elements.map((e) => `- ${e.label}: ${e.hint}`),
|
||||
].join('\n')
|
||||
: '업무 유형: (미분류)';
|
||||
const missingSection = missingLabels.length > 0
|
||||
? `결정론적 검사가 누락 가능성을 표시한 요소: ${missingLabels.join(', ')}`
|
||||
: '결정론적 검사 통과 (참고용 재확인)';
|
||||
|
||||
const system = [
|
||||
'너는 업무 산출물 검수자(Critic)다. 동료가 작성한 초안을 비판적으로 검토한다.',
|
||||
'검수 기준:',
|
||||
'1. 필수 요소가 *내용으로* 충실한가 (단어만 등장 ≠ 충족).',
|
||||
'2. 결정사항과 미결(논의만 된 것)이 구분돼 있는가.',
|
||||
'3. 근거 없는 단정·지어낸 수치/이름/날짜가 없는가. 원문에 없는 내용 발견 시 major.',
|
||||
'4. 정보가 없는 항목은 "(확인 필요)" 로 솔직히 표시했는가.',
|
||||
'',
|
||||
'반드시 아래 JSON *만* 출력 (다른 텍스트 금지):',
|
||||
'{"pass": true|false, "issues": [{"severity": "major"|"minor", "description": "..."}], "supplement": "누락 보완 텍스트 (보완 불가능하면 빈 문자열)"}',
|
||||
'supplement 는 초안에 *실제로 추가할 수 있는* 마크다운 섹션만. 원문에 없는 내용을 지어내 보완하는 것은 금지 — 그 경우 "(확인 필요)" 항목으로 작성.',
|
||||
].join('\n');
|
||||
|
||||
const draftCapped = draft.length > opts.maxDraftChars ? draft.slice(0, opts.maxDraftChars) + '\n…(잘림)' : draft;
|
||||
const user = [
|
||||
`[원래 요청]\n${userPrompt}`,
|
||||
`[검수 기준 컨텍스트]\n${reqSection}\n${missingSection}`,
|
||||
`[검수 대상 초안]\n${draftCapped}`,
|
||||
].join('\n\n');
|
||||
|
||||
return { system, user };
|
||||
}
|
||||
|
||||
/** Critic LLM 응답에서 JSON 추출 — 코드펜스/잡설 섞여도 첫 균형 {} 블록을 파싱. */
|
||||
export function parseCritique(raw: string): CritiqueResult | null {
|
||||
if (!raw || !raw.trim()) return null;
|
||||
const start = raw.indexOf('{');
|
||||
if (start === -1) return null;
|
||||
// 균형 괄호 스캔 — 중첩 객체(issues 배열 내부) 안전.
|
||||
let depth = 0;
|
||||
let end = -1;
|
||||
let inString = false;
|
||||
let escaped = false;
|
||||
for (let i = start; i < raw.length; i++) {
|
||||
const ch = raw[i];
|
||||
if (escaped) { escaped = false; continue; }
|
||||
if (ch === '\\') { escaped = true; continue; }
|
||||
if (ch === '"') { inString = !inString; continue; }
|
||||
if (inString) continue;
|
||||
if (ch === '{') depth++;
|
||||
else if (ch === '}') {
|
||||
depth--;
|
||||
if (depth === 0) { end = i; break; }
|
||||
}
|
||||
}
|
||||
if (end === -1) return null;
|
||||
try {
|
||||
const obj = JSON.parse(raw.slice(start, end + 1));
|
||||
const issues: CriticIssue[] = Array.isArray(obj.issues)
|
||||
? obj.issues
|
||||
.filter((i: any) => i && typeof i.description === 'string')
|
||||
.map((i: any) => ({
|
||||
severity: i.severity === 'major' ? 'major' : 'minor',
|
||||
description: String(i.description).slice(0, 500),
|
||||
}))
|
||||
: [];
|
||||
return {
|
||||
pass: obj.pass === true && issues.length === 0,
|
||||
issues,
|
||||
supplement: typeof obj.supplement === 'string' ? obj.supplement.slice(0, 4000) : '',
|
||||
raw: raw.slice(0, 200),
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Critic 검수 1회 실행. LLM 실패/파싱 실패 시 null — 호출자(hook)는 silent skip
|
||||
* (검수 실패가 main turn 을 막지 않도록).
|
||||
*/
|
||||
export async function runCriticReview(params: {
|
||||
userPrompt: string;
|
||||
draft: string;
|
||||
requirement: TaskRequirement | null;
|
||||
missingLabels: string[];
|
||||
callLlm: CritiqueLlmCall;
|
||||
options?: Partial<CriticOptions>;
|
||||
}): Promise<CritiqueResult | null> {
|
||||
const opts: CriticOptions = { ...DEFAULT_CRITIC_OPTIONS, ...(params.options || {}) };
|
||||
const { system, user } = buildCritiquePrompt(params.userPrompt, params.draft, params.requirement, params.missingLabels, opts);
|
||||
let raw: string;
|
||||
try {
|
||||
raw = await params.callLlm(system, user, opts.maxTokens);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
return parseCritique(raw);
|
||||
}
|
||||
|
||||
/** 검수 결과 footer — pass 면 빈 문자열 (노이즈 방지). */
|
||||
export function formatCriticFooter(critique: CritiqueResult): string {
|
||||
if (critique.pass) return '';
|
||||
const lines: string[] = [];
|
||||
lines.push('\n\n> 🔁 **검수 (Critic)** — 초안에서 발견된 문제:');
|
||||
for (const issue of critique.issues.slice(0, 6)) {
|
||||
const tag = issue.severity === 'major' ? '🔴' : '🟡';
|
||||
lines.push(`> - ${tag} ${issue.description}`);
|
||||
}
|
||||
if (critique.supplement && critique.supplement.trim()) {
|
||||
lines.push('>');
|
||||
lines.push('> **보완 제안:**');
|
||||
for (const l of critique.supplement.trim().split('\n')) {
|
||||
lines.push(`> ${l}`);
|
||||
}
|
||||
}
|
||||
return lines.join('\n');
|
||||
}
|
||||
Reference in New Issue
Block a user