Files
connectai/src/intelligence/criticAgent.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

175 lines
7.1 KiB
TypeScript

/**
* 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');
}