From 960f43f6436b2858c9d19b25f1dd21fa1d0a2a3a Mon Sep 17 00:00:00 2001 From: g1nation Date: Fri, 12 Jun 2026 11:59:05 +0900 Subject: [PATCH] =?UTF-8?q?feat(self):=20=EC=9E=90=EA=B8=B0=20=ED=8F=89?= =?UTF-8?q?=EA=B0=80=20=EC=A7=88=EC=9D=98=EC=97=90=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=9D=B8=EB=B2=A4=ED=86=A0=EB=A6=AC=20=EA=B0=95=EC=A0=9C=20?= =?UTF-8?q?=EC=A3=BC=EC=9E=85=20+=20=EC=B6=A9=EB=8F=8C=20=EC=8B=A0?= =?UTF-8?q?=EB=A2=B0=EB=8F=84=20=EB=B9=84=EA=B5=90=20=EA=B6=8C=EA=B3=A0=20?= =?UTF-8?q?(v2.2.226)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit [자기 지식 구식화 — 마지막 구멍 봉쇄] 인벤토리를 자동 생성해도(v2.2.225) 모델이 검색 없이 기억으로 답하면 무용 — 실사례: 답변 말미 "출처: 모델 지식 (검색 미사용)" 후 이미 있는 기능 (CoVe·멀티스텝 플래닝·노후점검 자동화)을 신규 제안. 프롬프트 규칙은 검색을 강제할 수 없으므로 scheduleContext 와 동일 패턴으로 해결: - selfAssessContext: "기능 개선/고도화/self-evolving/무슨 기능" 류 질의 감지 시 인벤토리 전문을 RAG 경쟁 없이 결정론적 주입 + "이미 있는 기능 신규 제안 금지, '현재 X 있음 — 빠진 증분 Y' 형태" 지시. 인벤토리 미생성 시 정직 안내. [충돌 해결사 — 권고까지만, 자동 결정은 안 함] - conflictScan 에 신뢰도 비교 추가: 양쪽 frontmatter(source_trust_level S~D, confidence_score) + 최신성으로 "신규/기존 우선 권고" 생성. 메타데이터 없거나 비등하면 권고 보류 (근거 없는 권고 금지). 삭제·덮어쓰기는 여전히 사람 결정. 테스트 17건 추가 (질의 감지·인벤토리 주입·신뢰 파싱·권고 분기). Co-Authored-By: Claude Opus 4.8 --- package-lock.json | 4 +- package.json | 2 +- src/agent.ts | 12 ++++ src/features/growth/conflictScan.ts | 53 ++++++++++++++- src/lib/contextBuilders/selfAssessContext.ts | 58 +++++++++++++++++ tests/selfAssessContext.test.ts | 68 ++++++++++++++++++++ 6 files changed, 192 insertions(+), 5 deletions(-) create mode 100644 src/lib/contextBuilders/selfAssessContext.ts create mode 100644 tests/selfAssessContext.test.ts diff --git a/package-lock.json b/package-lock.json index ac533ac..12679a9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "astra", - "version": "2.2.225", + "version": "2.2.226", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "astra", - "version": "2.2.225", + "version": "2.2.226", "license": "MIT", "dependencies": { "@lmstudio/sdk": "^1.5.0", diff --git a/package.json b/package.json index 0704b1f..1b8c662 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "astra", "displayName": "Astra", "description": "The personal intelligence layer for Antigravity and VS Code. A private cognitive partner for deep project context, memory, and proactive strategic decision-making.", - "version": "2.2.225", + "version": "2.2.226", "publisher": "g1nation", "license": "MIT", "icon": "assets/icon.png", diff --git a/src/agent.ts b/src/agent.ts index 155fc2c..5a369a8 100644 --- a/src/agent.ts +++ b/src/agent.ts @@ -20,6 +20,7 @@ import { SessionManager } from './core/session'; import { AgentWorkflowManager } from './agents/AgentWorkflowManager'; import { buildAstraModeArchitectureContext } from './lib/contextBuilders/astraModeArchitecture'; import { isScheduleRequest, buildScheduleContext } from './lib/contextBuilders/scheduleContext'; +import { isSelfAssessRequest, buildSelfAssessContext } from './lib/contextBuilders/selfAssessContext'; import { looksLikeCorrection, captureCorrection } from './intelligence/correctionLoop'; import { shouldUseMultiAgentWorkflow } from './lib/contextBuilders/multiAgentRouting'; import { buildThinkingPartnerResponseContract } from './lib/contextBuilders/thinkingPartnerContract'; @@ -537,6 +538,17 @@ export class AgentExecutor { } } + // [자기 평가 정본 주입] 기능 개선/자기 평가 질의는 RAG 경쟁에 맡기지 않고 + // 현행 기능 인벤토리를 결정론적으로 주입 — 모델이 검색 없이 기억으로 답해 + // 이미 있는 기능을 신규 제안하던 구식화 버그(3회 재발)의 마지막 구멍 봉쇄. + if (prompt && loopDepth === 0 && !isCasualConversation && activeBrain?.localBrainPath && isSelfAssessRequest(prompt)) { + try { + contextBlock += `\n\n${buildSelfAssessContext(activeBrain.localBrainPath)}`; + } catch (e: any) { + logError('자기 평가 컨텍스트 주입 실패 (계속 진행).', { error: e?.message ?? String(e) }); + } + } + // [Correction Loop ①] 이 발화가 직전 답변에 대한 *정정*이면 fire-and-forget // 캡처 — 오류 분류 → 태깅 레슨 + 회귀 케이스(.astra/eval/corrections.jsonl). // 정정 자체가 Ground Truth 가 되어 주간 회귀 테스트·약점 프로필의 원료가 된다. diff --git a/src/features/growth/conflictScan.ts b/src/features/growth/conflictScan.ts index b932ee9..c41aa8f 100644 --- a/src/features/growth/conflictScan.ts +++ b/src/features/growth/conflictScan.ts @@ -61,6 +61,48 @@ export interface ConflictFinding { newDoc: string; existingDoc: string; summary: string; + /** 신뢰도 비교 기반 우선 권고 (자동 삭제는 하지 않음 — 결정은 사람). */ + recommend: string; +} + +// ── 신뢰도 비교 — frontmatter(source_trust_level·confidence_score) + 최신성 ── + +export interface DocTrust { + trustLevel: string; // S/A/B/C/D 또는 '' + confidence: number; // 0~1, 미상 -1 + mtimeMs: number; +} + +const TRUST_RANK: Record = { S: 4, A: 3, B: 2, C: 1, D: 0 }; + +/** 문서 머리 frontmatter 에서 신뢰 메타데이터 추출 (없으면 빈 값). */ +export function parseDocTrust(content: string, mtimeMs: number): DocTrust { + const head = (content || '').slice(0, 1200); + const trust = head.match(/^source_trust_level:\s*["']?([SABCD])["']?\s*$/mi)?.[1]?.toUpperCase() || ''; + const confRaw = head.match(/^confidence_score:\s*["']?([\d.]+)["']?\s*$/mi)?.[1]; + const confidence = confRaw !== undefined && Number.isFinite(Number(confRaw)) ? Math.max(0, Math.min(1, Number(confRaw))) : -1; + return { trustLevel: trust, confidence, mtimeMs }; +} + +const fmtTrust = (t: DocTrust) => + `${t.trustLevel || '등급없음'}·${t.confidence >= 0 ? t.confidence.toFixed(1) : '점수없음'}`; + +/** + * 충돌 양쪽의 신뢰도를 비교해 우선 권고 문자열 생성. + * 점수 = 신뢰등급(0~4)×2 + confidence(0~1)×2 + 최신성(+1). 메타데이터가 양쪽 다 + * 없으면 권고 보류 — 근거 없는 권고는 하지 않는다. + */ +export function buildTrustRecommendation(newer: DocTrust, older: DocTrust): string { + const hasMetaN = !!newer.trustLevel || newer.confidence >= 0; + const hasMetaO = !!older.trustLevel || older.confidence >= 0; + if (!hasMetaN && !hasMetaO) return '권고 보류 — 양쪽 모두 신뢰 메타데이터 없음 (내용으로 직접 판단 필요)'; + const score = (t: DocTrust, isNewer: boolean) => + (TRUST_RANK[t.trustLevel] ?? 0) * 2 + (t.confidence >= 0 ? t.confidence : 0) * 2 + (isNewer ? 1 : 0); + const sNew = score(newer, true), sOld = score(older, false); + if (Math.abs(sNew - sOld) < 1) return `권고 보류 — 신뢰도 비등 (신규 ${fmtTrust(newer)} vs 기존 ${fmtTrust(older)})`; + return sNew > sOld + ? `권고: **신규 우선** (신규 ${fmtTrust(newer)}·최신 vs 기존 ${fmtTrust(older)})` + : `권고: **기존 우선** (기존 ${fmtTrust(older)} vs 신규 ${fmtTrust(newer)}·최신)`; } /** 1회 스캔 실행. 반환: 요약 문자열. */ @@ -114,7 +156,14 @@ export async function runConflictScanOnce(): Promise { if (!m) continue; const parsed = JSON.parse(m[0]); if (parsed?.conflict === true && String(parsed?.summary || '').trim()) { - findings.push({ newDoc: rel, existingDoc: n.relativePath, summary: String(parsed.summary).slice(0, 200) }); + let existingMtime = 0; + try { existingMtime = fs.statSync(n.filePath).mtimeMs; } catch { /* 0 유지 */ } + findings.push({ + newDoc: rel, + existingDoc: n.relativePath, + summary: String(parsed.summary).slice(0, 200), + recommend: buildTrustRecommendation(parseDocTrust(content, t.m), parseDocTrust(existing, existingMtime)), + }); } } } catch (e: any) { @@ -128,7 +177,7 @@ export async function runConflictScanOnce(): Promise { const reportFile = path.join(brainPath, REPORT_REL); const block = [ `\n## ${new Date().toLocaleString()} — 충돌 ${findings.length}건`, - ...findings.map(f => `- ⚔️ 신규 **${f.newDoc}** ↔ 기존 **${f.existingDoc}**: ${f.summary}\n → 어느 쪽을 유지할지 결정 후 다른 쪽을 수정/삭제하세요 (시스템은 덮어쓰지 않음).`), + ...findings.map(f => `- ⚔️ 신규 **${f.newDoc}** ↔ 기존 **${f.existingDoc}**: ${f.summary}\n → ${f.recommend}\n → 어느 쪽을 유지할지 결정 후 다른 쪽을 수정/삭제하세요 (시스템은 덮어쓰지 않음).`), ].join('\n'); try { fs.mkdirSync(path.dirname(reportFile), { recursive: true }); diff --git a/src/lib/contextBuilders/selfAssessContext.ts b/src/lib/contextBuilders/selfAssessContext.ts new file mode 100644 index 0000000..f6d1c49 --- /dev/null +++ b/src/lib/contextBuilders/selfAssessContext.ts @@ -0,0 +1,58 @@ +/** + * 자기 평가/개선 질의 컨텍스트 — "기능 개선 아이디어 줘" 류 질문에 ASTRA 의 + * *현행* 기능 인벤토리(자동 생성 문서)를 결정론적으로 직접 주입한다. + * + * 문제 (3회 재발한 자기 지식 구식화의 마지막 구멍): 인벤토리 문서를 자동 생성해도 + * RAG 점수 경쟁에서 안 뽑히거나 모델이 검색 없이 기억으로 답하면 — 실제 사례: + * 답변 말미에 "출처: 모델 지식 (검색 미사용)" — 이미 있는 기능을 신규 제안한다. + * 프롬프트 규칙("인벤토리와 대조하라")은 검색을 강제할 수 없다. + * + * 수정: scheduleContext(일정 질의→캘린더 실데이터 강제 주입)와 동일 패턴 — + * 자기 평가 질의를 감지하면 인벤토리 전문을 RAG 경쟁 없이 컨텍스트에 넣는다. + * "검색 안 함" 실패 모드 자체를 제거. + */ +import * as fs from 'fs'; +import * as path from 'path'; +import { INVENTORY_FILE } from '../../extension/featureInventory'; + +const IMPROVE_RE = /(개선|고도화|발전|보완|제안|평가|분석|방향|방법|아이디어|로드맵|업그레이드|날카롭|강화)/i; +const SELF_RE = /(기능|역량|능력|아키텍처|구조|시스템|self.?evolv|자기\s*진화|자기\s*개선|아스트라|astra|너(가|의|는)?|네가)/i; +const CAPABILITY_RE = /(무슨|어떤|할\s*수\s*있는)\s*(기능|일|것)|기능\s*(목록|리스트)|capabilit/i; + +/** + * 자기 평가·개선·기능 질의인지. 오탐 비용이 낮으므로(인벤토리 ~3KB 추가 주입뿐) + * 누락(또 구식 제안)보다 과잉 감지를 택한다. + */ +export function isSelfAssessRequest(prompt: string): boolean { + const p = (prompt || '').trim(); + if (!p || p.length > 600) return false; + return CAPABILITY_RE.test(p) || (IMPROVE_RE.test(p) && SELF_RE.test(p)); +} + +const MAX_INVENTORY_CHARS = 7000; + +/** 인벤토리 전문 + 대조 지시 블록. 파일 없으면 정직한 안내 (지어내기 방지). */ +export function buildSelfAssessContext(brainPath: string): string { + const header = '[ASTRA 현행 기능 인벤토리 — 소스 코드에서 자동 생성된 정본]'; + let body = ''; + try { + const file = path.join(brainPath, INVENTORY_FILE); + if (fs.existsSync(file)) body = fs.readFileSync(file, 'utf8').slice(0, MAX_INVENTORY_CHARS); + } catch { /* 아래 fallback */ } + if (!body.trim()) { + return [ + header, + '상태: 인벤토리 문서가 아직 생성되지 않음 (다음 활성화 때 자동 생성).', + '→ 기억에 의존해 기능 목록을 단정하지 말고, 기능 존재 여부가 불확실하면 "확인 필요"로 표시하라.', + ].join('\n'); + } + return [ + header, + '자기 기능 평가·개선 제안 시 반드시 아래 인벤토리와 대조하라:', + '- 아래에 **이미 있는 기능을 신규 제안하지 마라.**', + '- 제안은 "현재 X가 있고, 빠진 증분은 Y" 형태로.', + '- 아래에 없는 기능을 있다고 주장하지도 마라.', + '', + body, + ].join('\n'); +} diff --git a/tests/selfAssessContext.test.ts b/tests/selfAssessContext.test.ts new file mode 100644 index 0000000..2d45cb9 --- /dev/null +++ b/tests/selfAssessContext.test.ts @@ -0,0 +1,68 @@ +/** + * 자기 평가 정본 주입 + 충돌 신뢰도 비교 — 순수 로직 테스트. + */ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { isSelfAssessRequest, buildSelfAssessContext } from '../src/lib/contextBuilders/selfAssessContext'; +import { INVENTORY_FILE } from '../src/extension/featureInventory'; +import { parseDocTrust, buildTrustRecommendation } from '../src/features/growth/conflictScan'; + +describe('isSelfAssessRequest — 자기 평가/기능 질의 감지', () => { + test.each([ + '너의 기능 개선 아이디어 줘', + 'self-evolving 엔진을 더 날카롭게 만들 방법은?', + '아스트라 아키텍처 분석하고 개선 방향 제안해줘', + '시스템 고도화 방안 알려줘', + '너 무슨 기능 있어?', + '아스트라가 할 수 있는 일 목록', + ])('감지: %s', (p) => expect(isSelfAssessRequest(p)).toBe(true)); + + test.each([ + '오늘 업무 목록 알려줘', + '이 회의록 요약해줘', + '삼성전자 주가 어때', + '커밋하고 푸쉬해줘', + ])('비대상: %s', (p) => expect(isSelfAssessRequest(p)).toBe(false)); +}); + +describe('buildSelfAssessContext', () => { + test('인벤토리 있으면 전문 + 대조 지시 주입', () => { + const brain = fs.mkdtempSync(path.join(os.tmpdir(), 'astra-sa-')); + fs.writeFileSync(path.join(brain, INVENTORY_FILE), '# 인벤토리 v9\n- 기능 A', 'utf8'); + const block = buildSelfAssessContext(brain); + expect(block).toContain('이미 있는 기능을 신규 제안하지 마라'); + expect(block).toContain('기능 A'); + }); + test('인벤토리 없으면 정직한 안내 (지어내기 방지)', () => { + const block = buildSelfAssessContext(fs.mkdtempSync(path.join(os.tmpdir(), 'astra-sa-'))); + expect(block).toContain('생성되지 않음'); + expect(block).toContain('확인 필요'); + }); +}); + +describe('충돌 신뢰도 비교', () => { + const fm = (trust: string, conf: number) => `---\nsource_trust_level: "${trust}"\nconfidence_score: ${conf}\n---\n본문`; + test('frontmatter 파싱', () => { + const t = parseDocTrust(fm('S', 0.95), 123); + expect(t).toMatchObject({ trustLevel: 'S', confidence: 0.95, mtimeMs: 123 }); + expect(parseDocTrust('본문만 있음', 1).trustLevel).toBe(''); + expect(parseDocTrust('본문만 있음', 1).confidence).toBe(-1); + }); + test('신규 S vs 기존 C → 신규 우선', () => { + const r = buildTrustRecommendation(parseDocTrust(fm('S', 1.0), 2), parseDocTrust(fm('C', 0.5), 1)); + expect(r).toContain('신규 우선'); + }); + test('신규 등급없음 vs 기존 S → 기존 우선 (최신이어도 신뢰가 이김)', () => { + const r = buildTrustRecommendation(parseDocTrust('본문', 2), parseDocTrust(fm('S', 1.0), 1)); + expect(r).toContain('기존 우선'); + }); + test('양쪽 메타 없음 → 권고 보류 (근거 없는 권고 금지)', () => { + const r = buildTrustRecommendation(parseDocTrust('a', 2), parseDocTrust('b', 1)); + expect(r).toContain('권고 보류'); + }); + test('비등 → 보류', () => { + const r = buildTrustRecommendation(parseDocTrust(fm('A', 0.8), 2), parseDocTrust(fm('A', 0.9), 1)); + expect(r).toContain('권고 보류'); + }); +});