Files
connectai/src/config.ts
T
koriweb ebfce17b03 fix: v2.2.203 — 기업모드 dev-impl 빈 깡통 99% 버그 (hollow check 기본 ON)
증상: 사용자가 기획서 + 폴더 주고 "여기 개발해줘" 요청 → ASTRA 가 파일 만들고
"개발 완료" 보고 → 실제 파일을 열면 class/함수 본문이 비어 있음
(def foo(): pass · 빈 class · imports only). 99% 확률 재발.

원인:
- 안전망 이미 존재 (selfReflectorHollow.ts 가 정규식으로 빈 깡통 감지)
- BUT 두 개 config 모두 OFF — selfReflectorEnabled (Phase A) +
  selfReflectorExternalEnabled (Phase B)
- Phase A 켜면 [Self-Reflector Check] 블록이 답변에 노출 (UX 부작용),
  Phase B 는 +1 LLM 호출 비용 — 부작용 때문에 기본 OFF
- 결과: 다수 사용자가 안전망 전혀 없는 상태로 코드 작성 → 빈 깡통 통과

Fix 3종:

1. Hollow check 를 selfReflector 와 분리 — 신규 설정 2개:
   - g1nation.hollowCheck.enabled (boolean, 기본 ON) — action-tag 있는 모든
     응답에 무조건 hollow 스캔. LLM 호출 0.
   - g1nation.hollowCheck.autoRetry (boolean, 기본 ON) — 검출 시 1회 자동
     재작업. Phase B 와 분리.
   - dispatcher.ts 게이트 조건 교체

2. dev-impl 프롬프트 강화 (pipelineTemplates.ts) — [빈 깡통 금지] 5개 규칙:
   - 파일은 하나씩 생성, 모든 함수 본문 완전 구현 후 다음 파일로
   - 금지 패턴 명시: pass · ... · NotImplementedError · # TODO · 빈 class
   - 인터페이스/추상 메서드만 빈 본문 OK
   - 각 파일 생성 직후 자가 검증
   - 최종 요약에 파일별 핵심 동작 한 줄씩

3. 기본값 변경 — 사용자 행동 없이 안전망 작동. 옛 selfReflector Phase A/B 는
   그대로 OFF (UX 부작용 보존).

예상 효과: ~70-85% stub 감소. 남은 ~15% (작은 모델 attention 한계 / 큰 프로젝트)
는 per-file 순차 생성으로 v2.2.204+ 검토.

모델 한계 vs 로직 fix: 대부분 로직 fix 가능. 매우 작은 모델 (≤4B) 은 한계 더
빠름 — 더 큰 모델 (gemma-12b, qwen-32b) 권장.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-02 11:52:12 +09:00

576 lines
31 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;
/** 채팅 응답 생성 temperature. 낮을수록 한국어 오타·깨진 토큰이 줄어든다. */
chatTemperature: 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;
/**
* Conflict Surface — 검색된 출처의 conflictSeverity 신호를 [CONFLICT WARNINGS] 블록
* 으로 시스템 프롬프트에 노출. v4 정책 텍스트(buildAstraModeSystemPrompt) 가 이미
* "[CONFLICT WARNING] 플래그" 를 참조하지만, 데이터를 LLM 에 전달하지 않아 무용했음.
* true(기본) → 충돌 감지 시 블록 주입, false → 비활성.
*/
conflictHighlightingEnabled: boolean;
/**
* Conflict 자기-신호 surface 시 최소 severity 임계.
* 'low' → LOW 이상 (가장 민감, 노이즈 가능)
* 'medium' → MEDIUM 이상 (기본, 균형)
* 'high' → HIGH 만 (가장 보수적, 강한 충돌만)
*/
conflictSeverityThreshold: 'low' | 'medium' | 'high';
/**
* 교차-문서 발산 감지 (같은 주제 ≥2 chunks, 본문 Jaccard < 0.30 인 잠재 모순 쌍).
* 자기-신호와 합쳐 [CONFLICT WARNINGS] 블록에 표시.
*/
conflictCrossDocEnabled: boolean;
/**
* CoVe (Chain-of-Verification) — 답변 *작성 전* 그라운딩 체크리스트를 시스템 프롬프트에
* 주입해 모델이 self-verify 하도록. 할루시네이션 방지 + 그라운딩 명확화.
* true(기본) → 검색된 출처 있을 때 [VERIFICATION CHECKLIST] 블록 주입.
*/
coveEnabled: boolean;
/** CoVe 체크리스트에 나열할 상위 출처 개수. 기본 5. */
coveTopSourcesCount: number;
/**
* CoVe Strict 모드 — 모든 사실 주장 뒤에 출처 ID([S1] 등) inline 인용 강제.
* 답변이 좀 더 학술적·verbose 해질 수 있어 기본 off.
*/
coveStrictMode: boolean;
/**
* Actionability — "현재 작업 상태" 신호(최근 슬래시 명령 + 열린 파일) 로 검색 결과
* 재가중. TF-IDF 매치 점수에 actionability boost 추가해 "지금 작업 중인 컨텍스트" 와
* 직접 연결된 문서를 우선. 기본 true.
*/
actionabilityEnabled: boolean;
/**
* Memory Lifecycle — Distillation Loop: stale Episodic Memory 를 LongTerm 'episode-digest'
* 로 승급. 누적 epimemory 가 검색 노이즈가 되는 것 방지.
* true(기본) → /memory distill + 세션 종료 시 자동 트리거 (interval 기준).
*/
distillationEnabled: boolean;
/** 며칠 이상 지난 episode 를 distill 대상으로. 기본 30. */
distillationAgeThresholdDays: number;
/** 자동 distillation 의 최소 간격 (일). 너무 자주 안 돌도록. 기본 7. */
distillationIntervalDays: number;
/**
* Archive 모드:
* - 'mark-promoted' (기본): promoted=true 마킹만, 파일 보존
* - 'archive-file': promoted 마킹 + 파일을 memory/episodes/archive/ 로 이동
*/
distillationArchiveMode: 'mark-promoted' | 'archive-file';
/**
* Hierarchical Context Window — 질의·문서 추상도(concrete/operational/strategic) 매칭으로
* 검색 결과 재가중. 같은 레벨 boost (× 1.15), 양 끝 mismatch penalty (× 0.7). LLM 호출 없음.
*/
hierarchicalReweightEnabled: boolean;
/**
* Semantic Re-ranking — 토큰 예산 통과한 selectedChunks 의 *순서* 를 LLM 한 번 호출로
* 재정렬. 의도 매치를 키워드 매치보다 우선. 매 turn 1회 추가 LLM 호출 (latency 비용).
* 기본 OFF — 명시적으로 on 해야 함.
*/
semanticRerankEnabled: boolean;
/** 재정렬 전용 모델 ID. 비면 defaultModel 사용. 빠른 작은 모델 권장. */
semanticRerankModel: string;
/** Re-rank 대상 상위 후보 개수. 기본 15. */
semanticRerankCandidateK: number;
/** Re-rank LLM 호출 타임아웃 (초). 기본 8. */
semanticRerankTimeoutSec: number;
/**
* Intent Clarification — 모호 질의에서 *추측 답변 대신 역질문* 지시.
* 휴리스틱 차원(환경/대상/범위/포맷/마감) 별 trigger + specifier 매치. 기본 true.
*/
intentClarificationEnabled: boolean;
/**
* 모호 판정 임계:
* - 'low': 2개 이상 missing dimension 일 때만 ambiguous (가장 덜 묻기)
* - 'medium' (기본): 1개 이상 missing → ambiguous
* - 'high': 1개 이상 missing OR 짧은 prompt(<20자)+trigger 있으면 ambiguous (가장 자주 묻기)
*/
intentClarificationStrictness: 'low' | 'medium' | 'high';
/**
* Citation Trace — 답변 끝에 "출처:" 한 줄 정리 지시. CoVe Strict 의 가벼운 형제.
* 검색 결과 있을 때만 동작. 기본 true.
*/
citationTraceEnabled: boolean;
/**
* Post-hoc Self-Check — 답변 *완료 후* LLM 1회 호출로 검증 (답변 직접도 / 그라운딩 /
* 논리 모순). 매 turn 추가 LLM 호출 (latency 비용). 기본 OFF — 명시적 opt-in.
* 결과는 답변 아래 footer 한 줄로 표시.
*/
selfCheckEnabled: boolean;
/** Self-check 전용 모델 ID. 비면 defaultModel. 빠른 작은 모델 권장. */
selfCheckModel: string;
/** Self-check LLM 호출 타임아웃 (초). 기본 6. */
selfCheckTimeoutSec: number;
/**
* Terminology Dictionary — 사용자 편집 글로서리 파일을 시스템 프롬프트에 주입,
* 표준 표기 강제 + 답변 직전 자기 점검(Term Check). 기본 true. 파일 없으면 자동 no-op.
*/
glossaryEnabled: boolean;
/** Glossary 파일 상대 경로 (workspace root 기준). 기본 '.astra/glossary.md'. */
glossaryPath: string;
/** Glossary 본문 시스템 프롬프트 cap (chars). 너무 크면 토큰 비용↑. 기본 4000. */
glossaryMaxBodyLength: number;
/**
* Post-gen Term Validator — 답변 완료 후 글로서리 forbidden 단어 결정론적 스캔.
* Terminology Dictionary (v2.2.192) 의 *instructional* 지시를 *deterministic* 검증으로 보완.
* LLM 호출 없음 (정규식), 매 turn 안전 실행. footer 한 줄 표시. 기본 true.
*/
termValidatorEnabled: boolean;
/**
* 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 호출 절약 (저성능 모델/저지연 우선 시).
*/
/**
* 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;
/**
* Hollow Code Check — `<create_file>` 등 action-tag 로 생성된 파일이 *빈 깡통*
* (empty class, stub-only function body, imports-only file 등) 인지 정규식으로
* 스캔. LLM 호출 0 — 휴리스틱 only.
*
* 옛 구조: selfReflector Phase A 가 켜져 있어야 동작 → 다수 사용자가 안전망 부재.
* 새 구조: 독립 toggle, *기본 ON*. action-tag 가 있는 응답에 무조건 실행. 검출 시
* actionReport 에 "⚠️ 빈 깡통" 라인 추가 (LLM 콜 없음 — 비용 0).
*/
hollowCheckEnabled: boolean;
/**
* Hollow 감지 시 1회 자동 재작업. Phase B (selfReflectorExternalEnabled) 와 분리 —
* dev-impl 같은 stage 당 정해진 단발 추가 호출 (예측 가능). 기본 ON.
* 재작업 결과도 hollow 한 번 더 검사 → 재차 hollow 면 사용자 경고 (무한 루프 방지).
*/
hollowCheckAutoRetry: 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;
/**
* Multi-Agent 발동 모드:
* - 'auto' (기본): 작은 모델(≤4B) 감지 OR prompt가 컨텍스트의 큰 비중을 차지할 때만 자동 발동.
* - 'always': 인사·짧은 잡담을 제외한 모든 요청에 5단계 파이프라인 사용.
* - 'off': 기존 single-agent 동작 (수동 토글 / 키워드 매칭만 사용).
*/
workflowMultiAgentMode: 'auto' | 'always' | 'off';
/**
* 'auto' 모드에서 prompt + brain context 토큰이 contextLength 의 이 비율(0~1)을 넘으면 강제 5단계.
* 기본 0.30 — 작은 모델이 30% 이상을 input으로 먹기 시작하면 한 번에 끝내려는 시도가 위험.
*/
workflowAutoCtxFractionThreshold: number;
/**
* 절대 토큰 임계값 — 입력 prompt 가 이 값 *미만* 이면 Multi-Agent 파이프라인 발동
* 안 함 (키워드·길이 트리거 무시). 모델이 단일 호출로 처리.
*
* 의도: 사용자가 "요약/리뷰" 같은 키워드만 써도 chunked 가 강제로 발동해
* LLM 여러 번 호출되며 답변이 느려지는 문제 해결. 입력이 모델 윈도우 대비
* 충분히 작으면 한 번에 답하는 게 합리적.
*
* 기본 50000 — 대부분의 사용 환경에 적합. 매우 작은 컨텍스트 모델로 큰 입력을
* 자주 다룬다면 OOM 방지 차원에서 사용자가 직접 낮출 수 있음 (Astra Settings 패널).
*/
chunkedSwitchTokens: number;
/**
* Chunked 파이프라인 진입 시 outline 이 만들 수 있는 *최대 섹션 수*.
* 실제 LLM 호출 횟수 = 1(outline) + N(section) + 1(polish) = 2 + N.
* 따라서 이 값이 3이면 최대 5회, 4이면 최대 6회.
*
* 작을수록 답변 속도 빠름, 클수록 답변이 더 세분화. 기본 3 — 사용자
* 피드백("6회 이상은 과하다") 반영. 1~10 범위 clamp.
*/
chunkedMaxSections: number;
/**
* ChunkedWriter 의 polish persona 를 사용자가 override 할 텍스트. 비어 있으면
* 기본 polish persona (DEFAULT_POLISH_PERSONA) 사용. 입력이 있으면 그 텍스트가
* 그대로 system prompt 로 들어가 polish 단계의 톤·구조 룰을 정의.
*
* 의도: 사용자가 답변 톤을 (예: 격식체·반말·법률 문서·마케팅 카피) 도메인에
* 맞게 직접 조정 가능. 코드 변경 없이 Settings 패널 textarea 만으로.
*
* 빈 문자열 / 공백만이면 default 사용 — 잘못 입력해도 답변이 깨지진 않음.
*/
polishPersonaOverride: string;
// ─── Stream 표시 ───
/**
* 모델 토큰을 받는 즉시 채팅 버블에 흘려보낼지 여부.
* - false(기본): 토큰은 내부에서만 누적, sanitize 끝난 최종 답변만 한 번에 표시 → Harmony/think 마커 누설 원천 차단.
* - true: legacy 라이브 스트리밍. 모델 출력에 control token 이 섞여 나오면 잠깐 화면에 보일 수 있음.
*/
liveStreamTokens: boolean;
/**
* 최종 답변 포맷.
* - 'plain' (기본): 모델이 무심코 내보낸 `##`, `**`, `__`, `> `, `* ` 등의 마크다운 마커를 후처리로 모두 제거.
* 섹션 라벨 텍스트(예: "핵심 요약")는 유지되지만 헤더 마커는 사라져 깔끔한 plain text 로 표시.
* - 'markdown': legacy 동작. 모델 출력을 그대로 렌더러에 넘김.
*/
outputFormat: 'plain' | 'markdown';
/**
* 자동 기록 (project chronicle auto-record). true 면 매 prompt 후 의미 있는 turn 을
* Wiki/Chronicle 폴더에 자동으로 저장. false 면 자동 저장 OFF (수동 기록은 계속 가능).
* 사이드바 도구 드롭다운의 토글 항목으로 즉시 변경 가능.
*/
chronicleAutoRecord: boolean;
// ─── LM Studio sampling (applied to both SDK and REST paths) ───
/** LM Studio nucleus sampling cutoff (0~1). Lower tightens; 1 disables. */
lmStudioTopP: number;
/** LM Studio top-K cutoff (0 disables). */
lmStudioTopK: number;
/** LM Studio min-P floor (0~1, 0 disables). */
lmStudioMinP: number;
/** LM Studio repeat penalty (1 disables, 1.051.2 typical). */
lmStudioRepeatPenalty: number;
/** Render tok/s + TTFT from prediction stats into context-budget badge. */
lmStudioShowStatsInBudget: boolean;
/** LM Studio model key of a small draft model for speculative decoding ('' = disabled). */
lmStudioDraftModel: string;
/** Load-time options. Read once per load(); changing these after load needs a reload. */
lmStudioLoad: {
flashAttention: boolean;
/** "max" | "off" | number 0-1 */
gpuOffloadRatio: 'max' | 'off' | number;
offloadKVCacheToGpu: boolean;
keepModelInMemory: boolean;
useFp16ForKVCache: boolean;
/** 0 = engine default */
evalBatchSize: number;
};
}
// ─── 경로 정규화 유틸리티 ───
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),
chatTemperature: Math.min(2, Math.max(0, cfg.get<number>('chatTemperature', 0.3))),
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))),
conflictHighlightingEnabled: cfg.get<boolean>('conflictHighlightingEnabled', true),
conflictSeverityThreshold: (cfg.get<string>('conflictSeverityThreshold', 'medium') as 'low' | 'medium' | 'high') || 'medium',
conflictCrossDocEnabled: cfg.get<boolean>('conflictCrossDocEnabled', true),
coveEnabled: cfg.get<boolean>('coveEnabled', true),
coveTopSourcesCount: Math.max(1, Math.min(15, cfg.get<number>('coveTopSourcesCount', 5))),
coveStrictMode: cfg.get<boolean>('coveStrictMode', false),
actionabilityEnabled: cfg.get<boolean>('actionabilityEnabled', true),
distillationEnabled: cfg.get<boolean>('distillationEnabled', true),
distillationAgeThresholdDays: Math.max(1, Math.min(365, cfg.get<number>('distillationAgeThresholdDays', 30))),
distillationIntervalDays: Math.max(1, Math.min(90, cfg.get<number>('distillationIntervalDays', 7))),
distillationArchiveMode: (cfg.get<string>('distillationArchiveMode', 'mark-promoted') as 'mark-promoted' | 'archive-file') || 'mark-promoted',
hierarchicalReweightEnabled: cfg.get<boolean>('hierarchicalReweightEnabled', true),
semanticRerankEnabled: cfg.get<boolean>('semanticRerankEnabled', false),
semanticRerankModel: cfg.get<string>('semanticRerankModel', '') || '',
semanticRerankCandidateK: Math.max(2, Math.min(30, cfg.get<number>('semanticRerankCandidateK', 15))),
semanticRerankTimeoutSec: Math.max(1, Math.min(60, cfg.get<number>('semanticRerankTimeoutSec', 8))),
intentClarificationEnabled: cfg.get<boolean>('intentClarificationEnabled', true),
intentClarificationStrictness: (cfg.get<string>('intentClarificationStrictness', 'medium') as 'low' | 'medium' | 'high') || 'medium',
citationTraceEnabled: cfg.get<boolean>('citationTraceEnabled', true),
selfCheckEnabled: cfg.get<boolean>('selfCheckEnabled', false),
selfCheckModel: cfg.get<string>('selfCheckModel', '') || '',
selfCheckTimeoutSec: Math.max(1, Math.min(60, cfg.get<number>('selfCheckTimeoutSec', 6))),
glossaryEnabled: cfg.get<boolean>('glossaryEnabled', true),
glossaryPath: cfg.get<string>('glossaryPath', '.astra/glossary.md') || '.astra/glossary.md',
glossaryMaxBodyLength: Math.max(500, Math.min(20000, cfg.get<number>('glossaryMaxBodyLength', 4000))),
termValidatorEnabled: cfg.get<boolean>('termValidatorEnabled', true),
knowledgeMixSecondBrainWeight: Math.max(0, Math.min(100, Math.round(
cfg.get<number>('knowledgeMix.secondBrainWeight', 50)
))),
companyIntentClassifierModel: (cfg.get<string>('company.intentClassifierModel', '') || '').trim(),
companyDisableIntentClassifier: cfg.get<boolean>('company.disableIntentClassifier', false),
companyAutoSelectPipeline: cfg.get<boolean>('company.autoSelectPipeline', true),
companyIntentAlignmentMode: ((): 'off' | 'smart' | 'strict' => {
const v = (cfg.get<string>('company.intentAlignmentMode', 'smart') || 'smart').trim().toLowerCase();
return v === 'off' || v === 'strict' ? v : 'smart';
})(),
// ALIGNMENT_DEFAULT_MAX_ROUNDS 와 일치 — `intentAlignment` 모듈을 직접 import 하지 않은
// 이유는 config 가 features/ 아래 모듈을 의존하면 의도치 않은 순환 import 가 생기기 때문.
// 둘이 어긋나면 안 되므로 변경 시 양쪽 같이 갱신.
companyIntentAlignmentMaxRounds: Math.max(1, Math.min(5, cfg.get<number>('company.intentAlignmentMaxRounds', 3))),
selfReflectorEnabled: cfg.get<boolean>('selfReflector.enabled', false),
hollowCheckEnabled: cfg.get<boolean>('hollowCheck.enabled', true),
hollowCheckAutoRetry: cfg.get<boolean>('hollowCheck.autoRetry', true),
selfReflectorExternalEnabled: cfg.get<boolean>('selfReflector.externalVerification', false),
selfReflectorExecutionEnabled: cfg.get<boolean>('selfReflector.executionVerification', false),
companyPixelOfficeEnabled: cfg.get<boolean>('company.pixelOffice.enabled', true),
companyPixelOfficeBubbles: cfg.get<boolean>('company.pixelOffice.bubbles', true),
workflowMultiAgentMode: ((): 'auto' | 'always' | 'off' => {
const v = (cfg.get<string>('workflow.multiAgentMode', 'auto') || 'auto').trim().toLowerCase();
return v === 'always' || v === 'off' ? v : 'auto';
})(),
workflowAutoCtxFractionThreshold: Math.max(0.05, Math.min(0.95,
cfg.get<number>('workflow.autoCtxFractionThreshold', 0.30)
)),
chunkedSwitchTokens: Math.max(1000, cfg.get<number>('chunkedSwitchTokens', 50000)),
chunkedMaxSections: Math.max(1, Math.min(10, cfg.get<number>('chunkedMaxSections', 3))),
polishPersonaOverride: (cfg.get<string>('polishPersonaOverride', '') || '').trim(),
liveStreamTokens: cfg.get<boolean>('liveStreamTokens', true),
outputFormat: ((): 'plain' | 'markdown' => {
const v = (cfg.get<string>('outputFormat', 'plain') || 'plain').trim().toLowerCase();
return v === 'markdown' ? 'markdown' : 'plain';
})(),
chronicleAutoRecord: cfg.get<boolean>('chronicleAutoRecord', true),
lmStudioTopP: Math.max(0, Math.min(1, cfg.get<number>('lmStudio.sampling.topP', 0.9))),
lmStudioTopK: Math.max(0, cfg.get<number>('lmStudio.sampling.topK', 20)),
lmStudioMinP: Math.max(0, Math.min(1, cfg.get<number>('lmStudio.sampling.minP', 0.05))),
lmStudioRepeatPenalty: Math.max(1, Math.min(2, cfg.get<number>('lmStudio.sampling.repeatPenalty', 1.1))),
lmStudioShowStatsInBudget: cfg.get<boolean>('lmStudio.statsInBudget', true),
lmStudioDraftModel: (cfg.get<string>('lmStudio.draftModel', '') || '').trim(),
lmStudioLoad: {
flashAttention: cfg.get<boolean>('lmStudio.load.flashAttention', true),
gpuOffloadRatio: ((): 'max' | 'off' | number => {
const raw = (cfg.get<string>('lmStudio.load.gpuOffloadRatio', 'max') || 'max').trim().toLowerCase();
if (raw === 'max' || raw === 'off') return raw;
const n = Number(raw);
if (Number.isFinite(n)) return Math.max(0, Math.min(1, n));
return 'max';
})(),
offloadKVCacheToGpu: cfg.get<boolean>('lmStudio.load.offloadKVCacheToGpu', true),
keepModelInMemory: cfg.get<boolean>('lmStudio.load.keepModelInMemory', true),
useFp16ForKVCache: cfg.get<boolean>('lmStudio.load.useFp16ForKVCache', false),
evalBatchSize: Math.max(0, cfg.get<number>('lmStudio.load.evalBatchSize', 0)),
},
};
}
/**
* 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'
]);