@triflux/core 10.0.0-alpha.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/hooks/agent-route-guard.mjs +109 -0
- package/hooks/cross-review-tracker.mjs +122 -0
- package/hooks/error-context.mjs +148 -0
- package/hooks/hook-manager.mjs +352 -0
- package/hooks/hook-orchestrator.mjs +312 -0
- package/hooks/hook-registry.json +213 -0
- package/hooks/hooks.json +89 -0
- package/hooks/keyword-rules.json +581 -0
- package/hooks/lib/resolve-root.mjs +59 -0
- package/hooks/mcp-config-watcher.mjs +85 -0
- package/hooks/pipeline-stop.mjs +76 -0
- package/hooks/safety-guard.mjs +106 -0
- package/hooks/subagent-verifier.mjs +80 -0
- package/hub/assign-callbacks.mjs +133 -0
- package/hub/bridge.mjs +799 -0
- package/hub/cli-adapter-base.mjs +192 -0
- package/hub/codex-adapter.mjs +190 -0
- package/hub/codex-compat.mjs +78 -0
- package/hub/codex-preflight.mjs +147 -0
- package/hub/delegator/contracts.mjs +37 -0
- package/hub/delegator/index.mjs +14 -0
- package/hub/delegator/schema/delegator-tools.schema.json +250 -0
- package/hub/delegator/service.mjs +307 -0
- package/hub/delegator/tool-definitions.mjs +35 -0
- package/hub/fullcycle.mjs +96 -0
- package/hub/gemini-adapter.mjs +179 -0
- package/hub/hitl.mjs +143 -0
- package/hub/intent.mjs +193 -0
- package/hub/lib/process-utils.mjs +361 -0
- package/hub/middleware/request-logger.mjs +81 -0
- package/hub/paths.mjs +30 -0
- package/hub/pipeline/gates/confidence.mjs +56 -0
- package/hub/pipeline/gates/consensus.mjs +94 -0
- package/hub/pipeline/gates/index.mjs +5 -0
- package/hub/pipeline/gates/selfcheck.mjs +82 -0
- package/hub/pipeline/index.mjs +318 -0
- package/hub/pipeline/state.mjs +191 -0
- package/hub/pipeline/transitions.mjs +124 -0
- package/hub/platform.mjs +225 -0
- package/hub/quality/deslop.mjs +253 -0
- package/hub/reflexion.mjs +372 -0
- package/hub/research.mjs +146 -0
- package/hub/router.mjs +791 -0
- package/hub/routing/complexity.mjs +166 -0
- package/hub/routing/index.mjs +117 -0
- package/hub/routing/q-learning.mjs +336 -0
- package/hub/session-fingerprint.mjs +352 -0
- package/hub/state.mjs +245 -0
- package/hub/team-bridge.mjs +25 -0
- package/hub/token-mode.mjs +224 -0
- package/hub/workers/worker-utils.mjs +104 -0
- package/hud/colors.mjs +88 -0
- package/hud/constants.mjs +81 -0
- package/hud/hud-qos-status.mjs +206 -0
- package/hud/providers/claude.mjs +309 -0
- package/hud/providers/codex.mjs +151 -0
- package/hud/providers/gemini.mjs +320 -0
- package/hud/renderers.mjs +424 -0
- package/hud/terminal.mjs +140 -0
- package/hud/utils.mjs +287 -0
- package/package.json +31 -0
- package/scripts/lib/claudemd-manager.mjs +325 -0
- package/scripts/lib/context.mjs +67 -0
- package/scripts/lib/cross-review-utils.mjs +51 -0
- package/scripts/lib/env-probe.mjs +241 -0
- package/scripts/lib/gemini-profiles.mjs +85 -0
- package/scripts/lib/hook-utils.mjs +14 -0
- package/scripts/lib/keyword-rules.mjs +166 -0
- package/scripts/lib/logger.mjs +105 -0
- package/scripts/lib/mcp-filter.mjs +739 -0
- package/scripts/lib/mcp-guard-engine.mjs +940 -0
- package/scripts/lib/mcp-manifest.mjs +79 -0
- package/scripts/lib/mcp-server-catalog.mjs +118 -0
- package/scripts/lib/psmux-info.mjs +119 -0
- package/scripts/lib/remote-spawn-transfer.mjs +196 -0
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
// hub/routing/complexity.mjs — 작업 복잡도 스코어링
|
|
2
|
+
// 작업 설명 텍스트에서 복잡도를 0-1 범위로 계산한다.
|
|
3
|
+
// 외부 의존성 없음 (순수 텍스트 분석)
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* 복잡도 지표 키워드 사전
|
|
7
|
+
* 카테고리별 키워드와 가중치 (0-1)
|
|
8
|
+
*/
|
|
9
|
+
const COMPLEXITY_INDICATORS = {
|
|
10
|
+
// 높은 복잡도 (0.7-1.0)
|
|
11
|
+
high: {
|
|
12
|
+
keywords: [
|
|
13
|
+
'refactor', 'architecture', 'security', 'migration', 'distributed',
|
|
14
|
+
'concurrent', 'parallel', 'optimization', 'performance', 'scalability',
|
|
15
|
+
'cryptograph', 'encryption', 'authentication', 'authorization',
|
|
16
|
+
'database schema', 'data model', 'state machine', 'event-driven',
|
|
17
|
+
'microservice', 'orchestrat', 'pipeline', 'workflow',
|
|
18
|
+
// 한국어
|
|
19
|
+
'리팩터링', '리팩토링', '아키텍처', '보안', '마이그레이션', '분산',
|
|
20
|
+
'동시성', '병렬', '최적화', '성능', '확장성',
|
|
21
|
+
'암호화', '인증', '인가', '데이터베이스 스키마', '데이터 모델',
|
|
22
|
+
'상태 머신', '이벤트 드리븐', '마이크로서비스', '오케스트레이션',
|
|
23
|
+
],
|
|
24
|
+
weight: 0.85,
|
|
25
|
+
},
|
|
26
|
+
// 중간 복잡도 (0.4-0.7)
|
|
27
|
+
medium: {
|
|
28
|
+
keywords: [
|
|
29
|
+
'implement', 'integrate', 'api', 'endpoint', 'middleware',
|
|
30
|
+
'validation', 'error handling', 'testing', 'debug', 'fix bug',
|
|
31
|
+
'configuration', 'deploy', 'ci/cd', 'docker', 'container',
|
|
32
|
+
'cache', 'queue', 'webhook', 'notification', 'logging',
|
|
33
|
+
// 한국어
|
|
34
|
+
'구현', '통합', '엔드포인트', '미들웨어', '유효성 검사',
|
|
35
|
+
'에러 처리', '오류 처리', '테스트', '디버깅', '버그 수정',
|
|
36
|
+
'설정', '배포', '컨테이너', '캐시', '알림', '로깅',
|
|
37
|
+
],
|
|
38
|
+
weight: 0.55,
|
|
39
|
+
},
|
|
40
|
+
// 낮은 복잡도 (0.1-0.4)
|
|
41
|
+
low: {
|
|
42
|
+
keywords: [
|
|
43
|
+
'readme', 'comment', 'typo', 'rename', 'format', 'lint',
|
|
44
|
+
'update version', 'bump', 'add dependency', 'install',
|
|
45
|
+
'simple', 'trivial', 'minor', 'small change', 'one-liner',
|
|
46
|
+
// 한국어
|
|
47
|
+
'문서화', '주석', '오타', '이름 변경', '포맷', '버전 업데이트',
|
|
48
|
+
'의존성 추가', '설치', '간단', '사소한', '소규모', '한 줄',
|
|
49
|
+
],
|
|
50
|
+
weight: 0.2,
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 어휘 복잡도 계산 (20%)
|
|
56
|
+
* 고유 단어 비율 + 평균 단어 길이 기반
|
|
57
|
+
* @param {string[]} words
|
|
58
|
+
* @returns {number} 0-1
|
|
59
|
+
*/
|
|
60
|
+
function lexicalComplexity(words) {
|
|
61
|
+
if (words.length === 0) return 0;
|
|
62
|
+
const unique = new Set(words);
|
|
63
|
+
const typeTokenRatio = unique.size / words.length;
|
|
64
|
+
const avgWordLen = words.reduce((sum, w) => sum + w.length, 0) / words.length;
|
|
65
|
+
// 긴 단어(기술 용어)가 많을수록 복잡
|
|
66
|
+
const lenScore = Math.min(avgWordLen / 10, 1);
|
|
67
|
+
return typeTokenRatio * 0.5 + lenScore * 0.5;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* 시맨틱 깊이 계산 (35%)
|
|
72
|
+
* 키워드 사전 매칭 기반
|
|
73
|
+
* @param {string} text
|
|
74
|
+
* @returns {number} 0-1
|
|
75
|
+
*/
|
|
76
|
+
function semanticDepth(text) {
|
|
77
|
+
const lower = text.toLowerCase();
|
|
78
|
+
let maxWeight = 0;
|
|
79
|
+
let matchCount = 0;
|
|
80
|
+
|
|
81
|
+
for (const [, category] of Object.entries(COMPLEXITY_INDICATORS)) {
|
|
82
|
+
for (const kw of category.keywords) {
|
|
83
|
+
if (lower.includes(kw)) {
|
|
84
|
+
matchCount++;
|
|
85
|
+
if (category.weight > maxWeight) maxWeight = category.weight;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
// 매칭 키워드 수와 최고 가중치 조합
|
|
90
|
+
const countScore = Math.min(matchCount / 5, 1);
|
|
91
|
+
return maxWeight * 0.6 + countScore * 0.4;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* 작업 범위 계산 (25%)
|
|
96
|
+
* 문장 수, 줄 수, 파일/경로 참조 기반
|
|
97
|
+
* @param {string} text
|
|
98
|
+
* @returns {number} 0-1
|
|
99
|
+
*/
|
|
100
|
+
function taskScope(text) {
|
|
101
|
+
const lines = text.split('\n').filter((l) => l.trim().length > 0);
|
|
102
|
+
const sentences = text.split(/[.!?]+/).filter((s) => s.trim().length > 0);
|
|
103
|
+
const fileRefs = (text.match(/[\w\-/]+\.\w{1,5}/g) || []).length;
|
|
104
|
+
|
|
105
|
+
const lineScore = Math.min(lines.length / 20, 1);
|
|
106
|
+
const sentenceScore = Math.min(sentences.length / 10, 1);
|
|
107
|
+
const fileScore = Math.min(fileRefs / 5, 1);
|
|
108
|
+
|
|
109
|
+
return lineScore * 0.3 + sentenceScore * 0.3 + fileScore * 0.4;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* 불확실성 계산 (20%)
|
|
114
|
+
* 모호한 표현, 질문, 조건문 기반
|
|
115
|
+
* @param {string} text
|
|
116
|
+
* @returns {number} 0-1
|
|
117
|
+
*/
|
|
118
|
+
function uncertainty(text) {
|
|
119
|
+
const lower = text.toLowerCase();
|
|
120
|
+
const uncertainWords = [
|
|
121
|
+
'maybe', 'perhaps', 'might', 'could', 'possibly', 'unclear',
|
|
122
|
+
'not sure', 'investigate', 'explore', 'research', 'try',
|
|
123
|
+
'consider', 'evaluate', 'assess', 'determine', 'figure out',
|
|
124
|
+
];
|
|
125
|
+
let count = 0;
|
|
126
|
+
for (const w of uncertainWords) {
|
|
127
|
+
if (lower.includes(w)) count++;
|
|
128
|
+
}
|
|
129
|
+
const questions = (text.match(/\?/g) || []).length;
|
|
130
|
+
const wordScore = Math.min(count / 4, 1);
|
|
131
|
+
const questionScore = Math.min(questions / 3, 1);
|
|
132
|
+
return wordScore * 0.6 + questionScore * 0.4;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* 작업 복잡도 스코어링
|
|
137
|
+
* @param {string} taskDescription — 작업 설명 텍스트
|
|
138
|
+
* @returns {{ score: number, breakdown: { lexical: number, semantic: number, scope: number, uncertainty: number } }}
|
|
139
|
+
*/
|
|
140
|
+
export function scoreComplexity(taskDescription) {
|
|
141
|
+
if (!taskDescription || typeof taskDescription !== 'string') {
|
|
142
|
+
return { score: 0, breakdown: { lexical: 0, semantic: 0, scope: 0, uncertainty: 0 } };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const words = taskDescription.toLowerCase().split(/\s+/).filter((w) => w.length > 0);
|
|
146
|
+
const lexical = lexicalComplexity(words);
|
|
147
|
+
const semantic = semanticDepth(taskDescription);
|
|
148
|
+
const scope = taskScope(taskDescription);
|
|
149
|
+
const uncertain = uncertainty(taskDescription);
|
|
150
|
+
|
|
151
|
+
// 가중 합산: 어휘(20%) + 시맨틱(35%) + 범위(25%) + 불확실성(20%)
|
|
152
|
+
const score = Math.min(
|
|
153
|
+
lexical * 0.20 + semantic * 0.35 + scope * 0.25 + uncertain * 0.20,
|
|
154
|
+
1,
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
score: Math.round(score * 1000) / 1000,
|
|
159
|
+
breakdown: {
|
|
160
|
+
lexical: Math.round(lexical * 1000) / 1000,
|
|
161
|
+
semantic: Math.round(semantic * 1000) / 1000,
|
|
162
|
+
scope: Math.round(scope * 1000) / 1000,
|
|
163
|
+
uncertainty: Math.round(uncertain * 1000) / 1000,
|
|
164
|
+
},
|
|
165
|
+
};
|
|
166
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// hub/routing/index.mjs — 통합 라우팅 진입점
|
|
2
|
+
// Q-Learning 동적 라우팅 + agent-map.json 정적 폴백
|
|
3
|
+
// 환경변수 TRIFLUX_DYNAMIC_ROUTING=true 로 옵트인 (기본 false)
|
|
4
|
+
|
|
5
|
+
import { createRequire } from 'node:module';
|
|
6
|
+
import { QLearningRouter } from './q-learning.mjs';
|
|
7
|
+
import { scoreComplexity } from './complexity.mjs';
|
|
8
|
+
|
|
9
|
+
const _require = createRequire(import.meta.url);
|
|
10
|
+
|
|
11
|
+
/** agent-map.json 정적 매핑 */
|
|
12
|
+
let AGENT_MAP;
|
|
13
|
+
try {
|
|
14
|
+
AGENT_MAP = _require('../team/agent-map.json');
|
|
15
|
+
} catch {
|
|
16
|
+
AGENT_MAP = {};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** 싱글턴 라우터 인스턴스 (lazy init) */
|
|
20
|
+
let _router = null;
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* 라우터 인스턴스 획득 (lazy singleton)
|
|
24
|
+
* @returns {QLearningRouter}
|
|
25
|
+
*/
|
|
26
|
+
function getRouter() {
|
|
27
|
+
if (!_router) {
|
|
28
|
+
_router = new QLearningRouter();
|
|
29
|
+
_router.load();
|
|
30
|
+
}
|
|
31
|
+
return _router;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 동적 라우팅 활성화 여부
|
|
36
|
+
* @returns {boolean}
|
|
37
|
+
*/
|
|
38
|
+
function isDynamicRoutingEnabled() {
|
|
39
|
+
const env = process.env.TRIFLUX_DYNAMIC_ROUTING;
|
|
40
|
+
return env === 'true' || env === '1';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 통합 라우팅 결정
|
|
45
|
+
* 우선순위: Q-Learning 예측 (신뢰도 >= 0.6) -> agent-map.json 기본값
|
|
46
|
+
*
|
|
47
|
+
* @param {string} agentType — 에이전트 역할명 ("executor", "designer" 등)
|
|
48
|
+
* @param {string} [taskDescription=''] — 작업 설명 (동적 라우팅용)
|
|
49
|
+
* @returns {{ cliType: string, source: 'dynamic' | 'static', confidence: number, complexity: number }}
|
|
50
|
+
*/
|
|
51
|
+
export function resolveRoute(agentType, taskDescription = '') {
|
|
52
|
+
// 정적 기본값
|
|
53
|
+
const staticCli = AGENT_MAP[agentType] || agentType;
|
|
54
|
+
const { score: complexity } = scoreComplexity(taskDescription);
|
|
55
|
+
|
|
56
|
+
// 동적 라우팅 비활성화 시 정적 매핑 반환
|
|
57
|
+
if (!isDynamicRoutingEnabled()) {
|
|
58
|
+
return { cliType: staticCli, source: 'static', confidence: 1, complexity };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// 작업 설명 없으면 정적 폴백
|
|
62
|
+
if (!taskDescription || taskDescription.trim().length === 0) {
|
|
63
|
+
return { cliType: staticCli, source: 'static', confidence: 1, complexity };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const router = getRouter();
|
|
67
|
+
const prediction = router.predict(taskDescription);
|
|
68
|
+
|
|
69
|
+
// 신뢰도 기준 미달 시 정적 폴백
|
|
70
|
+
if (prediction.confidence < 0.6) {
|
|
71
|
+
return { cliType: staticCli, source: 'static', confidence: prediction.confidence, complexity };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
cliType: prediction.action,
|
|
76
|
+
source: 'dynamic',
|
|
77
|
+
confidence: prediction.confidence,
|
|
78
|
+
complexity,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* 라우팅 피드백 업데이트
|
|
84
|
+
* @param {string} taskDescription — 작업 설명
|
|
85
|
+
* @param {string} action — 수행한 CLI 타입
|
|
86
|
+
* @param {number} reward — 보상 (-1 ~ 1)
|
|
87
|
+
* @param {boolean} [persist=true] — 영속화 여부
|
|
88
|
+
*/
|
|
89
|
+
export function updateRoute(taskDescription, action, reward, persist = true) {
|
|
90
|
+
if (!isDynamicRoutingEnabled()) return;
|
|
91
|
+
|
|
92
|
+
const router = getRouter();
|
|
93
|
+
router.update(taskDescription, action, reward);
|
|
94
|
+
if (persist) router.save();
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* 라우터 상태 조회 (진단용)
|
|
99
|
+
* @returns {{ enabled: boolean, epsilon: number, totalUpdates: number, stateCount: number }}
|
|
100
|
+
*/
|
|
101
|
+
export function routerStatus() {
|
|
102
|
+
const enabled = isDynamicRoutingEnabled();
|
|
103
|
+
if (!enabled) {
|
|
104
|
+
return { enabled, epsilon: 0, totalUpdates: 0, stateCount: 0 };
|
|
105
|
+
}
|
|
106
|
+
const router = getRouter();
|
|
107
|
+
return {
|
|
108
|
+
enabled,
|
|
109
|
+
epsilon: router.epsilon,
|
|
110
|
+
totalUpdates: router.totalUpdates,
|
|
111
|
+
stateCount: router.stateCount,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// re-export for convenience
|
|
116
|
+
export { scoreComplexity } from './complexity.mjs';
|
|
117
|
+
export { QLearningRouter, ACTIONS } from './q-learning.mjs';
|
|
@@ -0,0 +1,336 @@
|
|
|
1
|
+
// hub/routing/q-learning.mjs — 테이블 기반 Q-Learning 동적 라우팅
|
|
2
|
+
// agent-map.json 폴백을 유지하면서, 작업 결과 피드백으로 가중치를 학습한다.
|
|
3
|
+
// 외부 의존성 없음 (fs, path, crypto만 사용)
|
|
4
|
+
|
|
5
|
+
import { readFileSync, writeFileSync, mkdirSync, existsSync } from 'node:fs';
|
|
6
|
+
import { join } from 'node:path';
|
|
7
|
+
import { createHash } from 'node:crypto';
|
|
8
|
+
import { createRequire } from 'node:module';
|
|
9
|
+
import { homedir } from 'node:os';
|
|
10
|
+
import { scoreComplexity } from './complexity.mjs';
|
|
11
|
+
|
|
12
|
+
const _require = createRequire(import.meta.url);
|
|
13
|
+
|
|
14
|
+
/** agent-map.json 정적 매핑 (폴백용) */
|
|
15
|
+
let AGENT_MAP;
|
|
16
|
+
try {
|
|
17
|
+
AGENT_MAP = _require('../team/agent-map.json');
|
|
18
|
+
} catch {
|
|
19
|
+
AGENT_MAP = {};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** 사용 가능한 CLI 액션 */
|
|
23
|
+
const ACTIONS = ['codex', 'gemini', 'claude', 'haiku', 'sonnet'];
|
|
24
|
+
|
|
25
|
+
/** 특성 벡터 키워드 (48차원, 에이전트 타입 기반) */
|
|
26
|
+
const FEATURE_KEYWORDS = [
|
|
27
|
+
// 실행/구현 (codex 친화)
|
|
28
|
+
'implement', 'execute', 'build', 'fix', 'debug', 'code', 'refactor', 'test',
|
|
29
|
+
// 분석/설계 (claude/sonnet 친화)
|
|
30
|
+
'analyze', 'architect', 'plan', 'review', 'security', 'optimize', 'research', 'evaluate',
|
|
31
|
+
// 디자인/문서 (gemini 친화)
|
|
32
|
+
'design', 'ui', 'ux', 'frontend', 'visual', 'document', 'write', 'explain',
|
|
33
|
+
// 간단/빠른 (haiku 친화)
|
|
34
|
+
'simple', 'quick', 'trivial', 'rename', 'format', 'lint', 'typo', 'minor',
|
|
35
|
+
// 한국어 — 실행/구현 (codex 친화)
|
|
36
|
+
'구현', '빌드', '수정', '디버깅', '리팩터링', '테스트',
|
|
37
|
+
// 한국어 — 분석/설계 (claude/sonnet 친화)
|
|
38
|
+
'분석', '아키텍처', '설계', '검토', '보안', '최적화',
|
|
39
|
+
// 한국어 — 디자인/문서 (gemini 친화)
|
|
40
|
+
'디자인', '문서화',
|
|
41
|
+
// 한국어 — 간단/빠른 (haiku 친화)
|
|
42
|
+
'간단', '사소한',
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* 텍스트에서 48차원 특성 벡터 추출
|
|
47
|
+
* 단어 경계를 기준으로 매칭하여 부분 문자열 오탐을 방지한다.
|
|
48
|
+
* @param {string} text
|
|
49
|
+
* @returns {number[]} 48-dim binary feature vector
|
|
50
|
+
*/
|
|
51
|
+
function extractFeatures(text) {
|
|
52
|
+
const lower = text.toLowerCase();
|
|
53
|
+
return FEATURE_KEYWORDS.map((kw) => {
|
|
54
|
+
// 영문 단일 단어: 단어 경계(\b) 적용
|
|
55
|
+
// 한국어 또는 다중 단어 구문: 공백/문장 경계 기반 포함 여부 확인
|
|
56
|
+
if (/^[a-z]+$/.test(kw)) {
|
|
57
|
+
return new RegExp(`\\b${kw}\\b`).test(lower) ? 1 : 0;
|
|
58
|
+
}
|
|
59
|
+
return lower.includes(kw) ? 1 : 0;
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* 특성 벡터를 상태 키로 변환 (해시 기반)
|
|
65
|
+
* @param {number[]} features
|
|
66
|
+
* @returns {string}
|
|
67
|
+
*/
|
|
68
|
+
function stateKey(features) {
|
|
69
|
+
const hash = createHash('sha256').update(features.join(',')).digest('hex');
|
|
70
|
+
return hash.slice(0, 16);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* LRU 캐시 (예측 결과 캐싱)
|
|
75
|
+
*/
|
|
76
|
+
class LRUCache {
|
|
77
|
+
/** @param {number} maxSize @param {number} ttlMs */
|
|
78
|
+
constructor(maxSize = 256, ttlMs = 5 * 60 * 1000) {
|
|
79
|
+
this._maxSize = maxSize;
|
|
80
|
+
this._ttlMs = ttlMs;
|
|
81
|
+
/** @type {Map<string, { value: *, ts: number }>} */
|
|
82
|
+
this._cache = new Map();
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
get(key) {
|
|
86
|
+
const entry = this._cache.get(key);
|
|
87
|
+
if (!entry) return undefined;
|
|
88
|
+
if (Date.now() - entry.ts > this._ttlMs) {
|
|
89
|
+
this._cache.delete(key);
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
// LRU: 다시 삽입하여 순서 갱신
|
|
93
|
+
this._cache.delete(key);
|
|
94
|
+
this._cache.set(key, entry);
|
|
95
|
+
return entry.value;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
set(key, value) {
|
|
99
|
+
if (this._cache.has(key)) this._cache.delete(key);
|
|
100
|
+
this._cache.set(key, { value, ts: Date.now() });
|
|
101
|
+
// 초과 시 가장 오래된 항목 제거
|
|
102
|
+
if (this._cache.size > this._maxSize) {
|
|
103
|
+
const oldest = this._cache.keys().next().value;
|
|
104
|
+
this._cache.delete(oldest);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
clear() {
|
|
109
|
+
this._cache.clear();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Q-Learning 라우터
|
|
115
|
+
* 테이블 기반 Q-Learning으로 작업→CLI 매핑을 학습한다.
|
|
116
|
+
*/
|
|
117
|
+
export class QLearningRouter {
|
|
118
|
+
/**
|
|
119
|
+
* @param {object} [opts]
|
|
120
|
+
* @param {number} [opts.learningRate=0.1] — 학습률 (alpha)
|
|
121
|
+
* @param {number} [opts.discountFactor=0.9] — 할인율 (gamma)
|
|
122
|
+
* @param {number} [opts.epsilon=0.3] — 탐색 확률 (epsilon-greedy)
|
|
123
|
+
* @param {number} [opts.epsilonDecay=0.995] — 엡실론 감쇠율
|
|
124
|
+
* @param {number} [opts.epsilonMin=0.05] — 최소 엡실론
|
|
125
|
+
* @param {number} [opts.minConfidence=0.6] — 최소 신뢰도 (이하면 폴백)
|
|
126
|
+
* @param {string} [opts.modelPath] — Q-table 영속화 경로
|
|
127
|
+
*/
|
|
128
|
+
constructor(opts = {}) {
|
|
129
|
+
this._lr = opts.learningRate ?? 0.1;
|
|
130
|
+
this._gamma = opts.discountFactor ?? 0.9;
|
|
131
|
+
this._epsilon = opts.epsilon ?? 0.3;
|
|
132
|
+
this._epsilonDecay = opts.epsilonDecay ?? 0.995;
|
|
133
|
+
// epsilon=0 시에도 최소 탐색 보장 (pure-exploit 방지)
|
|
134
|
+
this._epsilonMin = opts.epsilonMin ?? Math.max(0.01, Math.min(0.05, this._epsilon));
|
|
135
|
+
this._minConfidence = opts.minConfidence ?? 0.6;
|
|
136
|
+
this._modelPath = opts.modelPath ?? join(homedir(), '.omc', 'routing-model.json');
|
|
137
|
+
|
|
138
|
+
/** @type {Map<string, Map<string, number>>} state -> (action -> Q-value) */
|
|
139
|
+
this._qTable = new Map();
|
|
140
|
+
|
|
141
|
+
/** @type {Map<string, number>} state -> visit count */
|
|
142
|
+
this._visitCounts = new Map();
|
|
143
|
+
|
|
144
|
+
/** 총 업데이트 횟수 */
|
|
145
|
+
this._totalUpdates = 0;
|
|
146
|
+
|
|
147
|
+
/** 예측 캐시 */
|
|
148
|
+
this._cache = new LRUCache(256, 5 * 60 * 1000);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 상태에 대한 Q-values 조회 (없으면 초기화)
|
|
153
|
+
* @param {string} state
|
|
154
|
+
* @returns {Map<string, number>}
|
|
155
|
+
*/
|
|
156
|
+
_getQValues(state) {
|
|
157
|
+
if (!this._qTable.has(state)) {
|
|
158
|
+
const qValues = new Map();
|
|
159
|
+
for (const action of ACTIONS) {
|
|
160
|
+
qValues.set(action, 0);
|
|
161
|
+
}
|
|
162
|
+
this._qTable.set(state, qValues);
|
|
163
|
+
}
|
|
164
|
+
return this._qTable.get(state);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* 작업 설명으로부터 최적 CLI 타입 예측
|
|
169
|
+
* @param {string} taskDescription
|
|
170
|
+
* @returns {{ action: string, confidence: number, exploration: boolean, complexity: number }}
|
|
171
|
+
*/
|
|
172
|
+
predict(taskDescription) {
|
|
173
|
+
const features = extractFeatures(taskDescription);
|
|
174
|
+
const state = stateKey(features);
|
|
175
|
+
|
|
176
|
+
// 캐시 확인
|
|
177
|
+
const cached = this._cache.get(state);
|
|
178
|
+
if (cached) return cached;
|
|
179
|
+
|
|
180
|
+
const qValues = this._getQValues(state);
|
|
181
|
+
const visits = this._visitCounts.get(state) || 0;
|
|
182
|
+
|
|
183
|
+
// 엡실론-그리디: 탐색 vs 활용
|
|
184
|
+
const isExploration = Math.random() < this._epsilon;
|
|
185
|
+
|
|
186
|
+
let action;
|
|
187
|
+
if (isExploration) {
|
|
188
|
+
// 무작위 탐색
|
|
189
|
+
action = ACTIONS[Math.floor(Math.random() * ACTIONS.length)];
|
|
190
|
+
} else {
|
|
191
|
+
// 최적 액션 선택 (최대 Q-value)
|
|
192
|
+
let maxQ = -Infinity;
|
|
193
|
+
action = ACTIONS[0];
|
|
194
|
+
for (const [a, q] of qValues) {
|
|
195
|
+
if (q > maxQ) {
|
|
196
|
+
maxQ = q;
|
|
197
|
+
action = a;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// 신뢰도 계산: 방문 횟수 기반 (최소 10회 이상이면 안정)
|
|
203
|
+
const confidence = visits >= 10
|
|
204
|
+
? Math.min(visits / 50, 1)
|
|
205
|
+
: visits / 10;
|
|
206
|
+
|
|
207
|
+
const { score: complexity } = scoreComplexity(taskDescription);
|
|
208
|
+
|
|
209
|
+
const result = { action, confidence, exploration: isExploration, complexity };
|
|
210
|
+
// 탐색(랜덤) 결과는 캐싱하지 않음 — 매번 새로운 랜덤 액션 생성
|
|
211
|
+
if (!isExploration) this._cache.set(state, result);
|
|
212
|
+
return result;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Q-table 업데이트 (보상 피드백)
|
|
217
|
+
* @param {string} taskDescription — 작업 설명
|
|
218
|
+
* @param {string} action — 수행한 액션 (CLI 타입)
|
|
219
|
+
* @param {number} reward — 보상 (-1 ~ 1)
|
|
220
|
+
*/
|
|
221
|
+
update(taskDescription, action, reward) {
|
|
222
|
+
if (!ACTIONS.includes(action)) return;
|
|
223
|
+
|
|
224
|
+
const features = extractFeatures(taskDescription);
|
|
225
|
+
const state = stateKey(features);
|
|
226
|
+
const qValues = this._getQValues(state);
|
|
227
|
+
const oldQ = qValues.get(action) || 0;
|
|
228
|
+
|
|
229
|
+
// 최대 미래 Q-value (단일 상태이므로 현재 상태의 max)
|
|
230
|
+
let maxFutureQ = -Infinity;
|
|
231
|
+
for (const [, q] of qValues) {
|
|
232
|
+
if (q > maxFutureQ) maxFutureQ = q;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Q-Learning 업데이트: Q(s,a) = Q(s,a) + lr * (reward + gamma * max_Q(s') - Q(s,a))
|
|
236
|
+
const newQ = oldQ + this._lr * (reward + this._gamma * maxFutureQ - oldQ);
|
|
237
|
+
qValues.set(action, newQ);
|
|
238
|
+
|
|
239
|
+
// 방문 횟수 증가
|
|
240
|
+
this._visitCounts.set(state, (this._visitCounts.get(state) || 0) + 1);
|
|
241
|
+
this._totalUpdates++;
|
|
242
|
+
|
|
243
|
+
// 엡실론 감쇠
|
|
244
|
+
this._epsilon = Math.max(this._epsilonMin, this._epsilon * this._epsilonDecay);
|
|
245
|
+
|
|
246
|
+
// 캐시 무효화 (해당 상태)
|
|
247
|
+
this._cache.clear();
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Q-table을 JSON 파일로 영속화
|
|
252
|
+
*/
|
|
253
|
+
save() {
|
|
254
|
+
const dir = join(this._modelPath, '..');
|
|
255
|
+
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
256
|
+
|
|
257
|
+
const data = {
|
|
258
|
+
version: 1,
|
|
259
|
+
epsilon: this._epsilon,
|
|
260
|
+
totalUpdates: this._totalUpdates,
|
|
261
|
+
qTable: {},
|
|
262
|
+
visitCounts: {},
|
|
263
|
+
};
|
|
264
|
+
|
|
265
|
+
for (const [state, qValues] of this._qTable) {
|
|
266
|
+
data.qTable[state] = Object.fromEntries(qValues);
|
|
267
|
+
}
|
|
268
|
+
for (const [state, count] of this._visitCounts) {
|
|
269
|
+
data.visitCounts[state] = count;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
writeFileSync(this._modelPath, JSON.stringify(data, null, 2), 'utf8');
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* 영속화된 Q-table 로드
|
|
277
|
+
* @returns {boolean} 로드 성공 여부
|
|
278
|
+
*/
|
|
279
|
+
load() {
|
|
280
|
+
if (!existsSync(this._modelPath)) return false;
|
|
281
|
+
|
|
282
|
+
try {
|
|
283
|
+
const raw = readFileSync(this._modelPath, 'utf8');
|
|
284
|
+
const data = JSON.parse(raw);
|
|
285
|
+
if (data.version !== 1) return false;
|
|
286
|
+
|
|
287
|
+
this._epsilon = data.epsilon ?? this._epsilon;
|
|
288
|
+
this._totalUpdates = data.totalUpdates ?? 0;
|
|
289
|
+
this._qTable = new Map();
|
|
290
|
+
this._visitCounts = new Map();
|
|
291
|
+
|
|
292
|
+
for (const [state, qObj] of Object.entries(data.qTable || {})) {
|
|
293
|
+
const qValues = new Map();
|
|
294
|
+
for (const [action, q] of Object.entries(qObj)) {
|
|
295
|
+
if (ACTIONS.includes(action)) qValues.set(action, q);
|
|
296
|
+
}
|
|
297
|
+
this._qTable.set(state, qValues);
|
|
298
|
+
}
|
|
299
|
+
for (const [state, count] of Object.entries(data.visitCounts || {})) {
|
|
300
|
+
this._visitCounts.set(state, count);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
this._cache.clear();
|
|
304
|
+
return true;
|
|
305
|
+
} catch {
|
|
306
|
+
return false;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* agent-map.json 폴백 조회
|
|
312
|
+
* @param {string} agentType — 에이전트 역할명
|
|
313
|
+
* @returns {string} CLI 타입
|
|
314
|
+
*/
|
|
315
|
+
static fallback(agentType) {
|
|
316
|
+
return AGENT_MAP[agentType] || agentType;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/** 현재 엡실론 값 */
|
|
320
|
+
get epsilon() {
|
|
321
|
+
return this._epsilon;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/** 총 업데이트 횟수 */
|
|
325
|
+
get totalUpdates() {
|
|
326
|
+
return this._totalUpdates;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/** Q-table 상태 수 */
|
|
330
|
+
get stateCount() {
|
|
331
|
+
return this._qTable.size;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// 모듈 레벨 export
|
|
336
|
+
export { ACTIONS, FEATURE_KEYWORDS, extractFeatures, stateKey, LRUCache };
|