import * as vscode from 'vscode'; import * as os from 'os'; import * as path from 'path'; import * as fs from 'fs'; // ─── 브레인 프로필 인터페이스 ─── export interface BrainProfile { id: string; name: string; localBrainPath: string; secondBrainRepo?: string; description?: string; } // ─── 에이전트 설정 인터페이스 (통합 버전) ─── export interface IAgentConfig { ollamaUrl: string; defaultModel: string; maxTreeFiles: number; timeout: number; localBrainPath: string; secondBrainRepo: string; brainProfiles: BrainProfile[]; activeBrainId: string; maxContextSize: number; maxAutoSteps: number; dryRun: boolean; multiAgentEnabled: boolean; memoryEnabled: boolean; memoryShortTermMessages: number; memoryMediumTermSessions: number; memoryLongTermFiles: number; // ─── 컨텍스트 한계 관리 ─── contextLength: number; maxOutputTokens: number; contextSafetyMargin: number; contextOverflowPolicy: 'stopAtLimit' | 'truncateMiddle' | 'rollingWindow'; autoCompactHistory: boolean; /** 작은 모델(≤4B) 감지 시 예산 계산에 쓸 유효 context window 상한. 0 = 비활성화. */ smallModelContextCap: number; // ─── 응답 복구 (Thought Quarantine / Auto-Continuation) ─── /** 답변이 출력 토큰 한계에 걸리면 사용자 개입 없이 내부적으로 이어서 생성. */ autoContinueOnOutputLimit: boolean; /** 자동 이어쓰기 최대 횟수 (무한 반복 방지). 0 = 비활성화. */ maxAutoContinuations: number; /** 모델이 내부 사고만 출력하고 답변이 없으면 "최종 답변만" 지시로 1회 재생성. */ finalOnlyRetryOnThoughtLeak: boolean; // ─── Hybrid Semantic Search ─── /** * Embedding model name as registered in LM Studio / Ollama. Empty disables * semantic search and the retriever falls back to TF-IDF only. The user * must load this model in the engine before enabling it here. */ embeddingModel: string; /** * Blend between TF-IDF (sparse) and embedding cosine (dense) scoring. * 0 = TF-IDF only (status quo), 1 = embedding only. * Default 0.5 = equal weight, a reasonable starting point. */ embeddingBlendAlpha: number; /** * Global Knowledge Mix weight (0–100). Controls how much the assistant leans on * Second Brain evidence vs. model general knowledge when answering. * 0 → Second Brain disabled; model knowledge only. * 50 → Balanced (default). * 100 → Second Brain is primary evidence; model knowledge only fills gaps. * Per-agent overrides live in AgentKnowledgeEntry.secondBrainWeight and win. */ knowledgeMixSecondBrainWeight: number; /** * [Self-Reflection] Researcher와 Writer 사이에 메타인지 단계(Reflector)를 삽입할지 여부. * true(기본): Reflector가 plan/research를 비판적으로 검토한 critique을 Writer에 주입. * false: 기존 3단계(Planner→Researcher→Writer) 그대로 — 1 LLM 호출 절약 (저성능 모델/저지연 우선 시). */ /** * Model id used by the 1인 기업 mode intent classifier (route message to * pipeline vs casual chat). Empty → falls back to `defaultModel`. Recommended * a fast small model (gemma e2b 등) so classification adds <1 s per send. */ companyIntentClassifierModel: string; /** * Bypass the intent classifier and always run the full pipeline. Legacy * behaviour. Off by default because chat / question / thanks shouldn't * dispatch all agents. */ companyDisableIntentClassifier: boolean; /** * 분류기가 추천한 파이프라인으로 *이번 turn만* 자동 전환할지. 켜면 * 사용자가 명시적으로 활성화해 둔 파이프라인보다 분류기 추천이 우선. * 끄면 분류기 추천은 채팅 라벨에만 표시되고 dispatch는 사용자 활성 * 파이프라인 그대로. 처음 써 보는 사용자는 끈 채 추천만 보고 점차 * 신뢰 생기면 켜는 흐름을 권장 — 기본 false. */ companyAutoSelectPipeline: boolean; /** * Intent Alignment 모드. new_task 발생 시 사용자 의도를 C-G-C-F-Q로 * 정리하는 단계를 어떻게 다룰지. * - 'off' : alignment 비활성. 분류기가 new_task 라고 하면 곧장 pipeline. * - 'smart' : 기본값. confidence high면 자동 진행, medium/low면 사용자 확인. * - 'strict' : confidence 무관 항상 사용자에게 contract 확인 카드 띄움. */ companyIntentAlignmentMode: 'off' | 'smart' | 'strict'; /** alignment 라운드 최대 횟수 (질문→답변 사이클). 1~5. */ companyIntentAlignmentMaxRounds: number; /** * Pixel Office 시각화 패널을 사이드바에 표시할지 여부. UI layer 전용 — * 끈다고 Agent 행동이 바뀌지 않는다. Off면 webview는 패널을 숨기고 * 백엔드도 broadcast 자체를 skip해서 자원 절약. */ /** * Self-Reflector Phase A — 모든 LLM 응답 끝에 [Self-Reflector Check] * 자기검증 블록을 자동으로 붙이게 한다. 추가 LLM 콜 없음. 본질적으로 * 응답 품질 안전망이라 끌 이유는 적지만, 잡담 위주 환경에서 노이즈로 * 느껴진다면 꺼둘 수 있다. */ selfReflectorEnabled: boolean; /** * Self-Reflector Phase B — 회사 모드 specialist 응답 직후 *분리된 콘텍스트* * 에서 LLM 한 번 더 호출해 외부 시각으로 검증. 실패 시 1회 retry. 비용 * 추가되므로 기본 OFF. */ selfReflectorExternalEnabled: boolean; /** * Self-Reflector Phase C — 코드 산출물에 한해 syntax/lint를 실제로 돌려 * 실행 기반 검증. Python: py_compile, JS: node --check, TS: tsc --noEmit. * 실패 시 에러를 응답에 첨부. 기본 OFF — 사용자 환경에 toolchain이 * 깔려 있어야 의미가 있다. */ selfReflectorExecutionEnabled: boolean; companyPixelOfficeEnabled: boolean; /** * Pixel Office의 캐릭터 말풍선 연출을 켤지. enabled가 true이고 이 값도 * true일 때만 말풍선이 생성된다. 시끄럽게 느껴지면 사용자가 끌 수 있게. */ companyPixelOfficeBubbles: boolean; enableReflection: boolean; /** * [Self-Reflection → Knowledge] Reflector critique 중 의미 있는 발견을 brain의 * `lessons/auto-reflector/`에 lesson 카드로 영속화할지 여부. true(기본)이면 동일/유사 패턴이 * 다음 미션에서 retrieval로 자동 주입되고, 같은 critique이 반복될수록 occurrences/severity가 * 누적됨. false면 critique은 그 미션 한정으로만 사용되고 사라짐. */ autoLessonFromReflection: boolean; } // ─── 경로 정규화 유틸리티 ─── function normalizePath(p: string): string { if (!p) return p; if (p.startsWith('~/')) { return path.join(os.homedir(), p.substring(2)); } return p.trim(); } function toBrainProfile(raw: Partial | undefined, fallbackIndex: number): BrainProfile | null { if (!raw) return null; const localBrainPath = normalizePath(raw.localBrainPath || ''); if (!localBrainPath) return null; return { id: (raw.id || `brain-${fallbackIndex + 1}`).trim(), name: (raw.name || path.basename(localBrainPath) || `Brain ${fallbackIndex + 1}`).trim(), localBrainPath, secondBrainRepo: (raw.secondBrainRepo || '').trim(), description: (raw.description || '').trim() }; } // ─── VS Code 설정에서 읽어오는 값 (통합 구현) ─── export function getConfig(): IAgentConfig { const cfg = vscode.workspace.getConfiguration('g1nation'); // 브레인 프로필 로직 (utils.ts에서 이관) const legacyBrainPath = cfg.get('localBrainPath', ''); const legacyBrainRepo = cfg.get('secondBrainRepo', ''); const configuredProfiles = cfg.get[]>('brainProfiles', []); const profiles = configuredProfiles .map((profile, index) => toBrainProfile(profile, index)) .filter((profile): profile is BrainProfile => !!profile); if (profiles.length === 0) { const fallbackPath = normalizePath(legacyBrainPath) || path.join(os.homedir(), '.g1nation-brain'); profiles.push({ id: 'default-brain', name: 'Local Brain', localBrainPath: fallbackPath, secondBrainRepo: legacyBrainRepo.trim(), description: legacyBrainPath ? 'Migrated from your existing localBrainPath setting' : 'Auto-created local knowledge folder.' }); } const activeBrainId = cfg.get('activeBrainId', profiles[0].id) || profiles[0].id; const activeBrain = profiles.find((profile) => profile.id === activeBrainId) || profiles[0]; return { ollamaUrl: cfg.get('ollamaUrl', 'http://127.0.0.1:11434') || 'http://127.0.0.1:11434', defaultModel: cfg.get('defaultModel', 'gemma4:e2b') || 'gemma4:e2b', maxTreeFiles: 200, timeout: cfg.get('requestTimeout', 300) * 1000, localBrainPath: activeBrain.localBrainPath, secondBrainRepo: activeBrain.secondBrainRepo || '', brainProfiles: profiles, activeBrainId: activeBrain.id, maxContextSize: cfg.get('maxContextSize', 12000), maxAutoSteps: cfg.get('maxAutoSteps', 50), dryRun: cfg.get('dryRun', false), multiAgentEnabled: cfg.get('multiAgentEnabled', false), memoryEnabled: cfg.get('memoryEnabled', true), memoryShortTermMessages: Math.max(0, cfg.get('memoryShortTermMessages', 8)), memoryMediumTermSessions: Math.max(0, cfg.get('memoryMediumTermSessions', 5)), memoryLongTermFiles: Math.max(0, cfg.get('memoryLongTermFiles', 6)), contextLength: Math.max(2048, cfg.get('contextLength', 32768)), maxOutputTokens: Math.max(256, cfg.get('maxOutputTokens', 4096)), contextSafetyMargin: Math.max(0, cfg.get('contextSafetyMargin', 2048)), contextOverflowPolicy: ((): IAgentConfig['contextOverflowPolicy'] => { const v = cfg.get('contextOverflowPolicy', 'stopAtLimit'); return v === 'truncateMiddle' || v === 'rollingWindow' ? v : 'stopAtLimit'; })(), autoCompactHistory: cfg.get('autoCompactHistory', true), smallModelContextCap: Math.max(0, cfg.get('smallModelContextCap', 0)), autoContinueOnOutputLimit: cfg.get('autoContinueOnOutputLimit', true), maxAutoContinuations: Math.max(0, Math.min(10, cfg.get('maxAutoContinuations', 4))), finalOnlyRetryOnThoughtLeak: cfg.get('finalOnlyRetryOnThoughtLeak', true), embeddingModel: (cfg.get('embeddingModel', '') || '').trim(), embeddingBlendAlpha: Math.max(0, Math.min(1, cfg.get('embeddingBlendAlpha', 0.5))), knowledgeMixSecondBrainWeight: Math.max(0, Math.min(100, Math.round( cfg.get('knowledgeMix.secondBrainWeight', 50) ))), companyIntentClassifierModel: (cfg.get('company.intentClassifierModel', '') || '').trim(), companyDisableIntentClassifier: cfg.get('company.disableIntentClassifier', false), companyAutoSelectPipeline: cfg.get('company.autoSelectPipeline', true), companyIntentAlignmentMode: ((): 'off' | 'smart' | 'strict' => { const v = (cfg.get('company.intentAlignmentMode', 'smart') || 'smart').trim().toLowerCase(); return v === 'off' || v === 'strict' ? v : 'smart'; })(), companyIntentAlignmentMaxRounds: Math.max(1, Math.min(5, cfg.get('company.intentAlignmentMaxRounds', 3))), selfReflectorEnabled: cfg.get('selfReflector.enabled', false), selfReflectorExternalEnabled: cfg.get('selfReflector.externalVerification', false), selfReflectorExecutionEnabled: cfg.get('selfReflector.executionVerification', false), companyPixelOfficeEnabled: cfg.get('company.pixelOffice.enabled', true), companyPixelOfficeBubbles: cfg.get('company.pixelOffice.bubbles', true), enableReflection: cfg.get('enableReflection', true), autoLessonFromReflection: cfg.get('autoLessonFromReflection', true), }; } /** * Config Validator: Validates the current configuration. */ export function validateConfig(): { valid: boolean; errors: string[] } { const config = getConfig(); const errors: string[] = []; // 1. Ollama URL Validation try { new URL(config.ollamaUrl); } catch (e) { errors.push(`Invalid Ollama URL: ${config.ollamaUrl}`); } // 2. Brain Path Validation if (config.localBrainPath && !fs.existsSync(config.localBrainPath)) { errors.push(`Brain path does not exist: ${config.localBrainPath}`); } return { valid: errors.length === 0, errors }; } // ─── 보안 정책 (Security Policy) ─── export const SECURITY_POLICY = { allowedCommandPrefixes: [ 'npm', 'yarn', 'pnpm', 'npx', 'node', 'ts-node', 'git', 'python', 'python3', 'pip', 'pip3', 'docker', 'docker-compose', 'ls', 'dir', 'cat', 'type', 'echo', 'print', 'cargo', 'go', 'rustc', 'java', 'javac', 'mvn', 'gradle', 'flutter', 'dart', 'pub', 'webpack', 'vite', 'esbuild', 'parcel', 'jest', 'mocha', 'vitest', 'cypress', 'tsc', 'vue-tsc', ], forbiddenCommands: [ 'rm -rf', 'rm-rf', 'del /f', 'format', 'mkfs', 'dd if=', ':(){ :|:& };:', 'wget http', 'curl http', 'sudo', 'chmod 777', 'chown root', ], sensitiveFilePatterns: [ '.env', '.env.*', 'id_rsa', 'id_ed25519', '.gitconfig', '.npmrc', '.pypirc', 'credentials.json', 'service-account.json', ], maxFileSize: 10 * 1024 * 1024, maxContextFiles: 200, }; export const EXCLUDED_DIRS = new Set([ 'node_modules', '.git', '.vscode', 'out', 'dist', 'build', '.next', '.cache', '__pycache__', '.DS_Store', 'coverage', '.turbo', '.nuxt', '.output', 'vendor', 'target', '.astra' ]);