@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
package/hub/gemini-adapter.mjs
DELETED
|
@@ -1,174 +0,0 @@
|
|
|
1
|
-
// hub/gemini-adapter.mjs — legacy Gemini adapter compatibility layer
|
|
2
|
-
// The "gemini" route is retained as an alias, but execution is Antigravity/agy.
|
|
3
|
-
|
|
4
|
-
import { mkdirSync } from "node:fs";
|
|
5
|
-
import { tmpdir } from "node:os";
|
|
6
|
-
import { join } from "node:path";
|
|
7
|
-
import {
|
|
8
|
-
resolveGeminiModel,
|
|
9
|
-
resolveGeminiProfileForPurpose,
|
|
10
|
-
} from "../scripts/lib/gemini-profiles.mjs";
|
|
11
|
-
import {
|
|
12
|
-
executeWithCircuitBroker,
|
|
13
|
-
normalizePathForShell,
|
|
14
|
-
runProcess,
|
|
15
|
-
} from "./cli-adapter-base.mjs";
|
|
16
|
-
import { isActivityLifecycleEnabled } from "./lib/worker-lifecycle.mjs";
|
|
17
|
-
import { whichCommandAsync } from "./platform.mjs";
|
|
18
|
-
|
|
19
|
-
function shellSingleQuote(value) {
|
|
20
|
-
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// ── Antigravity stall inference ─────────────────────────────────
|
|
24
|
-
|
|
25
|
-
function inferStallMode(stdout, stderr) {
|
|
26
|
-
const text = `${stdout}\n${stderr}`.toLowerCase();
|
|
27
|
-
if (/(rate.?limit|quota|resource.?exhaust|429)/u.test(text))
|
|
28
|
-
return "rate_limited";
|
|
29
|
-
if (
|
|
30
|
-
/(unauthorized|forbidden|auth|login|token|credential|api.?key)/u.test(text)
|
|
31
|
-
)
|
|
32
|
-
return "auth_stall";
|
|
33
|
-
if (/\bmcp\b|playwright|tavily|brave|sequential|server/u.test(text))
|
|
34
|
-
return "mcp_stall";
|
|
35
|
-
return "timeout";
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// ── Preflight ───────────────────────────────────────────────────
|
|
39
|
-
|
|
40
|
-
async function runPreflight(opts = {}) {
|
|
41
|
-
const agyPath = await whichCommandAsync("agy");
|
|
42
|
-
if (!agyPath) {
|
|
43
|
-
return {
|
|
44
|
-
agyPath: null,
|
|
45
|
-
warnings: [
|
|
46
|
-
"Antigravity CLI not found. Install Antigravity and ensure `agy` is available on PATH.",
|
|
47
|
-
],
|
|
48
|
-
excludeMcpServers: [],
|
|
49
|
-
ok: false,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const warnings = [];
|
|
54
|
-
const excludeMcpServers = [];
|
|
55
|
-
|
|
56
|
-
for (const name of Array.isArray(opts.mcpServers) ? opts.mcpServers : []) {
|
|
57
|
-
const server = String(name ?? "").trim();
|
|
58
|
-
if (!server) continue;
|
|
59
|
-
// Antigravity MCP health는 best-effort: 실행 시점 probe는 수행하지 않음.
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
return { agyPath, warnings, excludeMcpServers, ok: true };
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// ── Command building ────────────────────────────────────────────
|
|
66
|
-
|
|
67
|
-
function buildGeminiCommand(prompt, resultFile, opts = {}) {
|
|
68
|
-
// agy 의 --print 는 값(프롬프트)을 받는 플래그다. 1.1.27 부터 stdin 프롬프트는
|
|
69
|
-
// "empty prompt" 로 거부되므로 프롬프트를 --print 의 값으로, 맨 끝에 넘긴다.
|
|
70
|
-
const parts = ["agy", "--dangerously-skip-permissions"];
|
|
71
|
-
if (typeof opts.model === "string" && opts.model.trim()) {
|
|
72
|
-
parts.push("--model", shellSingleQuote(opts.model.trim()));
|
|
73
|
-
}
|
|
74
|
-
parts.push("--print", shellSingleQuote(prompt));
|
|
75
|
-
|
|
76
|
-
if (resultFile) {
|
|
77
|
-
return `${parts.join(" ")} > ${shellSingleQuote(normalizePathForShell(resultFile))} 2>${shellSingleQuote(normalizePathForShell(resultFile + ".err"))}`;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
return parts.join(" ");
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
function resolveAntigravityModel(opts = {}) {
|
|
84
|
-
// effort 는 역할별 SSOT(scripts/lib/gemini-profiles.mjs). 명시 model 이 우선한다.
|
|
85
|
-
const explicit = typeof opts.model === "string" ? opts.model.trim() : "";
|
|
86
|
-
if (explicit)
|
|
87
|
-
return resolveGeminiModel(explicit, {
|
|
88
|
-
profilesPath: opts.geminiProfilesPath,
|
|
89
|
-
});
|
|
90
|
-
if (!opts.role && !opts.agent) return "";
|
|
91
|
-
return resolveGeminiModel(
|
|
92
|
-
resolveGeminiProfileForPurpose(opts.role || opts.agent),
|
|
93
|
-
{
|
|
94
|
-
profilesPath: opts.geminiProfilesPath,
|
|
95
|
-
},
|
|
96
|
-
);
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
function buildAttempts(opts, preflight) {
|
|
100
|
-
const timeout = Number.isFinite(opts.timeout) ? opts.timeout : 900_000;
|
|
101
|
-
const base = {
|
|
102
|
-
timeout,
|
|
103
|
-
model: resolveAntigravityModel(opts),
|
|
104
|
-
allowedMcpServers: Array.isArray(opts.mcpServers)
|
|
105
|
-
? [...opts.mcpServers]
|
|
106
|
-
: [],
|
|
107
|
-
excludeMcpServers: [...(preflight.excludeMcpServers || [])],
|
|
108
|
-
};
|
|
109
|
-
if (opts.retryOnFail === false) return [base];
|
|
110
|
-
const retryTimeout = isActivityLifecycleEnabled() ? timeout : timeout * 2;
|
|
111
|
-
return [base, { ...base, timeout: retryTimeout, allowedMcpServers: [] }];
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// ── Public: buildExecArgs ───────────────────────────────────────
|
|
115
|
-
|
|
116
|
-
export function buildExecArgs(opts = {}) {
|
|
117
|
-
const prompt = typeof opts.prompt === "string" ? opts.prompt : "";
|
|
118
|
-
return buildGeminiCommand(prompt, opts.resultFile || null, {
|
|
119
|
-
model: resolveAntigravityModel(opts),
|
|
120
|
-
allowedMcpServers: opts.mcpServers,
|
|
121
|
-
});
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
// ── Execution ───────────────────────────────────────────────────
|
|
125
|
-
|
|
126
|
-
async function runGemini(prompt, workdir, preflight, attempt, lease) {
|
|
127
|
-
const dir = join(tmpdir(), "triflux-antigravity-exec");
|
|
128
|
-
mkdirSync(dir, { recursive: true });
|
|
129
|
-
const resultFile = join(
|
|
130
|
-
dir,
|
|
131
|
-
`agy-${Date.now()}-${Math.random().toString(36).slice(2)}.txt`,
|
|
132
|
-
);
|
|
133
|
-
const command = buildGeminiCommand(prompt, resultFile, {
|
|
134
|
-
model: attempt.model,
|
|
135
|
-
allowedMcpServers: attempt.allowedMcpServers,
|
|
136
|
-
excludeMcpServers: attempt.excludeMcpServers,
|
|
137
|
-
});
|
|
138
|
-
// PRD A1: lease.env 가 있으면 spawn env 에 적용 (예: GOOGLE_API_KEY 등 account-specific
|
|
139
|
-
// 환경변수). lease 가 null 이면 default 동작 유지 (부모 env inherit).
|
|
140
|
-
const spawnEnv =
|
|
141
|
-
lease?.env && typeof lease.env === "object"
|
|
142
|
-
? { ...process.env, ...lease.env }
|
|
143
|
-
: undefined;
|
|
144
|
-
return runProcess(command, workdir, attempt.timeout, {
|
|
145
|
-
resultFile,
|
|
146
|
-
inferStallMode,
|
|
147
|
-
spawnEnv,
|
|
148
|
-
});
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
// ── Public API ──────────────────────────────────────────────────
|
|
152
|
-
|
|
153
|
-
export async function getCircuitState() {
|
|
154
|
-
const brokerMod = await import("./account-broker.mjs");
|
|
155
|
-
if (!brokerMod.broker) return { state: "closed", failures: [] };
|
|
156
|
-
const snap = brokerMod.broker
|
|
157
|
-
.snapshot()
|
|
158
|
-
.filter((a) => a.provider === "gemini");
|
|
159
|
-
return snap.length
|
|
160
|
-
? { state: snap[0].circuitState, accounts: snap }
|
|
161
|
-
: { state: "closed", failures: [] };
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
export function execute(opts = {}) {
|
|
165
|
-
return executeWithCircuitBroker({
|
|
166
|
-
// Account-broker still stores this compatibility bucket as "gemini";
|
|
167
|
-
// execution itself is routed to agy above.
|
|
168
|
-
provider: "gemini",
|
|
169
|
-
runFn: runGemini,
|
|
170
|
-
preflightFn: (o) => runPreflight({ mcpServers: o.mcpServers }),
|
|
171
|
-
buildAttemptsFn: buildAttempts,
|
|
172
|
-
opts,
|
|
173
|
-
});
|
|
174
|
-
}
|
package/hub/intent.mjs
DELETED
|
@@ -1,321 +0,0 @@
|
|
|
1
|
-
// hub/intent.mjs — Intent Classification Engine
|
|
2
|
-
// 사용자 요청의 "진짜 의도"를 분석 → 카테고리 분류 → 최적 에이전트/모델 자동 선택
|
|
3
|
-
|
|
4
|
-
import { execFileSync } from "node:child_process";
|
|
5
|
-
import crypto from "node:crypto";
|
|
6
|
-
import { resolveCodexAgentPolicy } from "../scripts/lib/agent-route-policy.mjs";
|
|
7
|
-
import { whichCommand } from "./platform.mjs";
|
|
8
|
-
|
|
9
|
-
/** 캐시 엔트리: { category, confidence, ts } */
|
|
10
|
-
const _intentCache = new Map();
|
|
11
|
-
const CACHE_TTL_MS = 5 * 60 * 1000; // 5분
|
|
12
|
-
|
|
13
|
-
/** codex 설치 여부 (프로세스당 1회 확인) */
|
|
14
|
-
let _codexAvailable = null;
|
|
15
|
-
|
|
16
|
-
/** @experimental 런타임 미연결 — 향후 통합 예정 */
|
|
17
|
-
function _isCodexAvailable() {
|
|
18
|
-
if (_codexAvailable !== null) return _codexAvailable;
|
|
19
|
-
_codexAvailable = Boolean(whichCommand("codex"));
|
|
20
|
-
return _codexAvailable;
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function _promptHash(prompt) {
|
|
24
|
-
return crypto.createHash("md5").update(prompt).digest("hex");
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function _getCached(hash) {
|
|
28
|
-
const entry = _intentCache.get(hash);
|
|
29
|
-
if (!entry) return null;
|
|
30
|
-
if (Date.now() - entry.ts > CACHE_TTL_MS) {
|
|
31
|
-
_intentCache.delete(hash);
|
|
32
|
-
return null;
|
|
33
|
-
}
|
|
34
|
-
return entry;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function _tryCodexClassify(prompt) {
|
|
38
|
-
try {
|
|
39
|
-
const raw = execFileSync(
|
|
40
|
-
"codex",
|
|
41
|
-
["exec", `Classify intent: ${prompt}. Reply JSON: {intent, confidence}`],
|
|
42
|
-
{ timeout: 8000, encoding: "utf8" },
|
|
43
|
-
);
|
|
44
|
-
// JSON 블록 추출 (응답에 다른 텍스트가 섞일 수 있음)
|
|
45
|
-
const match = raw.match(/\{[\s\S]*?\}/);
|
|
46
|
-
if (!match) return null;
|
|
47
|
-
const parsed = JSON.parse(match[0]);
|
|
48
|
-
const intent = typeof parsed.intent === "string" ? parsed.intent : null;
|
|
49
|
-
const confidence =
|
|
50
|
-
typeof parsed.confidence === "number" ? parsed.confidence : null;
|
|
51
|
-
if (!intent || confidence === null) return null;
|
|
52
|
-
// intent가 알려진 카테고리여야 함
|
|
53
|
-
if (!INTENT_CATEGORIES[intent]) return null;
|
|
54
|
-
return { category: intent, confidence };
|
|
55
|
-
} catch {
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* @experimental
|
|
62
|
-
* triflux 특화 의도 카테고리 (10개)
|
|
63
|
-
*/
|
|
64
|
-
export const INTENT_CATEGORIES = {
|
|
65
|
-
implement: { agent: "executor", mcp: "implement" },
|
|
66
|
-
debug: { agent: "debugger", mcp: "implement" },
|
|
67
|
-
analyze: { agent: "analyst", mcp: "analyze" },
|
|
68
|
-
design: { agent: "architect", mcp: "analyze" },
|
|
69
|
-
review: { agent: "code-reviewer", mcp: "review" },
|
|
70
|
-
document: { agent: "writer", mcp: "docs" },
|
|
71
|
-
research: { agent: "scientist", mcp: "analyze" },
|
|
72
|
-
"quick-fix": { agent: "build-fixer", mcp: "implement" },
|
|
73
|
-
explain: { agent: "writer", mcp: "docs" },
|
|
74
|
-
test: { agent: "test-engineer", mcp: null },
|
|
75
|
-
};
|
|
76
|
-
|
|
77
|
-
function routingFor(category) {
|
|
78
|
-
const route = INTENT_CATEGORIES[category] || INTENT_CATEGORIES.implement;
|
|
79
|
-
return {
|
|
80
|
-
agent: route.agent,
|
|
81
|
-
mcp: route.mcp,
|
|
82
|
-
effort: resolveCodexAgentPolicy(route.agent).profile,
|
|
83
|
-
};
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
/** @internal 키워드 → 카테고리 매핑 패턴 */
|
|
87
|
-
const KEYWORD_PATTERNS = [
|
|
88
|
-
{
|
|
89
|
-
category: "implement",
|
|
90
|
-
keywords: [
|
|
91
|
-
"구현",
|
|
92
|
-
"만들",
|
|
93
|
-
"추가",
|
|
94
|
-
"생성",
|
|
95
|
-
"작성",
|
|
96
|
-
"빌드",
|
|
97
|
-
"implement",
|
|
98
|
-
"create",
|
|
99
|
-
"add",
|
|
100
|
-
"build",
|
|
101
|
-
"make",
|
|
102
|
-
"develop",
|
|
103
|
-
],
|
|
104
|
-
weight: 1.0,
|
|
105
|
-
},
|
|
106
|
-
{
|
|
107
|
-
category: "debug",
|
|
108
|
-
keywords: [
|
|
109
|
-
"버그",
|
|
110
|
-
"에러",
|
|
111
|
-
"오류",
|
|
112
|
-
"고쳐",
|
|
113
|
-
"수정",
|
|
114
|
-
"디버그",
|
|
115
|
-
"fix",
|
|
116
|
-
"bug",
|
|
117
|
-
"error",
|
|
118
|
-
"debug",
|
|
119
|
-
"troubleshoot",
|
|
120
|
-
"crash",
|
|
121
|
-
"broken",
|
|
122
|
-
],
|
|
123
|
-
weight: 1.0,
|
|
124
|
-
},
|
|
125
|
-
{
|
|
126
|
-
category: "analyze",
|
|
127
|
-
keywords: [
|
|
128
|
-
"분석",
|
|
129
|
-
"조사",
|
|
130
|
-
"파악",
|
|
131
|
-
"analyze",
|
|
132
|
-
"investigate",
|
|
133
|
-
"examine",
|
|
134
|
-
"inspect",
|
|
135
|
-
],
|
|
136
|
-
weight: 0.9,
|
|
137
|
-
},
|
|
138
|
-
{
|
|
139
|
-
category: "design",
|
|
140
|
-
keywords: [
|
|
141
|
-
"설계",
|
|
142
|
-
"아키텍처",
|
|
143
|
-
"디자인",
|
|
144
|
-
"구조",
|
|
145
|
-
"design",
|
|
146
|
-
"architect",
|
|
147
|
-
"structure",
|
|
148
|
-
],
|
|
149
|
-
weight: 0.9,
|
|
150
|
-
},
|
|
151
|
-
{
|
|
152
|
-
category: "review",
|
|
153
|
-
keywords: ["리뷰", "검토", "코드리뷰", "review", "code review", "audit"],
|
|
154
|
-
weight: 1.0,
|
|
155
|
-
},
|
|
156
|
-
{
|
|
157
|
-
category: "document",
|
|
158
|
-
keywords: [
|
|
159
|
-
"문서",
|
|
160
|
-
"도큐먼트",
|
|
161
|
-
"문서화",
|
|
162
|
-
"document",
|
|
163
|
-
"docs",
|
|
164
|
-
"documentation",
|
|
165
|
-
"readme",
|
|
166
|
-
],
|
|
167
|
-
weight: 0.9,
|
|
168
|
-
},
|
|
169
|
-
{
|
|
170
|
-
category: "research",
|
|
171
|
-
keywords: ["리서치", "연구", "탐색", "research", "explore", "study"],
|
|
172
|
-
weight: 0.8,
|
|
173
|
-
},
|
|
174
|
-
{
|
|
175
|
-
category: "quick-fix",
|
|
176
|
-
keywords: ["빠르게", "간단히", "급한", "quick fix", "hotfix", "quick"],
|
|
177
|
-
weight: 0.85,
|
|
178
|
-
},
|
|
179
|
-
{
|
|
180
|
-
category: "explain",
|
|
181
|
-
keywords: [
|
|
182
|
-
"설명",
|
|
183
|
-
"뭐야",
|
|
184
|
-
"알려",
|
|
185
|
-
"이해",
|
|
186
|
-
"explain",
|
|
187
|
-
"what is",
|
|
188
|
-
"how does",
|
|
189
|
-
"tell me",
|
|
190
|
-
"describe",
|
|
191
|
-
],
|
|
192
|
-
weight: 1.0,
|
|
193
|
-
},
|
|
194
|
-
{
|
|
195
|
-
category: "test",
|
|
196
|
-
keywords: [
|
|
197
|
-
"테스트",
|
|
198
|
-
"테스팅",
|
|
199
|
-
"시험",
|
|
200
|
-
"검증",
|
|
201
|
-
"test",
|
|
202
|
-
"testing",
|
|
203
|
-
"spec",
|
|
204
|
-
"unit test",
|
|
205
|
-
],
|
|
206
|
-
weight: 1.0,
|
|
207
|
-
},
|
|
208
|
-
];
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* 키워드 기반 빠른 분류 (0-cost, Codex 호출 없이)
|
|
212
|
-
* 고신뢰(>0.8) 시 Codex triage 건너뜀
|
|
213
|
-
* @param {string} prompt - 사용자 프롬프트
|
|
214
|
-
* @returns {{ category: string, confidence: number }}
|
|
215
|
-
*/
|
|
216
|
-
export function quickClassify(prompt) {
|
|
217
|
-
if (!prompt || typeof prompt !== "string" || !prompt.trim()) {
|
|
218
|
-
return { category: "implement", confidence: 0.1 };
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
const lower = prompt.toLowerCase().trim();
|
|
222
|
-
let bestCategory = null;
|
|
223
|
-
let bestScore = 0;
|
|
224
|
-
let bestMatchCount = 0;
|
|
225
|
-
|
|
226
|
-
for (const { category, keywords, weight } of KEYWORD_PATTERNS) {
|
|
227
|
-
let matchCount = 0;
|
|
228
|
-
for (const kw of keywords) {
|
|
229
|
-
if (lower.includes(kw)) matchCount++;
|
|
230
|
-
}
|
|
231
|
-
if (matchCount > 0) {
|
|
232
|
-
const score = (matchCount / keywords.length) * weight;
|
|
233
|
-
if (score > bestScore) {
|
|
234
|
-
bestScore = score;
|
|
235
|
-
bestCategory = category;
|
|
236
|
-
bestMatchCount = matchCount;
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
if (!bestCategory) {
|
|
242
|
-
return { category: "implement", confidence: 0.3 };
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
// 매칭 품질 기반 신뢰도 (0.5~0.95 범위) — matchCount 기준으로 정규화 (3개 매칭이면 최대)
|
|
246
|
-
const confidence = Math.min(
|
|
247
|
-
0.95,
|
|
248
|
-
0.5 + (Math.min(bestMatchCount, 3) / 3) * 0.45,
|
|
249
|
-
);
|
|
250
|
-
return { category: bestCategory, confidence };
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
/**
|
|
254
|
-
* 전체 의도 분류 — routing 정보 포함
|
|
255
|
-
* Codex triage 경로: codex 설치 시 실행, confidence > 0.8이면 즉시 반환
|
|
256
|
-
* quickClassify가 고신뢰(>0.8)이면 Codex 건너뜀
|
|
257
|
-
* 결과는 md5 해시 기반 Map에 5분 TTL로 캐싱
|
|
258
|
-
* @param {string} prompt
|
|
259
|
-
* @returns {{ category: string, confidence: number, reasoning: string, routing: { agent: string, mcp: string|null, effort: string|null } }}
|
|
260
|
-
*/
|
|
261
|
-
export function classifyIntent(prompt) {
|
|
262
|
-
const hash = _promptHash(String(prompt ?? ""));
|
|
263
|
-
|
|
264
|
-
// 캐시 확인
|
|
265
|
-
const cached = _getCached(hash);
|
|
266
|
-
if (cached) {
|
|
267
|
-
const routing = routingFor(cached.category);
|
|
268
|
-
return {
|
|
269
|
-
category: cached.category,
|
|
270
|
-
confidence: cached.confidence,
|
|
271
|
-
reasoning: `cache-hit: ${cached.category} (${cached.confidence.toFixed(2)})`,
|
|
272
|
-
routing,
|
|
273
|
-
};
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
// quickClassify 먼저
|
|
277
|
-
const quick = quickClassify(prompt);
|
|
278
|
-
|
|
279
|
-
let category = quick.category;
|
|
280
|
-
let confidence = quick.confidence;
|
|
281
|
-
let reasoning;
|
|
282
|
-
|
|
283
|
-
// quickClassify가 고신뢰(>0.8)이면 Codex 건너뜀
|
|
284
|
-
if (quick.confidence > 0.8) {
|
|
285
|
-
reasoning = `keyword-match: ${category} (${confidence.toFixed(2)})`;
|
|
286
|
-
} else if (_isCodexAvailable()) {
|
|
287
|
-
// Codex triage
|
|
288
|
-
const codexResult = _tryCodexClassify(String(prompt ?? ""));
|
|
289
|
-
if (codexResult && codexResult.confidence > 0.8) {
|
|
290
|
-
category = codexResult.category;
|
|
291
|
-
confidence = codexResult.confidence;
|
|
292
|
-
reasoning = `codex-triage: ${category} (${confidence.toFixed(2)})`;
|
|
293
|
-
} else {
|
|
294
|
-
reasoning = `keyword-match(codex-fallback): ${category} (${confidence.toFixed(2)})`;
|
|
295
|
-
}
|
|
296
|
-
} else {
|
|
297
|
-
reasoning = `keyword-match: ${category} (${confidence.toFixed(2)})`;
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
// 캐시 저장
|
|
301
|
-
_intentCache.set(hash, { category, confidence, ts: Date.now() });
|
|
302
|
-
|
|
303
|
-
const routing = routingFor(category);
|
|
304
|
-
return {
|
|
305
|
-
category,
|
|
306
|
-
confidence,
|
|
307
|
-
reasoning,
|
|
308
|
-
routing,
|
|
309
|
-
};
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
/**
|
|
313
|
-
* 분류 히스토리 기반 학습 (reflexion 연동 가능)
|
|
314
|
-
* @param {string} prompt
|
|
315
|
-
* @param {string} actualCategory - 실제 카테고리
|
|
316
|
-
*/
|
|
317
|
-
export function refineClassification(prompt, actualCategory) {
|
|
318
|
-
// reflexion 연동 시 store에 오분류 기록 저장 예정
|
|
319
|
-
void prompt;
|
|
320
|
-
void actualCategory;
|
|
321
|
-
}
|