@triflux/core 10.50.1 → 10.51.0
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/agy-session-hook.mjs +11 -56
- package/hooks/codex-session-hook.mjs +11 -101
- package/hub/bridge.mjs +1 -79
- package/hub/cli-adapter-base.mjs +12 -90
- package/hub/codex-adapter.mjs +0 -1
- package/hub/codex-preflight.mjs +5 -28
- package/hub/index.mjs +3 -9
- package/hub/lib/cto-env.mjs +0 -23
- package/hub/lib/known-errors.json +2 -2
- package/hub/lib/process-utils.mjs +0 -123
- package/hub/lib/spawn-trace.mjs +3 -2
- package/hub/lib/timeout-defaults.mjs +0 -1
- package/hub/paths.mjs +0 -6
- package/hub/pipeline/index.mjs +0 -190
- package/hub/pipeline/transitions.mjs +0 -10
- package/hub/reflexion.mjs +1 -84
- package/hub/role-contract.mjs +0 -2
- package/hud/cli-policy.mjs +0 -26
- package/hud/colors.mjs +0 -13
- package/hud/constants.mjs +7 -153
- package/hud/context-monitor.mjs +3 -64
- package/hud/hud-qos-status.mjs +54 -264
- package/hud/providers/claude.mjs +4 -16
- package/hud/providers/gemini.mjs +83 -689
- package/hud/renderers.mjs +59 -367
- package/hud/terminal.mjs +1 -63
- package/hud/utils.mjs +2 -26
- package/package.json +1 -4
- package/scripts/lib/agent-route-policy.mjs +162 -14
- package/scripts/lib/claudemd-scanner.mjs +2 -199
- package/scripts/lib/cli-claude.mjs +0 -2
- package/scripts/lib/context.mjs +3 -19
- package/scripts/lib/env-probe.mjs +5 -64
- package/scripts/lib/gemini-profiles.mjs +2 -69
- package/scripts/lib/legacy-mcp-cleanup.mjs +14 -3
- package/scripts/lib/machine-profile.mjs +3 -17
- package/scripts/lib/mcp-filter.mjs +109 -53
- package/scripts/lib/mcp-health.mjs +2 -2
- package/scripts/lib/mcp-server-catalog.mjs +2 -2
- package/scripts/lib/psmux-info.mjs +25 -104
- package/scripts/lib/session-presence.mjs +1 -1
- package/scripts/lib/skill-template.mjs +0 -271
- package/hub/codex-compat.mjs +0 -12
- package/hub/dynamic-routing-engine.mjs +0 -511
- package/hub/fullcycle.mjs +0 -113
- package/hub/gemini-adapter.mjs +0 -174
- package/hub/intent.mjs +0 -321
- package/hub/lib/mcp-response-cache.mjs +0 -239
- package/hub/lib/path-utils.mjs +0 -165
- package/hub/lib/risk-tier.mjs +0 -53
- package/hub/lib/ssh-command.mjs +0 -231
- package/hub/lib/staged-file-detect.mjs +0 -43
- package/hub/lib/tfx-route-args.mjs +0 -353
- package/hub/lib/timeline-adapter.mjs +0 -190
- package/hub/pipeline/gates/confidence.mjs +0 -75
- package/hub/pipeline/gates/consensus.mjs +0 -106
- package/hub/pipeline/gates/index.mjs +0 -9
- package/hub/pipeline/gates/selfcheck.mjs +0 -120
- package/hub/quality/deslop.mjs +0 -274
- package/hub/research.mjs +0 -155
- package/hub/role-control-events.mjs +0 -113
- package/hub/routing/complexity.mjs +0 -269
- package/hub/routing/index.mjs +0 -122
- package/hub/routing/q-learning.mjs +0 -387
- package/hub/routing-snapshot.mjs +0 -261
- package/hub/token-mode.mjs +0 -228
- package/hud/mission-board.mjs +0 -56
- package/hud/providers/cto.mjs +0 -76
- package/mesh/index.mjs +0 -69
- package/mesh/mesh-budget.mjs +0 -137
- package/mesh/mesh-heartbeat.mjs +0 -100
- package/mesh/mesh-protocol.mjs +0 -96
- package/mesh/mesh-queue.mjs +0 -163
- package/mesh/mesh-registry.mjs +0 -78
- package/mesh/mesh-router.mjs +0 -77
- package/scripts/lib/agent-json.mjs +0 -27
- package/scripts/lib/async.mjs +0 -174
- package/scripts/lib/cli-agy.mjs +0 -85
- package/scripts/lib/cli-codex.mjs +0 -62
- package/scripts/lib/cli-gemini.mjs +0 -37
- package/scripts/lib/dynamic-route-cli.mjs +0 -107
- package/scripts/lib/env.mjs +0 -14
- package/scripts/lib/hub.mjs +0 -48
- package/scripts/lib/pid.mjs +0 -63
- package/scripts/lib/quota.mjs +0 -47
- package/scripts/lib/skill-state.mjs +0 -220
- package/scripts/lib/team.mjs +0 -75
- package/scripts/lib/timeout.mjs +0 -66
- package/scripts/lib/tmp.mjs +0 -26
- package/scripts/lib/toml.mjs +0 -132
|
@@ -1,387 +0,0 @@
|
|
|
1
|
-
// hub/routing/q-learning.mjs — 테이블 기반 Q-Learning 동적 라우팅
|
|
2
|
-
// agent-map.json 폴백을 유지하면서, 작업 결과 피드백으로 가중치를 학습한다.
|
|
3
|
-
// 외부 의존성 없음 (fs, path, crypto만 사용)
|
|
4
|
-
|
|
5
|
-
import { createHash } from "node:crypto";
|
|
6
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
|
-
import { createRequire } from "node:module";
|
|
8
|
-
import { homedir } from "node:os";
|
|
9
|
-
import { join } from "node:path";
|
|
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", "antigravity", "claude", "haiku", "sonnet"];
|
|
24
|
-
|
|
25
|
-
/** 특성 벡터 키워드 (48차원, 에이전트 타입 기반) */
|
|
26
|
-
const FEATURE_KEYWORDS = [
|
|
27
|
-
// 실행/구현 (codex 친화)
|
|
28
|
-
"implement",
|
|
29
|
-
"execute",
|
|
30
|
-
"build",
|
|
31
|
-
"fix",
|
|
32
|
-
"debug",
|
|
33
|
-
"code",
|
|
34
|
-
"refactor",
|
|
35
|
-
"test",
|
|
36
|
-
// 분석/설계 (claude/sonnet 친화)
|
|
37
|
-
"analyze",
|
|
38
|
-
"architect",
|
|
39
|
-
"plan",
|
|
40
|
-
"review",
|
|
41
|
-
"security",
|
|
42
|
-
"optimize",
|
|
43
|
-
"research",
|
|
44
|
-
"evaluate",
|
|
45
|
-
// 디자인/문서 (Antigravity 친화)
|
|
46
|
-
"design",
|
|
47
|
-
"ui",
|
|
48
|
-
"ux",
|
|
49
|
-
"frontend",
|
|
50
|
-
"visual",
|
|
51
|
-
"document",
|
|
52
|
-
"write",
|
|
53
|
-
"explain",
|
|
54
|
-
// 간단/빠른 (haiku 친화)
|
|
55
|
-
"simple",
|
|
56
|
-
"quick",
|
|
57
|
-
"trivial",
|
|
58
|
-
"rename",
|
|
59
|
-
"format",
|
|
60
|
-
"lint",
|
|
61
|
-
"typo",
|
|
62
|
-
"minor",
|
|
63
|
-
// 한국어 — 실행/구현 (codex 친화)
|
|
64
|
-
"구현",
|
|
65
|
-
"빌드",
|
|
66
|
-
"수정",
|
|
67
|
-
"디버깅",
|
|
68
|
-
"리팩터링",
|
|
69
|
-
"테스트",
|
|
70
|
-
// 한국어 — 분석/설계 (claude/sonnet 친화)
|
|
71
|
-
"분석",
|
|
72
|
-
"아키텍처",
|
|
73
|
-
"설계",
|
|
74
|
-
"검토",
|
|
75
|
-
"보안",
|
|
76
|
-
"최적화",
|
|
77
|
-
// 한국어 — 디자인/문서 (Antigravity 친화)
|
|
78
|
-
"디자인",
|
|
79
|
-
"문서화",
|
|
80
|
-
// 한국어 — 간단/빠른 (haiku 친화)
|
|
81
|
-
"간단",
|
|
82
|
-
"사소한",
|
|
83
|
-
];
|
|
84
|
-
|
|
85
|
-
/**
|
|
86
|
-
* 텍스트에서 48차원 특성 벡터 추출
|
|
87
|
-
* 단어 경계를 기준으로 매칭하여 부분 문자열 오탐을 방지한다.
|
|
88
|
-
* @param {string} text
|
|
89
|
-
* @returns {number[]} 48-dim binary feature vector
|
|
90
|
-
*/
|
|
91
|
-
function extractFeatures(text) {
|
|
92
|
-
const lower = text.toLowerCase();
|
|
93
|
-
return FEATURE_KEYWORDS.map((kw) => {
|
|
94
|
-
// 영문 단일 단어: 단어 경계(\b) 적용
|
|
95
|
-
// 한국어 또는 다중 단어 구문: 공백/문장 경계 기반 포함 여부 확인
|
|
96
|
-
if (/^[a-z]+$/.test(kw)) {
|
|
97
|
-
return new RegExp(`\\b${kw}\\b`).test(lower) ? 1 : 0;
|
|
98
|
-
}
|
|
99
|
-
return lower.includes(kw) ? 1 : 0;
|
|
100
|
-
});
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* 특성 벡터를 상태 키로 변환 (해시 기반)
|
|
105
|
-
* @param {number[]} features
|
|
106
|
-
* @returns {string}
|
|
107
|
-
*/
|
|
108
|
-
function stateKey(features) {
|
|
109
|
-
const hash = createHash("sha256").update(features.join(",")).digest("hex");
|
|
110
|
-
return hash.slice(0, 16);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* LRU 캐시 (예측 결과 캐싱)
|
|
115
|
-
*/
|
|
116
|
-
class LRUCache {
|
|
117
|
-
/** @param {number} maxSize @param {number} ttlMs */
|
|
118
|
-
constructor(maxSize = 256, ttlMs = 5 * 60 * 1000) {
|
|
119
|
-
this._maxSize = maxSize;
|
|
120
|
-
this._ttlMs = ttlMs;
|
|
121
|
-
/** @type {Map<string, { value: *, ts: number }>} */
|
|
122
|
-
this._cache = new Map();
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
get(key) {
|
|
126
|
-
const entry = this._cache.get(key);
|
|
127
|
-
if (!entry) return undefined;
|
|
128
|
-
if (Date.now() - entry.ts > this._ttlMs) {
|
|
129
|
-
this._cache.delete(key);
|
|
130
|
-
return undefined;
|
|
131
|
-
}
|
|
132
|
-
// LRU: 다시 삽입하여 순서 갱신
|
|
133
|
-
this._cache.delete(key);
|
|
134
|
-
this._cache.set(key, entry);
|
|
135
|
-
return entry.value;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
set(key, value) {
|
|
139
|
-
if (this._cache.has(key)) this._cache.delete(key);
|
|
140
|
-
this._cache.set(key, { value, ts: Date.now() });
|
|
141
|
-
// 초과 시 가장 오래된 항목 제거
|
|
142
|
-
if (this._cache.size > this._maxSize) {
|
|
143
|
-
const oldest = this._cache.keys().next().value;
|
|
144
|
-
this._cache.delete(oldest);
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
clear() {
|
|
149
|
-
this._cache.clear();
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
/**
|
|
154
|
-
* Q-Learning 라우터
|
|
155
|
-
* 테이블 기반 Q-Learning으로 작업→CLI 매핑을 학습한다.
|
|
156
|
-
*/
|
|
157
|
-
export class QLearningRouter {
|
|
158
|
-
/**
|
|
159
|
-
* @param {object} [opts]
|
|
160
|
-
* @param {number} [opts.learningRate=0.1] — 학습률 (alpha)
|
|
161
|
-
* @param {number} [opts.discountFactor=0.9] — 할인율 (gamma)
|
|
162
|
-
* @param {number} [opts.epsilon=0.3] — 탐색 확률 (epsilon-greedy)
|
|
163
|
-
* @param {number} [opts.epsilonDecay=0.995] — 엡실론 감쇠율
|
|
164
|
-
* @param {number} [opts.epsilonMin=0.05] — 최소 엡실론
|
|
165
|
-
* @param {number} [opts.minConfidence=0.6] — 최소 신뢰도 (이하면 폴백)
|
|
166
|
-
* @param {string} [opts.modelPath] — Q-table 영속화 경로
|
|
167
|
-
* @param {() => number} [opts.random=Math.random] — 테스트/재현용 RNG seam
|
|
168
|
-
*/
|
|
169
|
-
constructor(opts = {}) {
|
|
170
|
-
this._lr = opts.learningRate ?? 0.1;
|
|
171
|
-
this._gamma = opts.discountFactor ?? 0.9;
|
|
172
|
-
this._epsilon = opts.epsilon ?? 0.3;
|
|
173
|
-
this._epsilonDecay = opts.epsilonDecay ?? 0.995;
|
|
174
|
-
// epsilon=0 시에도 최소 탐색 보장 (pure-exploit 방지)
|
|
175
|
-
this._epsilonMin =
|
|
176
|
-
opts.epsilonMin ?? Math.max(0.01, Math.min(0.05, this._epsilon));
|
|
177
|
-
this._minConfidence = opts.minConfidence ?? 0.6;
|
|
178
|
-
this._modelPath =
|
|
179
|
-
opts.modelPath ?? join(homedir(), ".omc", "routing-model.json");
|
|
180
|
-
this._random =
|
|
181
|
-
typeof opts.random === "function" ? opts.random : Math.random;
|
|
182
|
-
|
|
183
|
-
/** @type {Map<string, Map<string, number>>} state -> (action -> Q-value) */
|
|
184
|
-
this._qTable = new Map();
|
|
185
|
-
|
|
186
|
-
/** @type {Map<string, number>} state -> visit count */
|
|
187
|
-
this._visitCounts = new Map();
|
|
188
|
-
|
|
189
|
-
/** 총 업데이트 횟수 */
|
|
190
|
-
this._totalUpdates = 0;
|
|
191
|
-
|
|
192
|
-
/** 예측 캐시 */
|
|
193
|
-
this._cache = new LRUCache(256, 5 * 60 * 1000);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
/**
|
|
197
|
-
* 상태에 대한 Q-values 조회 (없으면 초기화)
|
|
198
|
-
* @param {string} state
|
|
199
|
-
* @returns {Map<string, number>}
|
|
200
|
-
*/
|
|
201
|
-
_getQValues(state) {
|
|
202
|
-
if (!this._qTable.has(state)) {
|
|
203
|
-
const qValues = new Map();
|
|
204
|
-
for (const action of ACTIONS) {
|
|
205
|
-
qValues.set(action, 0);
|
|
206
|
-
}
|
|
207
|
-
this._qTable.set(state, qValues);
|
|
208
|
-
}
|
|
209
|
-
return this._qTable.get(state);
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
/**
|
|
213
|
-
* 작업 설명으로부터 최적 CLI 타입 예측
|
|
214
|
-
* @param {string} taskDescription
|
|
215
|
-
* @returns {{ action: string, confidence: number, exploration: boolean, complexity: number }}
|
|
216
|
-
*/
|
|
217
|
-
predict(taskDescription) {
|
|
218
|
-
const features = extractFeatures(taskDescription);
|
|
219
|
-
const state = stateKey(features);
|
|
220
|
-
|
|
221
|
-
// 캐시 확인
|
|
222
|
-
const cached = this._cache.get(state);
|
|
223
|
-
if (cached) return cached;
|
|
224
|
-
|
|
225
|
-
const qValues = this._getQValues(state);
|
|
226
|
-
const visits = this._visitCounts.get(state) || 0;
|
|
227
|
-
|
|
228
|
-
// 엡실론-그리디: 탐색 vs 활용
|
|
229
|
-
const isExploration = this._random() < this._epsilon;
|
|
230
|
-
|
|
231
|
-
let action;
|
|
232
|
-
if (isExploration) {
|
|
233
|
-
// 무작위 탐색
|
|
234
|
-
action = ACTIONS[Math.floor(this._random() * ACTIONS.length)];
|
|
235
|
-
} else {
|
|
236
|
-
// 최적 액션 선택 (최대 Q-value)
|
|
237
|
-
let maxQ = -Infinity;
|
|
238
|
-
action = ACTIONS[0];
|
|
239
|
-
for (const [a, q] of qValues) {
|
|
240
|
-
if (q > maxQ) {
|
|
241
|
-
maxQ = q;
|
|
242
|
-
action = a;
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
// 신뢰도 계산: 방문 횟수 기반 (최소 10회 이상이면 안정)
|
|
248
|
-
const confidence = visits >= 10 ? Math.min(visits / 50, 1) : visits / 10;
|
|
249
|
-
|
|
250
|
-
const { score: complexity } = scoreComplexity(taskDescription);
|
|
251
|
-
|
|
252
|
-
const result = {
|
|
253
|
-
action,
|
|
254
|
-
confidence,
|
|
255
|
-
exploration: isExploration,
|
|
256
|
-
complexity,
|
|
257
|
-
};
|
|
258
|
-
// 탐색(랜덤) 결과는 캐싱하지 않음 — 매번 새로운 랜덤 액션 생성
|
|
259
|
-
if (!isExploration) this._cache.set(state, result);
|
|
260
|
-
return result;
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
/**
|
|
264
|
-
* Q-table 업데이트 (보상 피드백)
|
|
265
|
-
* @param {string} taskDescription — 작업 설명
|
|
266
|
-
* @param {string} action — 수행한 액션 (CLI 타입)
|
|
267
|
-
* @param {number} reward — 보상 (-1 ~ 1)
|
|
268
|
-
*/
|
|
269
|
-
update(taskDescription, action, reward) {
|
|
270
|
-
if (!ACTIONS.includes(action)) return;
|
|
271
|
-
|
|
272
|
-
const features = extractFeatures(taskDescription);
|
|
273
|
-
const state = stateKey(features);
|
|
274
|
-
const qValues = this._getQValues(state);
|
|
275
|
-
const oldQ = qValues.get(action) || 0;
|
|
276
|
-
|
|
277
|
-
// 최대 미래 Q-value (단일 상태이므로 현재 상태의 max)
|
|
278
|
-
let maxFutureQ = -Infinity;
|
|
279
|
-
for (const [, q] of qValues) {
|
|
280
|
-
if (q > maxFutureQ) maxFutureQ = q;
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
// Q-Learning 업데이트: Q(s,a) = Q(s,a) + lr * (reward + gamma * max_Q(s') - Q(s,a))
|
|
284
|
-
const newQ = oldQ + this._lr * (reward + this._gamma * maxFutureQ - oldQ);
|
|
285
|
-
qValues.set(action, newQ);
|
|
286
|
-
|
|
287
|
-
// 방문 횟수 증가
|
|
288
|
-
this._visitCounts.set(state, (this._visitCounts.get(state) || 0) + 1);
|
|
289
|
-
this._totalUpdates++;
|
|
290
|
-
|
|
291
|
-
// 엡실론 감쇠
|
|
292
|
-
this._epsilon = Math.max(
|
|
293
|
-
this._epsilonMin,
|
|
294
|
-
this._epsilon * this._epsilonDecay,
|
|
295
|
-
);
|
|
296
|
-
|
|
297
|
-
// 캐시 무효화 (해당 상태)
|
|
298
|
-
this._cache.clear();
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* Q-table을 JSON 파일로 영속화
|
|
303
|
-
*/
|
|
304
|
-
save() {
|
|
305
|
-
const dir = join(this._modelPath, "..");
|
|
306
|
-
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
307
|
-
|
|
308
|
-
const data = {
|
|
309
|
-
version: 1,
|
|
310
|
-
epsilon: this._epsilon,
|
|
311
|
-
totalUpdates: this._totalUpdates,
|
|
312
|
-
qTable: {},
|
|
313
|
-
visitCounts: {},
|
|
314
|
-
};
|
|
315
|
-
|
|
316
|
-
for (const [state, qValues] of this._qTable) {
|
|
317
|
-
data.qTable[state] = Object.fromEntries(qValues);
|
|
318
|
-
}
|
|
319
|
-
for (const [state, count] of this._visitCounts) {
|
|
320
|
-
data.visitCounts[state] = count;
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
writeFileSync(this._modelPath, JSON.stringify(data, null, 2), "utf8");
|
|
324
|
-
}
|
|
325
|
-
|
|
326
|
-
/**
|
|
327
|
-
* 영속화된 Q-table 로드
|
|
328
|
-
* @returns {boolean} 로드 성공 여부
|
|
329
|
-
*/
|
|
330
|
-
load() {
|
|
331
|
-
if (!existsSync(this._modelPath)) return false;
|
|
332
|
-
|
|
333
|
-
try {
|
|
334
|
-
const raw = readFileSync(this._modelPath, "utf8");
|
|
335
|
-
const data = JSON.parse(raw);
|
|
336
|
-
if (data.version !== 1) return false;
|
|
337
|
-
|
|
338
|
-
this._epsilon = data.epsilon ?? this._epsilon;
|
|
339
|
-
this._totalUpdates = data.totalUpdates ?? 0;
|
|
340
|
-
this._qTable = new Map();
|
|
341
|
-
this._visitCounts = new Map();
|
|
342
|
-
|
|
343
|
-
for (const [state, qObj] of Object.entries(data.qTable || {})) {
|
|
344
|
-
const qValues = new Map();
|
|
345
|
-
for (const [action, q] of Object.entries(qObj)) {
|
|
346
|
-
if (ACTIONS.includes(action)) qValues.set(action, q);
|
|
347
|
-
}
|
|
348
|
-
this._qTable.set(state, qValues);
|
|
349
|
-
}
|
|
350
|
-
for (const [state, count] of Object.entries(data.visitCounts || {})) {
|
|
351
|
-
this._visitCounts.set(state, count);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
this._cache.clear();
|
|
355
|
-
return true;
|
|
356
|
-
} catch {
|
|
357
|
-
return false;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
/**
|
|
362
|
-
* agent-map.json 폴백 조회
|
|
363
|
-
* @param {string} agentType — 에이전트 역할명
|
|
364
|
-
* @returns {string} CLI 타입
|
|
365
|
-
*/
|
|
366
|
-
static fallback(agentType) {
|
|
367
|
-
return AGENT_MAP[agentType] || agentType;
|
|
368
|
-
}
|
|
369
|
-
|
|
370
|
-
/** 현재 엡실론 값 */
|
|
371
|
-
get epsilon() {
|
|
372
|
-
return this._epsilon;
|
|
373
|
-
}
|
|
374
|
-
|
|
375
|
-
/** 총 업데이트 횟수 */
|
|
376
|
-
get totalUpdates() {
|
|
377
|
-
return this._totalUpdates;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
/** Q-table 상태 수 */
|
|
381
|
-
get stateCount() {
|
|
382
|
-
return this._qTable.size;
|
|
383
|
-
}
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
// 모듈 레벨 export
|
|
387
|
-
export { ACTIONS, extractFeatures, FEATURE_KEYWORDS, LRUCache, stateKey };
|
package/hub/routing-snapshot.mjs
DELETED
|
@@ -1,261 +0,0 @@
|
|
|
1
|
-
// hub/routing-snapshot.mjs
|
|
2
|
-
//
|
|
3
|
-
// PRD: .omx/plans/prd-dynamic-routing-engine.md
|
|
4
|
-
//
|
|
5
|
-
// Phase 0 — pure read-only context projector. Wraps HUD providers and the
|
|
6
|
-
// AccountBroker public snapshot into a single deterministic snapshot that
|
|
7
|
-
// `evaluateDynamicRoute(request, snapshot, policy)` consumes.
|
|
8
|
-
//
|
|
9
|
-
// Phase 0 contract:
|
|
10
|
-
// - no callers yet (dormant). Only `evaluateDynamicRoute()` is meant to read this.
|
|
11
|
-
// - lazy import of HUD providers + broker so this module imports cleanly even
|
|
12
|
-
// when those surfaces are absent in tests.
|
|
13
|
-
// - never mutates upstream state — strictly read-only.
|
|
14
|
-
// - all output fields are derivable from `publicSnapshot()` / HUD redacted API,
|
|
15
|
-
// so writing the resulting snapshot to disk does not leak credentials.
|
|
16
|
-
|
|
17
|
-
const CLAUDE_HUD_MODULE = "../hud/providers/claude.mjs";
|
|
18
|
-
const CODEX_HUD_MODULE = "../hud/providers/codex.mjs";
|
|
19
|
-
const BROKER_MODULE = "./account-broker.mjs";
|
|
20
|
-
|
|
21
|
-
async function loadHudClaude() {
|
|
22
|
-
try {
|
|
23
|
-
return await import(CLAUDE_HUD_MODULE);
|
|
24
|
-
} catch {
|
|
25
|
-
return null;
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async function loadHudCodex() {
|
|
30
|
-
try {
|
|
31
|
-
return await import(CODEX_HUD_MODULE);
|
|
32
|
-
} catch {
|
|
33
|
-
return null;
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
async function loadBroker() {
|
|
38
|
-
try {
|
|
39
|
-
const mod = await import(BROKER_MODULE);
|
|
40
|
-
return mod.broker ?? mod.default ?? null;
|
|
41
|
-
} catch {
|
|
42
|
-
return null;
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function normalizeClaudeUsage(raw, now) {
|
|
47
|
-
if (!raw || typeof raw !== "object") return null;
|
|
48
|
-
const data = raw.data ?? raw;
|
|
49
|
-
if (!data) return null;
|
|
50
|
-
return {
|
|
51
|
-
fiveHourPercent: numericOrNull(data.fiveHourPercent),
|
|
52
|
-
weeklyPercent: numericOrNull(data.weeklyPercent),
|
|
53
|
-
fiveHourResetsAt: toEpoch(data.fiveHourResetsAt),
|
|
54
|
-
weeklyResetsAt: toEpoch(data.weeklyResetsAt),
|
|
55
|
-
observedAt: toEpoch(data.observedAt) ?? now,
|
|
56
|
-
isStale: Boolean(raw.isStale ?? data.isStale),
|
|
57
|
-
source: data.source ?? "hud/providers/claude.mjs",
|
|
58
|
-
};
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
function normalizeCodexUsage(raw, now) {
|
|
62
|
-
if (!raw || typeof raw !== "object") return null;
|
|
63
|
-
const buckets = raw.buckets ?? raw.data?.buckets ?? raw;
|
|
64
|
-
if (!buckets || typeof buckets !== "object") return null;
|
|
65
|
-
const entries = Object.entries(buckets);
|
|
66
|
-
if (entries.length === 0) return null;
|
|
67
|
-
const out = {};
|
|
68
|
-
for (const [id, bucket] of entries) {
|
|
69
|
-
if (!bucket || typeof bucket !== "object") continue;
|
|
70
|
-
out[id] = {
|
|
71
|
-
primary: normalizeCodexBucketLeg(bucket.primary),
|
|
72
|
-
secondary: normalizeCodexBucketLeg(bucket.secondary),
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
return {
|
|
76
|
-
buckets: out,
|
|
77
|
-
observedAt: toEpoch(raw.observedAt) ?? now,
|
|
78
|
-
isStale: Boolean(raw.shouldRefresh === false ? false : raw.isStale),
|
|
79
|
-
source: raw.source ?? "hud/providers/codex.mjs",
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function normalizeCodexBucketLeg(leg) {
|
|
84
|
-
if (!leg || typeof leg !== "object") return null;
|
|
85
|
-
return {
|
|
86
|
-
usedPercent: numericOrNull(leg.used_percent ?? leg.usedPercent),
|
|
87
|
-
resetsAt: toEpoch(leg.resets_at ?? leg.resetsAt),
|
|
88
|
-
};
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function normalizeBrokerSnapshot(raw) {
|
|
92
|
-
if (!Array.isArray(raw)) return null;
|
|
93
|
-
const grouped = { codex: [], claude: [], gemini: [] };
|
|
94
|
-
for (const entry of raw) {
|
|
95
|
-
if (!entry || typeof entry !== "object") continue;
|
|
96
|
-
const provider = entry.provider;
|
|
97
|
-
if (!grouped[provider]) continue;
|
|
98
|
-
grouped[provider].push({
|
|
99
|
-
id: entry.id,
|
|
100
|
-
tier: entry.tier ?? "unknown",
|
|
101
|
-
busy: Boolean(entry.busy),
|
|
102
|
-
cooldownUntil: toEpoch(entry.cooldownUntil),
|
|
103
|
-
lastUsedAt: toEpoch(entry.lastUsedAt),
|
|
104
|
-
remainingMs: numericOrNull(entry.remainingMs),
|
|
105
|
-
circuitState: entry.circuitState ?? "closed",
|
|
106
|
-
failureCount: numericOrNull(entry.failureCount) ?? 0,
|
|
107
|
-
expiresAt: toEpoch(entry.expiresAt),
|
|
108
|
-
drainAfter: toEpoch(entry.drainAfter),
|
|
109
|
-
});
|
|
110
|
-
}
|
|
111
|
-
return grouped;
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function numericOrNull(v) {
|
|
115
|
-
if (v === null || v === undefined) return null;
|
|
116
|
-
const n = Number(v);
|
|
117
|
-
return Number.isFinite(n) ? n : null;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
function toEpoch(v) {
|
|
121
|
-
if (v === null || v === undefined) return null;
|
|
122
|
-
if (typeof v === "number") return Number.isFinite(v) ? v : null;
|
|
123
|
-
const parsed = Date.parse(v);
|
|
124
|
-
return Number.isNaN(parsed) ? null : parsed;
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export async function buildRoutingSnapshot({
|
|
128
|
-
forceRefresh = false,
|
|
129
|
-
now = Date.now(),
|
|
130
|
-
} = {}) {
|
|
131
|
-
const [hudClaude, hudCodex, broker] = await Promise.all([
|
|
132
|
-
loadHudClaude(),
|
|
133
|
-
loadHudCodex(),
|
|
134
|
-
loadBroker(),
|
|
135
|
-
]);
|
|
136
|
-
|
|
137
|
-
let claude = null;
|
|
138
|
-
if (hudClaude?.readClaudeUsageSnapshot) {
|
|
139
|
-
try {
|
|
140
|
-
claude = normalizeClaudeUsage(
|
|
141
|
-
hudClaude.readClaudeUsageSnapshot({ forceRefresh }),
|
|
142
|
-
now,
|
|
143
|
-
);
|
|
144
|
-
} catch (err) {
|
|
145
|
-
claude = {
|
|
146
|
-
error: String(err?.message ?? err),
|
|
147
|
-
observedAt: now,
|
|
148
|
-
isStale: true,
|
|
149
|
-
};
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
let codex = null;
|
|
154
|
-
if (hudCodex?.readCodexRateLimitSnapshot) {
|
|
155
|
-
try {
|
|
156
|
-
codex = normalizeCodexUsage(
|
|
157
|
-
hudCodex.readCodexRateLimitSnapshot({ forceRefresh }),
|
|
158
|
-
now,
|
|
159
|
-
);
|
|
160
|
-
} catch (err) {
|
|
161
|
-
codex = {
|
|
162
|
-
error: String(err?.message ?? err),
|
|
163
|
-
observedAt: now,
|
|
164
|
-
isStale: true,
|
|
165
|
-
};
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
let brokerState = null;
|
|
170
|
-
if (broker?.publicSnapshot) {
|
|
171
|
-
try {
|
|
172
|
-
brokerState = normalizeBrokerSnapshot(broker.publicSnapshot());
|
|
173
|
-
} catch (err) {
|
|
174
|
-
brokerState = { error: String(err?.message ?? err) };
|
|
175
|
-
}
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
return {
|
|
179
|
-
schemaVersion: 1,
|
|
180
|
-
observedAt: now,
|
|
181
|
-
claude,
|
|
182
|
-
codex,
|
|
183
|
-
broker: brokerState,
|
|
184
|
-
};
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// Phase 1 — in-process snapshot cache.
|
|
188
|
-
// 의도: caller (Phase 1+ conductor / swarm-hypervisor) 가 spawn loop 에서
|
|
189
|
-
// 매번 HUD/broker IO 를 트리거하지 않도록 정해진 TTL 안에서 같은 snapshot
|
|
190
|
-
// 을 재사용한다. cache 는 모듈-local 이라 process 단위 (테스트 격리 위해
|
|
191
|
-
// `resetSnapshotCache()` 노출).
|
|
192
|
-
|
|
193
|
-
const DEFAULT_CACHE_TTL_MS = 900_000; // policy freshness max 와 정합
|
|
194
|
-
|
|
195
|
-
let _cache = null; // { snapshot, fetchedAt }
|
|
196
|
-
|
|
197
|
-
export function resetSnapshotCache() {
|
|
198
|
-
_cache = null;
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
/**
|
|
202
|
-
* Cached snapshot shortcut. cache TTL 안이면 재사용, 아니면 새로 빌드.
|
|
203
|
-
*
|
|
204
|
-
* @param {{
|
|
205
|
-
* forceRefresh?: boolean, // true 면 cache 무시하고 새로 빌드
|
|
206
|
-
* maxAgeMs?: number, // override cache TTL (default 900_000)
|
|
207
|
-
* now?: number, // pure-fn: caller 가 epoch 주입 (default Date.now())
|
|
208
|
-
* builder?: function, // 테스트용 builder 주입 (default buildRoutingSnapshot)
|
|
209
|
-
* }} [opts]
|
|
210
|
-
* @returns {Promise<object>}
|
|
211
|
-
*/
|
|
212
|
-
export async function getDefaultSnapshot(opts = {}) {
|
|
213
|
-
const now = typeof opts.now === "number" ? opts.now : Date.now();
|
|
214
|
-
const maxAgeMs =
|
|
215
|
-
typeof opts.maxAgeMs === "number" ? opts.maxAgeMs : DEFAULT_CACHE_TTL_MS;
|
|
216
|
-
const builder = opts.builder || buildRoutingSnapshot;
|
|
217
|
-
const forceRefresh = Boolean(opts.forceRefresh);
|
|
218
|
-
|
|
219
|
-
if (!forceRefresh && _cache && now - _cache.fetchedAt < maxAgeMs) {
|
|
220
|
-
return _cache.snapshot;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const snapshot = await builder({ forceRefresh, now });
|
|
224
|
-
_cache = { snapshot, fetchedAt: now };
|
|
225
|
-
return snapshot;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
/**
|
|
229
|
-
* Synchronous cache shortcut. cache TTL 안에 fresh snapshot이 있으면 즉시 반환,
|
|
230
|
-
* 아니면 null. caller (sync hot path — e.g. conductor.spawnSession) 가 broker.lease
|
|
231
|
-
* atomic 인변량을 깨지 않고 dynamic routing decision 을 sync 로 평가할 수 있도록 한다.
|
|
232
|
-
* builder 호출(IO)은 하지 않는다 — cache miss 면 caller 가 fallback decision 사용.
|
|
233
|
-
*
|
|
234
|
-
* @param {{
|
|
235
|
-
* maxAgeMs?: number, // override cache TTL (default 900_000)
|
|
236
|
-
* now?: number, // pure-fn: caller 가 epoch 주입 (default Date.now())
|
|
237
|
-
* }} [opts]
|
|
238
|
-
* @returns {object|null} cache fresh이면 snapshot, 아니면 null
|
|
239
|
-
*/
|
|
240
|
-
export function tryGetCachedSnapshot(opts = {}) {
|
|
241
|
-
const now = typeof opts.now === "number" ? opts.now : Date.now();
|
|
242
|
-
const maxAgeMs =
|
|
243
|
-
typeof opts.maxAgeMs === "number" ? opts.maxAgeMs : DEFAULT_CACHE_TTL_MS;
|
|
244
|
-
if (_cache && now - _cache.fetchedAt < maxAgeMs) {
|
|
245
|
-
return _cache.snapshot;
|
|
246
|
-
}
|
|
247
|
-
return null;
|
|
248
|
-
}
|
|
249
|
-
|
|
250
|
-
// Test helpers — pure normalizers exposed so unit tests can build snapshots
|
|
251
|
-
// from mock provider responses without running real HUD I/O.
|
|
252
|
-
export const __internal__ = {
|
|
253
|
-
normalizeClaudeUsage,
|
|
254
|
-
normalizeCodexUsage,
|
|
255
|
-
normalizeBrokerSnapshot,
|
|
256
|
-
// Phase 1 cache 테스트 helpers
|
|
257
|
-
getCacheStateForTest: () => (_cache ? { ..._cache } : null),
|
|
258
|
-
setCacheForTest: (state) => {
|
|
259
|
-
_cache = state ? { ...state } : null;
|
|
260
|
-
},
|
|
261
|
-
};
|