Files
connectai/src/config.ts
T
2026-05-14 21:58:59 +09:00

225 lines
9.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 (0100). 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 호출 절약 (저성능 모델/저지연 우선 시).
*/
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<BrainProfile> | 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<string>('localBrainPath', '');
const legacyBrainRepo = cfg.get<string>('secondBrainRepo', '');
const configuredProfiles = cfg.get<Partial<BrainProfile>[]>('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<string>('activeBrainId', profiles[0].id) || profiles[0].id;
const activeBrain = profiles.find((profile) => profile.id === activeBrainId) || profiles[0];
return {
ollamaUrl: cfg.get<string>('ollamaUrl', 'http://127.0.0.1:11434') || 'http://127.0.0.1:11434',
defaultModel: cfg.get<string>('defaultModel', 'gemma4:e2b') || 'gemma4:e2b',
maxTreeFiles: 200,
timeout: cfg.get<number>('requestTimeout', 300) * 1000,
localBrainPath: activeBrain.localBrainPath,
secondBrainRepo: activeBrain.secondBrainRepo || '',
brainProfiles: profiles,
activeBrainId: activeBrain.id,
maxContextSize: cfg.get<number>('maxContextSize', 12000),
maxAutoSteps: cfg.get<number>('maxAutoSteps', 50),
dryRun: cfg.get<boolean>('dryRun', false),
multiAgentEnabled: cfg.get<boolean>('multiAgentEnabled', false),
memoryEnabled: cfg.get<boolean>('memoryEnabled', true),
memoryShortTermMessages: Math.max(0, cfg.get<number>('memoryShortTermMessages', 8)),
memoryMediumTermSessions: Math.max(0, cfg.get<number>('memoryMediumTermSessions', 5)),
memoryLongTermFiles: Math.max(0, cfg.get<number>('memoryLongTermFiles', 6)),
contextLength: Math.max(2048, cfg.get<number>('contextLength', 32768)),
maxOutputTokens: Math.max(256, cfg.get<number>('maxOutputTokens', 4096)),
contextSafetyMargin: Math.max(0, cfg.get<number>('contextSafetyMargin', 2048)),
contextOverflowPolicy: ((): IAgentConfig['contextOverflowPolicy'] => {
const v = cfg.get<string>('contextOverflowPolicy', 'stopAtLimit');
return v === 'truncateMiddle' || v === 'rollingWindow' ? v : 'stopAtLimit';
})(),
autoCompactHistory: cfg.get<boolean>('autoCompactHistory', true),
smallModelContextCap: Math.max(0, cfg.get<number>('smallModelContextCap', 0)),
autoContinueOnOutputLimit: cfg.get<boolean>('autoContinueOnOutputLimit', true),
maxAutoContinuations: Math.max(0, Math.min(10, cfg.get<number>('maxAutoContinuations', 4))),
finalOnlyRetryOnThoughtLeak: cfg.get<boolean>('finalOnlyRetryOnThoughtLeak', true),
embeddingModel: (cfg.get<string>('embeddingModel', '') || '').trim(),
embeddingBlendAlpha: Math.max(0, Math.min(1, cfg.get<number>('embeddingBlendAlpha', 0.5))),
knowledgeMixSecondBrainWeight: Math.max(0, Math.min(100, Math.round(
cfg.get<number>('knowledgeMix.secondBrainWeight', 50)
))),
enableReflection: cfg.get<boolean>('enableReflection', true),
autoLessonFromReflection: cfg.get<boolean>('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'
]);