@triflux/core 10.90.0 → 10.92.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/hub/bridge.mjs +1 -0
- package/hub/lib/jev-addon.mjs +148 -5
- package/package.json +1 -1
- package/scripts/lib/agent-route-policy.mjs +28 -1
package/hub/bridge.mjs
CHANGED
package/hub/lib/jev-addon.mjs
CHANGED
|
@@ -360,8 +360,23 @@ async function callJevCli(request, options = {}) {
|
|
|
360
360
|
options.timeoutMs ?? env.TFX_JEV_TIMEOUT_MS ?? DEFAULT_JEV_TIMEOUT_MS,
|
|
361
361
|
);
|
|
362
362
|
const args = [request.kind, request.query, "--json"];
|
|
363
|
-
|
|
364
|
-
|
|
363
|
+
// CLI 에는 criteria 인자가 없어 선택지의 뜻을 문맥으로 함께 보낸다.
|
|
364
|
+
const criteria =
|
|
365
|
+
request.criteria &&
|
|
366
|
+
typeof request.criteria === "object" &&
|
|
367
|
+
!Array.isArray(request.criteria)
|
|
368
|
+
? Object.entries(request.criteria).map(
|
|
369
|
+
([key, text]) => `- ${key}: ${text}`,
|
|
370
|
+
)
|
|
371
|
+
: [];
|
|
372
|
+
const context = [
|
|
373
|
+
request.context,
|
|
374
|
+
criteria.length > 0 ? `Options:\n${criteria.join("\n")}` : "",
|
|
375
|
+
]
|
|
376
|
+
.filter(Boolean)
|
|
377
|
+
.join("\n\n");
|
|
378
|
+
if (context) {
|
|
379
|
+
args.push("--context", context);
|
|
365
380
|
}
|
|
366
381
|
if (Array.isArray(request.options) && request.options.length > 0) {
|
|
367
382
|
args.push("--options", request.options.join(","));
|
|
@@ -1062,6 +1077,19 @@ const COMPLEXITY_ROUTE_MAP = Object.freeze({
|
|
|
1062
1077
|
}),
|
|
1063
1078
|
});
|
|
1064
1079
|
|
|
1080
|
+
// 복잡도로 등급을 고르는 범용 역할이다. 그 밖의 역할은 기본 등급 대비 방향만 답하고
|
|
1081
|
+
// 실제 등급은 agent-route-policy.mjs 의 사다리가 정한다.
|
|
1082
|
+
const TIER_ROUTABLE_ROLES = new Set([
|
|
1083
|
+
"executor",
|
|
1084
|
+
"codex",
|
|
1085
|
+
"antigravity",
|
|
1086
|
+
"agy",
|
|
1087
|
+
"claude",
|
|
1088
|
+
"lead",
|
|
1089
|
+
]);
|
|
1090
|
+
// 승격은 비용을, 강등은 실패와 재시도를 키우므로 불확실하면 역할 기본 등급을 유지한다.
|
|
1091
|
+
const TIER_CHANGE_MIN_CONFIDENCE = 0.85;
|
|
1092
|
+
|
|
1065
1093
|
/**
|
|
1066
1094
|
* 6. Pre-route 통합 판단: 프롬프트 복잡도(모델 및 effort), 실행 권한 모드(read_only / workspace_auto / bypass),
|
|
1067
1095
|
* 필요한 MCP 프로필 및 서버 목록을 단일 queryJevBatch 호출로 판정한다.
|
|
@@ -1166,6 +1194,23 @@ export async function judgeTaskRouting(
|
|
|
1166
1194
|
},
|
|
1167
1195
|
};
|
|
1168
1196
|
|
|
1197
|
+
if (!TIER_ROUTABLE_ROLES.has(agentType)) {
|
|
1198
|
+
questions.tier_shift = {
|
|
1199
|
+
hook: "task_routing_tier_shift",
|
|
1200
|
+
kind: "choice",
|
|
1201
|
+
options: ["lower", "keep", "raise"],
|
|
1202
|
+
criteria: {
|
|
1203
|
+
lower:
|
|
1204
|
+
"Mechanical or tiny work needing far less judgment than this role normally applies.",
|
|
1205
|
+
keep: "Normal work for this role, even when it spans several files or sounds important.",
|
|
1206
|
+
raise:
|
|
1207
|
+
"Unusually hard for this role: security-critical, irreversible, or cross-system reasoning a normal run of this role would likely get wrong.",
|
|
1208
|
+
},
|
|
1209
|
+
query:
|
|
1210
|
+
"This worker role already has a default model tier chosen for its usual work. Compared with that default, what does this specific task need?",
|
|
1211
|
+
};
|
|
1212
|
+
}
|
|
1213
|
+
|
|
1169
1214
|
for (const server of candidateServers) {
|
|
1170
1215
|
const safeKey = `mcp_${server.replace(/[^a-zA-Z0-9_]/g, "_")}`;
|
|
1171
1216
|
questions[safeKey] = {
|
|
@@ -1185,6 +1230,13 @@ export async function judgeTaskRouting(
|
|
|
1185
1230
|
const compRes = results.complexity;
|
|
1186
1231
|
const modeRes = results.execution_mode;
|
|
1187
1232
|
const mcpProfRes = results.mcp_profile;
|
|
1233
|
+
const shiftRes = results.tier_shift;
|
|
1234
|
+
const tierShift =
|
|
1235
|
+
shiftRes &&
|
|
1236
|
+
shiftRes.confidence >= TIER_CHANGE_MIN_CONFIDENCE &&
|
|
1237
|
+
(shiftRes.choice === "lower" || shiftRes.choice === "raise")
|
|
1238
|
+
? shiftRes.choice
|
|
1239
|
+
: null;
|
|
1188
1240
|
|
|
1189
1241
|
const complexity =
|
|
1190
1242
|
compRes &&
|
|
@@ -1230,17 +1282,23 @@ export async function judgeTaskRouting(
|
|
|
1230
1282
|
!complexity &&
|
|
1231
1283
|
!executionMode &&
|
|
1232
1284
|
!mcpProfile &&
|
|
1285
|
+
!tierShift &&
|
|
1233
1286
|
neededMcpServers === null
|
|
1234
1287
|
) {
|
|
1235
1288
|
return null;
|
|
1236
1289
|
}
|
|
1237
1290
|
|
|
1238
|
-
const routeSpec =
|
|
1239
|
-
|
|
1240
|
-
|
|
1291
|
+
const routeSpec =
|
|
1292
|
+
complexity &&
|
|
1293
|
+
TIER_ROUTABLE_ROLES.has(agentType) &&
|
|
1294
|
+
(complexity === "standard" ||
|
|
1295
|
+
compRes.confidence >= TIER_CHANGE_MIN_CONFIDENCE)
|
|
1296
|
+
? COMPLEXITY_ROUTE_MAP[complexity] || null
|
|
1297
|
+
: null;
|
|
1241
1298
|
|
|
1242
1299
|
let codexProfile = routeSpec ? routeSpec.codexProfile : null;
|
|
1243
1300
|
if (
|
|
1301
|
+
routeSpec &&
|
|
1244
1302
|
complexity === "standard" &&
|
|
1245
1303
|
/deslop|cleanup|format|lint|rename|typo/i.test(trimmedPrompt)
|
|
1246
1304
|
) {
|
|
@@ -1278,6 +1336,7 @@ export async function judgeTaskRouting(
|
|
|
1278
1336
|
executionArgs: execFlags ? execFlags.args : null,
|
|
1279
1337
|
mcpProfile,
|
|
1280
1338
|
neededMcpServers,
|
|
1339
|
+
tierShift,
|
|
1281
1340
|
confidence: avgConfidence,
|
|
1282
1341
|
};
|
|
1283
1342
|
}
|
|
@@ -1312,6 +1371,33 @@ export async function judgeMcpSelection(
|
|
|
1312
1371
|
};
|
|
1313
1372
|
}
|
|
1314
1373
|
|
|
1374
|
+
/**
|
|
1375
|
+
* 7-1. Claude 워커 MCP 선택 shadow: 적용하지 않고 판정만 기록해 절감량과 도구 손실을 재는 근거를 쌓는다.
|
|
1376
|
+
*/
|
|
1377
|
+
export async function recordMcpSelectionShadow(
|
|
1378
|
+
{ taskText = "", agentType = "default", availableServers = [] } = {},
|
|
1379
|
+
options = {},
|
|
1380
|
+
) {
|
|
1381
|
+
if (!isJevEnabled(options) || availableServers.length === 0) return null;
|
|
1382
|
+
const selection = await judgeMcpSelection(
|
|
1383
|
+
{ taskText, agentType, availableServers },
|
|
1384
|
+
options,
|
|
1385
|
+
);
|
|
1386
|
+
recordJevStat(
|
|
1387
|
+
{
|
|
1388
|
+
hook: "mcp_selection_shadow",
|
|
1389
|
+
kind: "shadow",
|
|
1390
|
+
intervened: false,
|
|
1391
|
+
agentType,
|
|
1392
|
+
candidates: availableServers,
|
|
1393
|
+
selected: selection?.neededServers ?? null,
|
|
1394
|
+
confidence: selection?.confidence ?? null,
|
|
1395
|
+
},
|
|
1396
|
+
options,
|
|
1397
|
+
);
|
|
1398
|
+
return selection;
|
|
1399
|
+
}
|
|
1400
|
+
|
|
1315
1401
|
/**
|
|
1316
1402
|
* 8. 다중 세션 중 프롬프트에 맞는 대상 세션 자동 선택 (--thread auto 모호성 해소).
|
|
1317
1403
|
*/
|
|
@@ -1746,3 +1832,60 @@ export async function judgeReviewGate(
|
|
|
1746
1832
|
confidence: gateRes.confidence,
|
|
1747
1833
|
};
|
|
1748
1834
|
}
|
|
1835
|
+
|
|
1836
|
+
// 라우팅 정본의 결정트리 줄(`- D3: ...; D4 ...`)을 분기별 규칙으로 나눈다.
|
|
1837
|
+
export function parseRoutingBranches(markdown = "") {
|
|
1838
|
+
const branches = {};
|
|
1839
|
+
for (const line of String(markdown).split(/\r?\n/)) {
|
|
1840
|
+
if (!/^- D\d+:/.test(line)) continue;
|
|
1841
|
+
for (const part of line.slice(2).split(/;\s+(?=D\d+\s)/)) {
|
|
1842
|
+
const match = part.match(/^D(\d+):?\s+(.+)$/);
|
|
1843
|
+
if (match) branches[`D${match[1]}`] = match[2].trim();
|
|
1844
|
+
}
|
|
1845
|
+
}
|
|
1846
|
+
return branches;
|
|
1847
|
+
}
|
|
1848
|
+
|
|
1849
|
+
/**
|
|
1850
|
+
* 14. tfx-harness 스킬 선택: 라우팅 정본 전문을 읽지 않도록 맞는 분기 하나를 고른다.
|
|
1851
|
+
* D0(명시 지정)은 호출자가 먼저 처리하고, 기준은 정본에서 읽어 사본을 두지 않는다.
|
|
1852
|
+
*/
|
|
1853
|
+
export async function judgeRoutingBranch(
|
|
1854
|
+
{ task = "", routingText = "" } = {},
|
|
1855
|
+
options = {},
|
|
1856
|
+
) {
|
|
1857
|
+
if (!isJevEnabled(options)) return null;
|
|
1858
|
+
const trimmedTask = clampText(task, 1000);
|
|
1859
|
+
if (!trimmedTask) return null;
|
|
1860
|
+
|
|
1861
|
+
const { D0: _explicit, ...branches } = parseRoutingBranches(routingText);
|
|
1862
|
+
const allowed = Object.keys(branches);
|
|
1863
|
+
if (allowed.length < 2) return null;
|
|
1864
|
+
// 맞는 분기가 없을 때 억지로 고르지 않도록 none 을 같은 분포에서 경쟁시킨다.
|
|
1865
|
+
const none = "none";
|
|
1866
|
+
|
|
1867
|
+
const res = await queryJev(
|
|
1868
|
+
{
|
|
1869
|
+
hook: "routing_branch",
|
|
1870
|
+
kind: "choice",
|
|
1871
|
+
options: [...allowed, none],
|
|
1872
|
+
criteria: {
|
|
1873
|
+
...branches,
|
|
1874
|
+
[none]: "No branch condition clearly matches this request.",
|
|
1875
|
+
},
|
|
1876
|
+
query:
|
|
1877
|
+
"This is an ordered routing decision tree. Choose the lowest-numbered branch whose condition matches the request, or none.",
|
|
1878
|
+
context: `Request:\n${trimmedTask}`,
|
|
1879
|
+
},
|
|
1880
|
+
options,
|
|
1881
|
+
);
|
|
1882
|
+
if (!res || res.confidence < minConfidence(options)) return null;
|
|
1883
|
+
if (!allowed.includes(res.choice)) return null;
|
|
1884
|
+
// D4 소유자는 규칙 한 줄이 아니라 스택 공존 매트릭스가 정하므로 전문 읽기로 돌린다.
|
|
1885
|
+
if (res.choice === "D4") return null;
|
|
1886
|
+
return {
|
|
1887
|
+
branch: res.choice,
|
|
1888
|
+
rule: branches[res.choice],
|
|
1889
|
+
confidence: res.confidence,
|
|
1890
|
+
};
|
|
1891
|
+
}
|
package/package.json
CHANGED
|
@@ -187,6 +187,15 @@ const CANONICAL_CODEX_PROFILE_OVERRIDES = Object.freeze(
|
|
|
187
187
|
"astra_ultra",
|
|
188
188
|
]),
|
|
189
189
|
);
|
|
190
|
+
// 비용 순서다. Jev 는 방향만 답하고 등급은 여기서 한 단계만 옮긴다.
|
|
191
|
+
const CODEX_TIER_LADDER = Object.freeze([
|
|
192
|
+
"luna_low",
|
|
193
|
+
"luna_high",
|
|
194
|
+
"sol_med",
|
|
195
|
+
"sol_high",
|
|
196
|
+
"astra_xhigh",
|
|
197
|
+
]);
|
|
198
|
+
const NO_LOWER_CODEX_AGENTS = Object.freeze(new Set(["security-reviewer"]));
|
|
190
199
|
const ULTRA_ELIGIBLE_CODEX_AGENTS = Object.freeze(
|
|
191
200
|
new Set(["deep-executor", "scientist-deep"]),
|
|
192
201
|
);
|
|
@@ -212,6 +221,16 @@ export function resolveCodexAgentPolicy(agent) {
|
|
|
212
221
|
* later process-level `TFX_CODEX_PROFILE`. Ultra is reserved for an eligible
|
|
213
222
|
* top-level role; every nested/sandboxed or ineligible request is max.
|
|
214
223
|
*/
|
|
224
|
+
export function shiftCodexAgentProfile(agent, tierShift) {
|
|
225
|
+
const { profile } = resolveDirectCodexRoutePolicy(agent);
|
|
226
|
+
const index = CODEX_TIER_LADDER.indexOf(profile);
|
|
227
|
+
if (index === -1) return profile;
|
|
228
|
+
if (tierShift === "raise") return CODEX_TIER_LADDER[index + 1] ?? profile;
|
|
229
|
+
if (tierShift === "lower" && !NO_LOWER_CODEX_AGENTS.has(agent))
|
|
230
|
+
return CODEX_TIER_LADDER[index - 1] ?? profile;
|
|
231
|
+
return profile;
|
|
232
|
+
}
|
|
233
|
+
|
|
215
234
|
export function resolveCodexAgentProfile(
|
|
216
235
|
agent,
|
|
217
236
|
{
|
|
@@ -400,6 +419,7 @@ function parseCliArgs(argv) {
|
|
|
400
419
|
let profileOverride = "auto";
|
|
401
420
|
let geminiProfilesModule = "";
|
|
402
421
|
let nested = false;
|
|
422
|
+
let tierShift = "";
|
|
403
423
|
while (args.length > 0) {
|
|
404
424
|
const flag = args.shift();
|
|
405
425
|
if (flag === "--format") format = args.shift() || "";
|
|
@@ -410,6 +430,7 @@ function parseCliArgs(argv) {
|
|
|
410
430
|
else if (flag === "--gemini-profiles-module")
|
|
411
431
|
geminiProfilesModule = args.shift() || "";
|
|
412
432
|
else if (flag === "--nested") nested = true;
|
|
433
|
+
else if (flag === "--tier-shift") tierShift = args.shift() || "";
|
|
413
434
|
else throw new Error(`unknown argument: ${flag}`);
|
|
414
435
|
}
|
|
415
436
|
if (!new Set(["json", "tsv", "route"]).has(format)) {
|
|
@@ -422,6 +443,7 @@ function parseCliArgs(argv) {
|
|
|
422
443
|
profileOverride,
|
|
423
444
|
geminiProfilesModule,
|
|
424
445
|
nested,
|
|
446
|
+
tierShift,
|
|
425
447
|
};
|
|
426
448
|
}
|
|
427
449
|
|
|
@@ -433,9 +455,14 @@ async function runCli() {
|
|
|
433
455
|
profileOverride,
|
|
434
456
|
geminiProfilesModule,
|
|
435
457
|
nested,
|
|
458
|
+
tierShift,
|
|
436
459
|
} = parseCliArgs(process.argv.slice(2));
|
|
437
460
|
if (format === "route") {
|
|
438
461
|
if (!mapFile) throw new Error("--map-file is required for route format");
|
|
462
|
+
const routedProfile =
|
|
463
|
+
tierShift && normalizeCodexProfileName(profileOverride) === "auto"
|
|
464
|
+
? shiftCodexAgentProfile(agent, tierShift)
|
|
465
|
+
: profileOverride;
|
|
439
466
|
const map = JSON.parse(readFileSync(mapFile, "utf8"));
|
|
440
467
|
let weeklyPercent = 0;
|
|
441
468
|
if (agent === "code-reviewer" || agent === "quality-reviewer") {
|
|
@@ -470,7 +497,7 @@ async function runCli() {
|
|
|
470
497
|
}
|
|
471
498
|
}
|
|
472
499
|
process.stdout.write(
|
|
473
|
-
resolveAgentRoute(agent, map,
|
|
500
|
+
resolveAgentRoute(agent, map, routedProfile, nested, resolver, {
|
|
474
501
|
reviewAuthor: process.env.TFX_REVIEW_AUTHOR ?? "codex",
|
|
475
502
|
weeklyPercent,
|
|
476
503
|
}).join("\x1e"),
|