feat(core): 자기지식 접지·웹 접근·환경 자가점검 — 할루시네이션 방어 3중화 (v2.2.247)

- Alignment Self-Learning: 자가 조사(질문 전 두뇌 검색)·사용자 답변 두뇌 저장·핵심메시지/프로젝트 컨텍스트 주입 (alignmentResearch.ts 신규)
- 웹 접근: Bridge 폴백 직접 fetch(webFetch.ts 신규)·<fetch_url> 액션 태그·기업 모드 URL/아키텍처 컨텍스트 주입·bare 도메인 인식
- 트리거 버그 수정: startsWith('/') 가 절대경로를 슬래시 명령으로 오인 — 분석 지시·URL 주입 전멸 원인 (회귀 테스트 고정)
- 자기지식 접지: 기능 인벤토리 lazy 재생성·학습 메커니즘 정본 섹션·[인벤토리 대조] 태그 의무화·결정론적 재구현 제안 정정 훅(featureConceptMap.ts 신규)
- 환경 자가점검: HealthCheckMonitor 에 Bridge/두뇌 볼륨/git 자격증명/확장 버전 검사 4종 + readyBar ⚠ 표시
- 두뇌 동기화: 원격 미설정 시 로컬 새로고침 모드·staged 기준 commit 판정·인증 부재 안내
- 기타: outputFormat 기본 markdown(제목 렌더 복구)·레슨/행동제약 truncation 보호 구역 이동·[CONTEXT] 절단 우선순위 재정렬

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
g1nation
2026-06-12 23:46:07 +09:00
parent 553aa0b134
commit a114d968b0
42 changed files with 4178 additions and 2088 deletions
+43
View File
@@ -0,0 +1,43 @@
import { HandlerContext } from './types';
import { buildUrlContext } from '../../lib/contextBuilders/urlContext';
/**
* Action 15: Fetch URL (read-only — transaction record 불필요)
*
* LLM 이 작업 중 웹 페이지의 실제 내용이 필요할 때 emit 하는 태그.
* 결과 블록을 chatHistory 에 internal push — read_file 과 동일 패턴이라
* 일반 챗의 continuation loop(컨텍스트 주입 → 자동 재호출)가 그대로 작동해
* fetch 직후 모델이 내용을 분석한다.
*
* buildUrlContext 재사용: Bridge 추출 우선 → 직접 fetch 폴백 → 정직 실패
* 블록 + 5분 캐시. 실패 블록도 chatHistory 에 push 한다 — 모델이 "가져왔다"고
* 지어내지 않고 실패 사실을 사용자에게 전달해야 하므로.
*/
const FETCH_URL_MAX_PER_TURN = 2;
export async function applyWebFetchActions(ctx: HandlerContext): Promise<void> {
const { aiMessage, report } = ctx;
const fetchRegex = /<fetch_url\s+url=['"]?(https?:\/\/[^'">\s]+)['"]?\s*\/?>(?:<\/fetch_url>)?/gi;
let match;
let handled = 0;
const seen = new Set<string>();
while ((match = fetchRegex.exec(aiMessage)) !== null) {
if (handled >= FETCH_URL_MAX_PER_TURN) {
report.push(`⚠️ fetch_url 한도 초과 — 회당 최대 ${FETCH_URL_MAX_PER_TURN}개만 처리합니다.`);
break;
}
const url = match[1].trim();
if (seen.has(url)) continue;
seen.add(url);
handled++;
try {
const block = await buildUrlContext(url);
const ok = block.includes('실데이터');
report.push(ok ? `🌐 Fetched: ${url}` : `⚠️ Fetch failed: ${url}`);
ctx.chatHistory.push({ role: 'system', content: block, internal: true });
} catch (err: any) {
// buildUrlContext 는 throw 하지 않지만 방어적으로.
report.push(`⚠️ Fetch error: ${url}${String(err?.message ?? err).slice(0, 100)}`);
}
}
}
@@ -22,6 +22,11 @@ export interface BuildAgentModeSystemPromptInput {
actualModel: string;
/** For token-cost logging — getConfig().contextLength. */
contextLength: number;
/**
* 행동 제약·동적 블록 (behavior-constraints / cove-checklist 등) — Astra 모드와
* 동일하게 [CONTEXT] *밖* 보호 구역에 join. 없으면 옛 동작 그대로.
*/
dynamicBlocks?: Map<string, string>;
}
export function buildAgentModeSystemPrompt(input: BuildAgentModeSystemPromptInput): string {
@@ -38,6 +43,7 @@ export function buildAgentModeSystemPrompt(input: BuildAgentModeSystemPromptInpu
negativeCtx,
actualModel,
contextLength,
dynamicBlocks,
} = input;
// The Agent's prompt IS the primary directive (role / persona / tone / output format),
@@ -69,10 +75,20 @@ export function buildAgentModeSystemPrompt(input: BuildAgentModeSystemPromptInpu
// [CONTEXT] … [/CONTEXT] 사이만 컨텍스트 초과 시 trim 대상 — agentBlock(앞)·reminder(뒤)·negative 는 보호.
// memoryCtx(RAG/메모리/lessons)도 [CONTEXT] 안에 넣어 토큰이 빡빡할 때 대화 기록보다 먼저 잘리게 한다.
// 순서 = 잘림 우선순위 (truncation 은 body *뒤*부터 자름): contextBlock(사용자가
// 지금 가리킨 실데이터 — URL 본문/열린 파일/일정)을 맨 앞에 둬 최후까지 보존하고,
// 배경 지식(brain RAG)과 memory 는 그보다 먼저 잘리게 한다.
const priorConclusionBlock = priorConclusionCtx ? '\n\n' + priorConclusionCtx : '';
// [자기 지식 + 1인칭] Astra 모드와 공용 — Agent 모드에서도 자기 오보고/3인칭 화법 방지.
const selfIdentityBlock = '\n\n' + buildSelfIdentityBlock();
const fullSystemPrompt = `${agentBlock}${modeBridgeCtx ? '\n\n' + modeBridgeCtx : ''}${priorConclusionBlock}\n\n${strippedSystemPrompt}${selfIdentityBlock}${designerCtx}${secondBrainTraceCtx}\n\n[CONTEXT]\n${memoryCtx}\n${knowledgeContextForPrompt}\n${contextBlock}\n[/CONTEXT]\n${negativeCtx}${agentTailReminder}`;
// 동적 블록 (행동 제약·CoVe 등) — [CONTEXT] 밖에 join 해 truncation 보호.
let dynamicBlocksJoined = '';
if (dynamicBlocks && dynamicBlocks.size > 0) {
for (const body of dynamicBlocks.values()) {
if (body && body.trim()) dynamicBlocksJoined += '\n\n' + body;
}
}
const fullSystemPrompt = `${agentBlock}${modeBridgeCtx ? '\n\n' + modeBridgeCtx : ''}${priorConclusionBlock}\n\n${strippedSystemPrompt}${selfIdentityBlock}${designerCtx}${secondBrainTraceCtx}${dynamicBlocksJoined}\n\n[CONTEXT]\n${contextBlock}\n${knowledgeContextForPrompt}\n${memoryCtx}\n[/CONTEXT]\n${negativeCtx}${agentTailReminder}`;
return fullSystemPrompt;
}
@@ -101,5 +101,8 @@ export function buildAstraModeSystemPrompt(input: BuildAstraModeSystemPromptInpu
if (body && body.trim()) dynamicBlocksJoined += '\n\n' + body;
}
}
return `${systemPrompt}${modeBridgeCtx ? '\n\n' + modeBridgeCtx : ''}${priorConclusionBlock}${designerCtx}${projectArchitectureCtx}${localProjectKnowledgeCtx}${thinkingPartnerCtx}${astraStanceCtx}${secondBrainTraceCtx}${v4PolicyCtx}${selfGrowthIdentityCtx}${knowledgeMixCtx}${casualCtx}${dynamicBlocksJoined}\n\n[CONTEXT]\n${memoryCtx}\n${knowledgeContextForPrompt}\n${contextBlock}\n[/CONTEXT]\n${negativeCtx}`;
// [CONTEXT] 내부 순서 = 잘림 우선순위 (truncation 은 body *뒤*부터 자름):
// contextBlock(사용자가 지금 가리킨 실데이터 — URL 본문/열린 파일/일정)을 맨 앞에
// 둬 최후까지 보존, 배경 지식(brain RAG)과 memory 는 그보다 먼저 잘리게 한다.
return `${systemPrompt}${modeBridgeCtx ? '\n\n' + modeBridgeCtx : ''}${priorConclusionBlock}${designerCtx}${projectArchitectureCtx}${localProjectKnowledgeCtx}${thinkingPartnerCtx}${astraStanceCtx}${secondBrainTraceCtx}${v4PolicyCtx}${selfGrowthIdentityCtx}${knowledgeMixCtx}${casualCtx}${dynamicBlocksJoined}\n\n[CONTEXT]\n${contextBlock}\n${knowledgeContextForPrompt}\n${memoryCtx}\n[/CONTEXT]\n${negativeCtx}`;
}
+25
View File
@@ -24,6 +24,8 @@ import { appendReflection } from '../../intelligence/reflectionStore';
import { detectGaps } from '../../intelligence/gapDetector';
import { appendSuccessPattern } from '../../intelligence/skillScore';
import { getConfig } from '../../config';
import { detectReimplementedProposals, formatReimplementationFooter } from '../../extension/featureConceptMap';
import { isSelfAssessRequest, isAboutSelf } from '../../lib/contextBuilders/selfAssessContext';
const devilRebuttalHook: PostAnswerHook = {
id: 'devil-rebuttal',
@@ -226,6 +228,28 @@ const criticLoopHook: PostAnswerHook = {
},
};
/**
* 인벤토리 자동 대조 — 자기 평가/자기 분석 턴에서 답변이 *이미 구현된 기능*을
* 신규 도입하라고 제안하면 결정론적으로(LLM 콜 0) 정정 푸터를 붙인다.
*
* 배경: 인벤토리를 프롬프트에 주입해도 작은 로컬 모델이 mid-prompt 컨텍스트를
* 무시하고 "Reflection Layer 도입하라"(이미 Self-Reflector 3단계 구현) 같은
* 제안을 반복하는 실패가 재현됐다. 주입은 1차 방어선 — 이 훅이 최종 방어선.
*/
const inventoryCrossCheckHook: PostAnswerHook = {
id: 'inventory-cross-check',
runAsync: false,
run(ctx: PostAnswerHookContext): void {
if (!ctx.assistantAnswer || !ctx.assistantAnswer.trim()) return;
// 자기 대상 턴에만 — 일반 코딩/리서치 답변에 오탐 푸터를 붙이지 않게.
if (!(isSelfAssessRequest(ctx.userPrompt) || isAboutSelf(ctx.userPrompt))) return;
const hits = detectReimplementedProposals(ctx.assistantAnswer);
if (hits.length === 0) return;
const footer = formatReimplementationFooter(hits);
if (footer) ctx.getWebview()?.postMessage({ type: 'streamChunk', value: footer });
},
};
export const POST_ANSWER_HOOKS: PostAnswerHook[] = [
devilRebuttalHook,
postHocSelfCheckHook,
@@ -233,6 +257,7 @@ export const POST_ANSWER_HOOKS: PostAnswerHook[] = [
requirementCoverageHook,
confidenceEscalationHook,
criticLoopHook,
inventoryCrossCheckHook,
];
/** 모든 hook 을 안전하게 실행 — 한 hook 의 throw 가 다른 hook 막지 않음. */