agentlas 0.9.5 → 0.9.6
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/CHANGELOG.md +16 -0
- package/engine/agentlas-capabilities.cjs +94 -2
- package/engine/agentlas-experience-exchange.cjs +66 -11
- package/engine/agentlas-i18n.cjs +4 -0
- package/engine/agentlas-judgment.cjs +0 -0
- package/engine/agentlas-memory-governance.cjs +1 -11
- package/engine/agentlas-memory-import.cjs +1 -6
- package/engine/agentlas-repl.cjs +31 -10
- package/engine/agentlas-secret-patterns.cjs +71 -0
- package/engine/agentlas.cjs +172 -17
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.6 — 2026-07-25
|
|
4
|
+
|
|
5
|
+
- Agent and App Builder routing is now decided by the connected model: lexical
|
|
6
|
+
scores only recruit candidates, and the model can route a request the keyword
|
|
7
|
+
lists never matched (any language). The App Builder consent handshake is
|
|
8
|
+
unchanged, and every route receipt says whether the connected model or the
|
|
9
|
+
deterministic fallback decided.
|
|
10
|
+
- Whether an agent produces images (and therefore which runtime runs it) is now
|
|
11
|
+
judged by the connected model from the agent's own identity, with the old
|
|
12
|
+
keyword list demoted to reference hints. Conservative non-image vetoes stay.
|
|
13
|
+
- Task classification, routing, and image judgments all fail over to the
|
|
14
|
+
previous deterministic behavior — explicitly labeled — when no connected
|
|
15
|
+
model is available.
|
|
16
|
+
- Publication gates pin the Agentlas OS v1.1.61 runtime commit, which ships the
|
|
17
|
+
same judgment-engine migration across the bundled Core runtime.
|
|
18
|
+
|
|
3
19
|
## 0.9.4 — 2026-07-23
|
|
4
20
|
|
|
5
21
|
- `plugin add` no longer registers a code-hosting page (GitHub/GitLab/Bitbucket
|
|
@@ -63,7 +63,10 @@ const IMAGE_TOOL_MARKERS = [/nano-?banana/i, /\bimagen\b/i, /gpt-image/i, /grok\
|
|
|
63
63
|
// 겹치는 정규식 여러 개를 동시에 때려도 1클러스터다.
|
|
64
64
|
const MIN_BODY_IMAGE_SENTENCES = 3;
|
|
65
65
|
const BODY_SCAN_CAP = 16000; // 로컬 임포트 상한과 동일 — 클라우드 무제한 프롬프트의 전문 스캔 방지
|
|
66
|
-
|
|
66
|
+
// 어휘 판정 — 연결 모델이 없을 때의 결정적 폴백이자, 모델 판정 전의 참고 프리필터.
|
|
67
|
+
// 하우스 룰: 단어목록은 최종 결정을 내리지 않는다. 최종 판정은 resolveNeedsImage가
|
|
68
|
+
// 상주 판정 서비스(judgeLabels)에 묻고, 이 함수는 그 폴백으로만 살아남는다.
|
|
69
|
+
function needsImageLexical(agent) {
|
|
67
70
|
if (!agent) return false;
|
|
68
71
|
if (NON_IMAGE_ROLES.has(String(agent.role || "").toLowerCase())) return false;
|
|
69
72
|
// 정체성 존은 사용자가 선언한 이름/태그라인만 — slug는 폴더명에서 기계 파생되므로
|
|
@@ -87,6 +90,82 @@ function needsImage(agent) {
|
|
|
87
90
|
return false;
|
|
88
91
|
}
|
|
89
92
|
|
|
93
|
+
// ── 상주 판정 서비스 배선 — 모델이 의미로 최종 판정, IMAGE_HINTS는 참고 힌트 ──────
|
|
94
|
+
// needsImage 호출자(REPL 배지·autoRuntimeFor·routingNote)는 동기라서 warm-cache 패턴:
|
|
95
|
+
// 비동기 경로(resolveNeedsImage)가 먼저 판정해 캐시를 데우고, 동기 needsImage는 캐시만
|
|
96
|
+
// 읽는다. 캐시 미스 = 어휘 폴백 그대로 — imageJudgmentSource가 어느 쪽이었는지 라벨한다.
|
|
97
|
+
const IMAGE_VERDICT_CACHE_MAX = 200;
|
|
98
|
+
const imageVerdicts = new Map();
|
|
99
|
+
function imageJudgeInput(agent) {
|
|
100
|
+
const identity = [agent.slug, agent.name, agent.name_en, agent.tagline, agent.tagline_en].filter(Boolean).join(" | ");
|
|
101
|
+
return `${identity}\n---\n${String(agent.system_prompt || "").slice(0, 6000)}`;
|
|
102
|
+
}
|
|
103
|
+
// 역할/팀 베토는 보수적 하드 가드로 유지 — 조율 두뇌가 부서 소개 문장("Design HQ")으로
|
|
104
|
+
// 이미지 팀이 되던 사고(vibecoder/appbridge)는 모델 판정 대상에서 아예 뺀다.
|
|
105
|
+
function imageJudgeVetoed(agent) {
|
|
106
|
+
if (!agent) return true;
|
|
107
|
+
if (NON_IMAGE_ROLES.has(String(agent.role || "").toLowerCase())) return true;
|
|
108
|
+
if (String(agent.entity_kind || "").toLowerCase() === "team") return true;
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
// 이 에이전트의 직무가 이미지 생산인지를 연결 모델이 의미로 판정한다.
|
|
112
|
+
// 러너 없음/타임아웃/정크 → 어휘 판정을 "fallback"으로 라벨해 반환 (조용한 회귀 금지).
|
|
113
|
+
async function resolveNeedsImage(agent) {
|
|
114
|
+
const lexical = needsImageLexical(agent);
|
|
115
|
+
if (imageJudgeVetoed(agent)) return { image: lexical, source: "deterministic" };
|
|
116
|
+
let judgment;
|
|
117
|
+
try {
|
|
118
|
+
judgment = require("./agentlas-judgment.cjs");
|
|
119
|
+
} catch {
|
|
120
|
+
judgment = null;
|
|
121
|
+
}
|
|
122
|
+
if (!judgment || !judgment.hasJudgmentRunner()) return { image: lexical, source: "fallback" };
|
|
123
|
+
const input = imageJudgeInput(agent);
|
|
124
|
+
const cached = imageVerdicts.get(input);
|
|
125
|
+
if (cached) return cached;
|
|
126
|
+
const verdict = await judgment.judgeLabels({
|
|
127
|
+
kind: "agent-produces-images",
|
|
128
|
+
question:
|
|
129
|
+
"Does this agent's OWN job include producing images (generating or designing visual assets such as thumbnails, banners, logos, posters, product shots)?",
|
|
130
|
+
labels: ["image", "not-image"],
|
|
131
|
+
multi: false,
|
|
132
|
+
input,
|
|
133
|
+
hints: { image: IMAGE_HINTS.map((re) => re.source) },
|
|
134
|
+
guidance:
|
|
135
|
+
"Judge the agent's role from its identity and instructions, in any language. Mentioning images is not " +
|
|
136
|
+
"producing them: builders, orchestrators, PMs, curators, and coordination brains that commission or " +
|
|
137
|
+
"delegate image work are 'not-image'. Refusals or prohibitions ('never generate images') declare the " +
|
|
138
|
+
"opposite of a capability.",
|
|
139
|
+
fallback: [lexical ? "image" : "not-image"],
|
|
140
|
+
});
|
|
141
|
+
if (verdict.source !== "llm" || !verdict.labels.length) return { image: lexical, source: "fallback" };
|
|
142
|
+
const out = { image: verdict.labels[0] === "image", source: "llm", reason: verdict.reason || "" };
|
|
143
|
+
imageVerdicts.set(input, out);
|
|
144
|
+
if (imageVerdicts.size > IMAGE_VERDICT_CACHE_MAX) {
|
|
145
|
+
const oldest = imageVerdicts.keys().next().value;
|
|
146
|
+
if (oldest !== undefined) imageVerdicts.delete(oldest);
|
|
147
|
+
}
|
|
148
|
+
return out;
|
|
149
|
+
}
|
|
150
|
+
// Does this agent's job involve generating/handling images? Sync surface for badges and
|
|
151
|
+
// autoRuntimeFor: model verdict from the warm cache wins; miss = deterministic lexical fallback.
|
|
152
|
+
function needsImage(agent) {
|
|
153
|
+
if (!agent) return false;
|
|
154
|
+
if (!imageJudgeVetoed(agent)) {
|
|
155
|
+
const cached = imageVerdicts.get(imageJudgeInput(agent));
|
|
156
|
+
if (cached) return cached.image;
|
|
157
|
+
}
|
|
158
|
+
return needsImageLexical(agent);
|
|
159
|
+
}
|
|
160
|
+
// 라벨용 — 이 에이전트의 현재 이미지 판정이 모델("llm")인지 결정적 경로("deterministic")인지.
|
|
161
|
+
function imageJudgmentSource(agent) {
|
|
162
|
+
if (agent && !imageJudgeVetoed(agent) && imageVerdicts.has(imageJudgeInput(agent))) return "llm";
|
|
163
|
+
return "deterministic";
|
|
164
|
+
}
|
|
165
|
+
function clearImageJudgments() {
|
|
166
|
+
imageVerdicts.clear();
|
|
167
|
+
}
|
|
168
|
+
|
|
90
169
|
// Auto-pick a runtime spec for an agent given installed CLI kinds and the session default spec.
|
|
91
170
|
// Image agents route to an installed image-capable runtime; otherwise keep the session default.
|
|
92
171
|
function autoRuntimeFor(agent, { installedKinds, activeSpec }) {
|
|
@@ -103,4 +182,17 @@ function badge(spec) {
|
|
|
103
182
|
return c.image ? "🖼" : "";
|
|
104
183
|
}
|
|
105
184
|
|
|
106
|
-
module.exports = {
|
|
185
|
+
module.exports = {
|
|
186
|
+
RUNTIME_CAPS,
|
|
187
|
+
CLI_KINDS,
|
|
188
|
+
capsFor,
|
|
189
|
+
specOf,
|
|
190
|
+
runtimeFromSpec,
|
|
191
|
+
needsImage,
|
|
192
|
+
needsImageLexical,
|
|
193
|
+
resolveNeedsImage,
|
|
194
|
+
imageJudgmentSource,
|
|
195
|
+
clearImageJudgments,
|
|
196
|
+
autoRuntimeFor,
|
|
197
|
+
badge,
|
|
198
|
+
};
|
|
@@ -97,22 +97,20 @@ const SEMVER_RE = /^v?[0-9]+\.[0-9]+\.[0-9]+(?:[-+][A-Za-z0-9.-]+)?$/;
|
|
|
97
97
|
const ENV_RE = /^[A-Z][A-Z0-9_]*$/;
|
|
98
98
|
const SAFE_IDEMPOTENCY_RE = /^[A-Za-z0-9._:-]{8,200}$/;
|
|
99
99
|
|
|
100
|
-
const SECRET_PATTERNS =
|
|
101
|
-
/\bsk-(?:proj-)?[A-Za-z0-9_-]{20,}\b/i,
|
|
102
|
-
/\bxox[baprs]-[A-Za-z0-9-]{20,}\b/i,
|
|
103
|
-
/\bgh[pousr]_[A-Za-z0-9_]{20,}\b/,
|
|
104
|
-
/\bAKIA[0-9A-Z]{16}\b/,
|
|
105
|
-
/-----BEGIN (?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----/i,
|
|
106
|
-
/\b(?:api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret|password|passwd|private[_-]?key|cookie)\b\s*[:=]\s*['"]?[^\s'"]{8,}/i,
|
|
107
|
-
/\bauthorization\b\s*[:=]\s*['"]?(?:bearer|basic)\s+[A-Za-z0-9._~+/=-]{8,}/i,
|
|
108
|
-
];
|
|
100
|
+
const { SECRET_PATTERNS } = require("./agentlas-secret-patterns.cjs");
|
|
109
101
|
const PII_PATTERNS = [
|
|
110
102
|
/\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i,
|
|
111
103
|
/(?<!\w)(?:\+?\d[\d ().-]{8,}\d)(?!\w)/,
|
|
112
104
|
/\b(?:account|customer|client|tenant|workspace|user)[ _-]?(?:id|key|number|no)\s*[:=#]?\s*[A-Za-z0-9_-]{4,}\b|(?:계정|고객|사용자)[ _-]?(?:id|아이디|번호)\s*[:=#]?\s*[A-Za-z0-9_-]{4,}/i,
|
|
113
105
|
];
|
|
106
|
+
// Absolute LOCAL paths and file URLs only. The previous alternation matched any
|
|
107
|
+
// slash-containing token, so ordinary prose lost its experience: "TCP/IP", "read/write",
|
|
108
|
+
// "and/or", and web routes like "GET /api/users" were all reported as a local path. A
|
|
109
|
+
// leading-slash path now has to look like a real filesystem root (or start from a home /
|
|
110
|
+
// relative marker, a Windows drive, or a UNC share); a lone `/word` — which is what a web
|
|
111
|
+
// route looks like — no longer counts, and neither does `word/word` inside a sentence.
|
|
114
112
|
const LOCAL_PATH_PATTERNS = [
|
|
115
|
-
/(?:file:\/\/|(?:^|[\s"'`()\[\]{}=:,;])(?:\.\.[/\\]|~[/\\]|\/(
|
|
113
|
+
/(?:file:\/\/|(?:^|[\s"'`()\[\]{}=:,;])(?:\.\.[/\\]|~[/\\]|\/(?:Users|home|root|private|var|tmp|opt|etc|srv|mnt|media|Volumes|Applications|System|Library|usr)\/[^\s"'`<>]+|[A-Za-z]:[/\\]\S+|\\\\[^\\/\s]+[\\/][^\\/\s]+))/i,
|
|
116
114
|
];
|
|
117
115
|
const RAW_INTERACTION_PATTERNS = [
|
|
118
116
|
/(?:^|\n)\s*(?:system|assistant|user|tool|customer|agent)\s*:\s+/i,
|
|
@@ -1530,7 +1528,13 @@ function isCanonicalTaskId(value) {
|
|
|
1530
1528
|
function keywordOccurs(normalizedPrompt, rawKeyword) {
|
|
1531
1529
|
const keyword = normalizeClassificationText(rawKeyword);
|
|
1532
1530
|
if (!keyword) return false;
|
|
1533
|
-
if (/[가-힣]/.test(keyword))
|
|
1531
|
+
if (/[가-힣]/.test(keyword)) {
|
|
1532
|
+
// Korean has no word boundary, so a raw includes() matched inside longer compounds:
|
|
1533
|
+
// 번역 hit 번역기, 금융 hit compound finance words, 영업 hit 영업일 (business day).
|
|
1534
|
+
// Require the keyword not be glued to another Hangul syllable on either side.
|
|
1535
|
+
const escaped = keyword.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1536
|
+
return new RegExp(`(?<![가-힣])${escaped}(?![가-힣])`).test(normalizedPrompt);
|
|
1537
|
+
}
|
|
1534
1538
|
return ` ${normalizedPrompt} `.includes(` ${keyword} `);
|
|
1535
1539
|
}
|
|
1536
1540
|
|
|
@@ -1557,6 +1561,56 @@ function deriveCanonicalTaskClasses(prompt, options = {}) {
|
|
|
1557
1561
|
return { taskIds: matches, source: "deterministic-keyword-map", matchedTaskClasses: matches, invalidDeclaredCount: 0 };
|
|
1558
1562
|
}
|
|
1559
1563
|
|
|
1564
|
+
/**
|
|
1565
|
+
* Meaning-aware task classification. The resident judge decides; TASK_CLASS_KEYWORDS is
|
|
1566
|
+
* passed as reference hints only. This is what a keyword map cannot do: Korean compounds
|
|
1567
|
+
* (번역 inside 번역기), particle inflection (고객 문의를), dialect, slang, and any other
|
|
1568
|
+
* language all hinge on meaning, and no list enumerates them.
|
|
1569
|
+
*
|
|
1570
|
+
* An explicit declared task class still wins outright (it is data, not a guess), and with
|
|
1571
|
+
* no connected model the deterministic keyword prefilter is the fallback so classification
|
|
1572
|
+
* never stops working.
|
|
1573
|
+
*/
|
|
1574
|
+
async function resolveCanonicalTaskClasses(prompt, options = {}) {
|
|
1575
|
+
const declaredRaw = options.declaredTaskClasses ?? options.declaredTaskClass;
|
|
1576
|
+
if (declaredRaw != null && (Array.isArray(declaredRaw) ? declaredRaw.length : String(declaredRaw).trim())) {
|
|
1577
|
+
return deriveCanonicalTaskClasses(prompt, options);
|
|
1578
|
+
}
|
|
1579
|
+
const prefilter = deriveCanonicalTaskClasses(prompt, options);
|
|
1580
|
+
const judgment = require("./agentlas-judgment.cjs");
|
|
1581
|
+
if (!judgment.hasJudgmentRunner()) return prefilter;
|
|
1582
|
+
|
|
1583
|
+
const hints = {};
|
|
1584
|
+
for (const slug of CANONICAL_TASK_SLUGS) {
|
|
1585
|
+
const words = TASK_CLASS_KEYWORDS[slug];
|
|
1586
|
+
if (Array.isArray(words) && words.length) hints[slug] = words;
|
|
1587
|
+
}
|
|
1588
|
+
const verdict = await judgment.judgeLabels({
|
|
1589
|
+
kind: "experience-task-class",
|
|
1590
|
+
question:
|
|
1591
|
+
"Which kinds of work does this request actually involve? Judge the user's real task, not words that merely appear.",
|
|
1592
|
+
labels: CANONICAL_TASK_SLUGS,
|
|
1593
|
+
input: String(prompt || ""),
|
|
1594
|
+
guidance:
|
|
1595
|
+
"Return a label only when that kind of work is genuinely part of the request. A word inside an " +
|
|
1596
|
+
"unrelated compound or a different sense of the word does not count. Return an empty list for " +
|
|
1597
|
+
"content with no identifiable task (hashes, ids, random strings).",
|
|
1598
|
+
hints,
|
|
1599
|
+
fallback: [],
|
|
1600
|
+
signal: options.signal,
|
|
1601
|
+
});
|
|
1602
|
+
if (verdict.source !== "llm") return prefilter;
|
|
1603
|
+
const taskIds = CANONICAL_TASK_IDS.filter((id) =>
|
|
1604
|
+
verdict.labels.some((slug) => id === `${CANONICAL_TASK_PREFIX}${slug}`));
|
|
1605
|
+
return {
|
|
1606
|
+
taskIds,
|
|
1607
|
+
source: "model-judgment",
|
|
1608
|
+
matchedTaskClasses: taskIds,
|
|
1609
|
+
invalidDeclaredCount: 0,
|
|
1610
|
+
...(verdict.reason ? { judgmentReason: verdict.reason } : {}),
|
|
1611
|
+
};
|
|
1612
|
+
}
|
|
1613
|
+
|
|
1560
1614
|
function parseEnvironmentConstraint(value) {
|
|
1561
1615
|
const normalized = normalizedTaxonomyAtom(value);
|
|
1562
1616
|
const contract = EXPERIENCE_TAXONOMY_V1.environment;
|
|
@@ -2140,6 +2194,7 @@ module.exports = {
|
|
|
2140
2194
|
environmentConstraintsMatch,
|
|
2141
2195
|
selectApplicablePortableItems,
|
|
2142
2196
|
deriveCanonicalTaskClasses,
|
|
2197
|
+
resolveCanonicalTaskClasses,
|
|
2143
2198
|
readExactLocalBaseMarker,
|
|
2144
2199
|
exactTaskSignatureInPrompt,
|
|
2145
2200
|
resolveRuntimeExperienceForAgent,
|
package/engine/agentlas-i18n.cjs
CHANGED
|
@@ -111,6 +111,8 @@ const STRINGS = {
|
|
|
111
111
|
"team.usage": "usage: /team · /team <agent> <claude-code|codex|gemini|auto>",
|
|
112
112
|
"team.set": "%s → %s",
|
|
113
113
|
"routedImage": "routed to %s for image support",
|
|
114
|
+
"judge.source.llm": "judged by the connected model",
|
|
115
|
+
"judge.source.fallback": "deterministic fallback (no connected-model verdict)",
|
|
114
116
|
"usageBar": "tokens",
|
|
115
117
|
"config.title": "Engine auto engagement — explicit on/off (default: off)",
|
|
116
118
|
"config.storm": "Stormbreaker auto-engage on direct-routed real work",
|
|
@@ -325,6 +327,8 @@ const STRINGS = {
|
|
|
325
327
|
"team.usage": "사용법: /team · /team <에이전트> <claude-code|codex|gemini|auto>",
|
|
326
328
|
"team.set": "%s → %s",
|
|
327
329
|
"routedImage": "이미지 지원을 위해 %s로 라우팅",
|
|
330
|
+
"judge.source.llm": "판정: 연결 모델",
|
|
331
|
+
"judge.source.fallback": "판정: 결정적 폴백(연결 모델 없음)",
|
|
328
332
|
"usageBar": "토큰",
|
|
329
333
|
"config.title": "엔진 자동 개입 — 명시적 on/off (기본: off)",
|
|
330
334
|
"config.storm": "직답 라우팅된 실작업에 Stormbreaker 자동 개입",
|
|
Binary file
|
|
@@ -30,17 +30,7 @@ const FINAL_SCOPES = new Set(["user_global", "team", "agent", "project", "sessio
|
|
|
30
30
|
const SEMANTIC_DISPOSITIONS = new Set(["retain", "session", "discard", "review"]);
|
|
31
31
|
const CONFIDENCE_LEVELS = new Set(["high", "medium", "low"]);
|
|
32
32
|
|
|
33
|
-
const SECRET_PATTERNS =
|
|
34
|
-
/-----BEGIN [A-Z ]*PRIVATE KEY-----[\s\S]*?-----END [A-Z ]*PRIVATE KEY-----/i,
|
|
35
|
-
/\b(?:sk|rk|pk)-(?:ant|proj|live|test)?-?[A-Za-z0-9_-]{16,}\b/i,
|
|
36
|
-
/\bgh[pousr]_[A-Za-z0-9]{20,}\b/i,
|
|
37
|
-
/\bxox[baprs]-[A-Za-z0-9-]{16,}\b/i,
|
|
38
|
-
/\bAIza[A-Za-z0-9_-]{30,}\b/,
|
|
39
|
-
/\bAKIA[A-Z0-9]{16}\b/,
|
|
40
|
-
/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\b/,
|
|
41
|
-
/\b(?:password|passwd|api[_-]?key|access[_-]?token|refresh[_-]?token|client[_-]?secret)\s*[:=]\s*[^\s,;]{6,}/i,
|
|
42
|
-
/\bauthorization\s*:\s*bearer\s+[^\s,;]{8,}/i,
|
|
43
|
-
];
|
|
33
|
+
const { SECRET_PATTERNS } = require("./agentlas-secret-patterns.cjs");
|
|
44
34
|
const ABSOLUTE_PATH_PATTERNS = [
|
|
45
35
|
/(?:^|[\s("'`])~\/[A-Za-z0-9._-]/,
|
|
46
36
|
/(?:^|[\s("'`])\/(?:Users|home|private|var|tmp|opt|etc|Volumes|Applications|System|Library)\//,
|
|
@@ -30,12 +30,7 @@ const ALWAYS_KEEP_HINT = /(team[-_ ]?memory|glossary|dossier|handoff|safety|scop
|
|
|
30
30
|
|
|
31
31
|
// Minimal secret guard (a subset of the shared secret-patterns chokepoint) so a
|
|
32
32
|
// stray key in legacy notes never becomes a durable memory row.
|
|
33
|
-
const
|
|
34
|
-
/(sk-[A-Za-z0-9]{20,}|sk_live_[A-Za-z0-9]{16,}|github_pat_[A-Za-z0-9_]{20,}|gh[opsu]_[A-Za-z0-9]{20,}|AIza[0-9A-Za-z_-]{20,}|glpat-[A-Za-z0-9_-]{16,}|xox[baprs]-[A-Za-z0-9-]{10,}|-----BEGIN [A-Z ]*PRIVATE KEY-----|AKIA[0-9A-Z]{16})/;
|
|
35
|
-
|
|
36
|
-
function looksSecret(content) {
|
|
37
|
-
return SECRET_RE.test(String(content || ""));
|
|
38
|
-
}
|
|
33
|
+
const { looksSecret } = require("./agentlas-secret-patterns.cjs");
|
|
39
34
|
|
|
40
35
|
function substantiveBody(body) {
|
|
41
36
|
const lines = body.split("\n").map((l) => l.trim()).filter(Boolean);
|
package/engine/agentlas-repl.cjs
CHANGED
|
@@ -643,12 +643,26 @@ function startRepl(opts) {
|
|
|
643
643
|
state.modelPinned = false;
|
|
644
644
|
state.native = {};
|
|
645
645
|
}
|
|
646
|
+
// 이미지 능력 판정 warm-cache — 동기 호출자(applyRuntimeFor/배지)가 읽기 전에 비동기
|
|
647
|
+
// 경로에서 상주 판정 서비스를 먼저 데운다. 러너 없음/실패는 어휘 폴백(라벨은 routingNote가 찍음).
|
|
648
|
+
async function warmImageJudgment(agentRow) {
|
|
649
|
+
if (!agentRow || typeof caps.resolveNeedsImage !== "function") return;
|
|
650
|
+
try {
|
|
651
|
+
await caps.resolveNeedsImage(agentRow);
|
|
652
|
+
} catch {
|
|
653
|
+
/* lexical fallback */
|
|
654
|
+
}
|
|
655
|
+
}
|
|
646
656
|
// Tell the user when we routed to an image-capable runtime, or when the current one can't make images.
|
|
647
657
|
function routingNote(subject) {
|
|
648
658
|
if (!subject || !caps.needsImage(subject.capAgent)) return;
|
|
649
659
|
const spec = caps.specOf(state.runtime);
|
|
650
660
|
if (caps.capsFor(spec).image) {
|
|
651
|
-
if (spec !== caps.specOf(baseRuntime))
|
|
661
|
+
if (spec !== caps.specOf(baseRuntime)) {
|
|
662
|
+
// 판정 주체 라벨 — 모델 판정인지 결정적 폴백인지 반드시 밝힌다(조용한 폴백 금지).
|
|
663
|
+
const judged = typeof caps.imageJudgmentSource === "function" && caps.imageJudgmentSource(subject.capAgent) === "llm";
|
|
664
|
+
ui.info(ui.t("routedImage", spec) + " — " + ui.t(judged ? "judge.source.llm" : "judge.source.fallback"));
|
|
665
|
+
}
|
|
652
666
|
} else {
|
|
653
667
|
ui.warn(ui.t("guard.imageWarn", caps.capsFor(spec).label || spec));
|
|
654
668
|
}
|
|
@@ -701,10 +715,11 @@ function startRepl(opts) {
|
|
|
701
715
|
state.routePreambleOnce = null;
|
|
702
716
|
applyRuntimeFor(state.subject);
|
|
703
717
|
}
|
|
704
|
-
function switchSubject(kind, query) {
|
|
718
|
+
async function switchSubject(kind, query) {
|
|
705
719
|
if (kind === "agent") {
|
|
706
720
|
const agent = H.resolveAgent(db, query);
|
|
707
721
|
if (!agent) return ui.error(ui.t("noAgent", query));
|
|
722
|
+
await warmImageJudgment(agent); // 런타임 자동 배정 전에 모델 판정을 데운다
|
|
708
723
|
setSubjectAgent(agent);
|
|
709
724
|
} else {
|
|
710
725
|
const firm = H.resolveFirm(db, query);
|
|
@@ -933,11 +948,11 @@ function startRepl(opts) {
|
|
|
933
948
|
}
|
|
934
949
|
case "agent":
|
|
935
950
|
if (!arg) return ui.warn(ui.t("agentUsage")), true;
|
|
936
|
-
switchSubject("agent", arg);
|
|
951
|
+
await switchSubject("agent", arg);
|
|
937
952
|
return true;
|
|
938
953
|
case "firm":
|
|
939
954
|
if (!arg) return ui.warn(ui.t("firmUsage")), true;
|
|
940
|
-
switchSubject("firm", arg);
|
|
955
|
+
await switchSubject("firm", arg);
|
|
941
956
|
return true;
|
|
942
957
|
case "runtime":
|
|
943
958
|
setRuntime(arg);
|
|
@@ -1364,7 +1379,8 @@ function startRepl(opts) {
|
|
|
1364
1379
|
}
|
|
1365
1380
|
|
|
1366
1381
|
// ── interactive picker (when no agent was given) ──
|
|
1367
|
-
function chooseAndStart(setter, row) {
|
|
1382
|
+
async function chooseAndStart(setter, row) {
|
|
1383
|
+
if (setter === setSubjectAgent) await warmImageJudgment(row); // firm은 팀 베토라 판정 대상 아님
|
|
1368
1384
|
setter(row);
|
|
1369
1385
|
ui.ok(ui.t("switched", state.subject.label));
|
|
1370
1386
|
routingNote(state.subject);
|
|
@@ -1402,12 +1418,14 @@ function startRepl(opts) {
|
|
|
1402
1418
|
if (a) return chooseAndStart(setSubjectAgent, a);
|
|
1403
1419
|
const f = H.resolveFirm(db, t);
|
|
1404
1420
|
if (f) return chooseAndStart(setSubjectFirm, f);
|
|
1405
|
-
if (H.autoRouteAgent) {
|
|
1406
|
-
|
|
1421
|
+
if (H.autoRouteAgent || H.resolveAutoRoute) {
|
|
1422
|
+
// 연결 모델이 라우트를 최종 판정한다(resolveAutoRoute) — 없으면 어휘 폴백.
|
|
1423
|
+
const choice = H.resolveAutoRoute ? await H.resolveAutoRoute(db, t, ui.lang) : H.autoRouteAgent(db, t, ui.lang);
|
|
1407
1424
|
if (choice) {
|
|
1408
1425
|
if (choice.direct) {
|
|
1409
1426
|
setSubjectDirect();
|
|
1410
1427
|
} else {
|
|
1428
|
+
await warmImageJudgment(choice.agent);
|
|
1411
1429
|
setSubjectAgent(choice.agent);
|
|
1412
1430
|
}
|
|
1413
1431
|
state.routePreambleOnce = H.autoRoutePreamble ? H.autoRoutePreamble(choice, ui.lang) : null;
|
|
@@ -1442,6 +1460,7 @@ function startRepl(opts) {
|
|
|
1442
1460
|
if (/^\d+$/.test(t)) {
|
|
1443
1461
|
const n = parseInt(t, 10);
|
|
1444
1462
|
if (n >= 1 && n <= ags.length) {
|
|
1463
|
+
await warmImageJudgment(ags[n - 1]);
|
|
1445
1464
|
setSubjectAgent(ags[n - 1]);
|
|
1446
1465
|
ui.ok(ui.t("switched", state.subject.label));
|
|
1447
1466
|
routingNote(state.subject);
|
|
@@ -1450,13 +1469,14 @@ function startRepl(opts) {
|
|
|
1450
1469
|
}
|
|
1451
1470
|
if (single) {
|
|
1452
1471
|
const a = H.resolveAgent(db, t);
|
|
1453
|
-
if (a) { setSubjectAgent(a); ui.ok(ui.t("switched", state.subject.label)); routingNote(state.subject); return; }
|
|
1472
|
+
if (a) { await warmImageJudgment(a); setSubjectAgent(a); ui.ok(ui.t("switched", state.subject.label)); routingNote(state.subject); return; }
|
|
1454
1473
|
const f = H.resolveFirm(db, t);
|
|
1455
1474
|
if (f) { setSubjectFirm(f); ui.ok(ui.t("switched", state.subject.label)); routingNote(state.subject); return; }
|
|
1456
1475
|
}
|
|
1457
1476
|
}
|
|
1458
|
-
if (H.autoRouteAgent) {
|
|
1459
|
-
|
|
1477
|
+
if (H.autoRouteAgent || H.resolveAutoRoute) {
|
|
1478
|
+
// 연결 모델이 라우트를 최종 판정한다(resolveAutoRoute) — 없으면 어휘 폴백.
|
|
1479
|
+
const choice = H.resolveAutoRoute ? await H.resolveAutoRoute(db, t, ui.lang) : H.autoRouteAgent(db, t, ui.lang);
|
|
1460
1480
|
if (choice) {
|
|
1461
1481
|
if (choice.direct) {
|
|
1462
1482
|
setSubjectDirect();
|
|
@@ -1466,6 +1486,7 @@ function startRepl(opts) {
|
|
|
1466
1486
|
ui.info(H.autoRouteNote ? H.autoRouteNote(choice, ui.lang) : `direct answer (no agent)`);
|
|
1467
1487
|
}
|
|
1468
1488
|
} else {
|
|
1489
|
+
await warmImageJudgment(choice.agent);
|
|
1469
1490
|
setSubjectAgent(choice.agent);
|
|
1470
1491
|
state.routePreambleOnce = H.autoRoutePreamble ? H.autoRoutePreamble(choice, ui.lang) : null;
|
|
1471
1492
|
ui.info(H.autoRouteNote ? H.autoRouteNote(choice, ui.lang) : `auto-routed to ${choice.agent.name}`);
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Credential/secret detection for every terminal write boundary that must never persist
|
|
3
|
+
// or transmit a live key. Three inline copies had drifted apart (memory import missed
|
|
4
|
+
// JWTs, generic `key=` assignments and bearer headers; governance missed sk_live_/
|
|
5
|
+
// github_pat_/glpat-; experience exchange missed Google AIza and JWTs), so the same
|
|
6
|
+
// secret was caught at one boundary and stored in plain text at another. One list, one
|
|
7
|
+
// behaviour: extend HERE, not at a call site. Mirrors the desktop's
|
|
8
|
+
// shared/secret-patterns.ts.
|
|
9
|
+
//
|
|
10
|
+
// Scope rule: match *credential shapes*, not the words around them. Ordinary prose that
|
|
11
|
+
// mentions "token", or a hyphenated phrase like "risk-management-notes", must not trip
|
|
12
|
+
// this — a false positive silently drops a user's memory, which is its own data loss.
|
|
13
|
+
|
|
14
|
+
/** Live-credential shapes across the providers this product actually touches. */
|
|
15
|
+
const SECRET_SHAPES = [
|
|
16
|
+
// GitHub: classic PAT, OAuth/user/server/refresh tokens, fine-grained PAT.
|
|
17
|
+
/gh[pousr]_[A-Za-z0-9]{20,}/,
|
|
18
|
+
/github_pat_[A-Za-z0-9_]{20,}/,
|
|
19
|
+
// Slack bot/user/app tokens.
|
|
20
|
+
/xox[baprs]-[A-Za-z0-9-]{20,}/,
|
|
21
|
+
// AWS access key ids (long-lived and STS).
|
|
22
|
+
/(?:AKIA|ASIA)[0-9A-Z]{16}/,
|
|
23
|
+
// Google / Firebase API keys.
|
|
24
|
+
/AIza[0-9A-Za-z_-]{30,}/,
|
|
25
|
+
// Stripe and similar: secret/restricted/publishable, live or test.
|
|
26
|
+
/(?:sk|rk|pk)_(?:live|test)_[A-Za-z0-9]{16,}/,
|
|
27
|
+
// OpenAI / Anthropic, including provider-segmented forms. The \b prevents an ordinary
|
|
28
|
+
// hyphenated phrase ("ask-forgiveness-not-permission") from matching.
|
|
29
|
+
/\bsk-(?:proj-|ant-)?[A-Za-z0-9_-]{12,}/,
|
|
30
|
+
// HuggingFace, GitLab, npm.
|
|
31
|
+
/hf_[A-Za-z0-9]{20,}/,
|
|
32
|
+
/glpat-[A-Za-z0-9_-]{20,}/,
|
|
33
|
+
/npm_[A-Za-z0-9]{20,}/,
|
|
34
|
+
// JWTs (three base64url segments) — bearer tokens frequently land in pasted logs.
|
|
35
|
+
/eyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{8,}/,
|
|
36
|
+
// Telegram bot tokens.
|
|
37
|
+
/\b[0-9]{8,}:[A-Za-z0-9_-]{25,}\b/,
|
|
38
|
+
// Private key blocks.
|
|
39
|
+
/-----BEGIN [A-Z ]*PRIVATE KEY-----/,
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
/** `password: hunter2` style assignments, where the value shape alone proves nothing. */
|
|
43
|
+
const SECRET_ASSIGNMENT_RE =
|
|
44
|
+
/\b(?:password|passwd|secret|api[_-]?key|access[_-]?token|refresh[_-]?token|auth[_-]?token|client[_-]?secret|private[_-]?key|cookie|bearer)\b\s*[:=]\s*['"]?[^\s,;'"]{6,}/i;
|
|
45
|
+
|
|
46
|
+
/** `Authorization: Bearer …` / `Basic …` headers. */
|
|
47
|
+
const AUTH_HEADER_RE = /\bauthorization\b\s*[:=]\s*['"]?(?:bearer|basic)\s+[A-Za-z0-9._~+/=-]{8,}/i;
|
|
48
|
+
|
|
49
|
+
/** Single source of truth. Case-insensitive: providers are inconsistent about casing. */
|
|
50
|
+
const SECRET_PATTERNS = [
|
|
51
|
+
...SECRET_SHAPES.map((re) => new RegExp(re.source, "i")),
|
|
52
|
+
SECRET_ASSIGNMENT_RE,
|
|
53
|
+
AUTH_HEADER_RE,
|
|
54
|
+
];
|
|
55
|
+
|
|
56
|
+
/** True when the text contains something that looks like a live credential. */
|
|
57
|
+
function looksSecret(content) {
|
|
58
|
+
const text = String(content || "");
|
|
59
|
+
return SECRET_PATTERNS.some((re) => re.test(text));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Replace credential-shaped substrings with a marker, preserving surrounding text. */
|
|
63
|
+
function redactSecrets(content, marker = "[redacted-secret]") {
|
|
64
|
+
let out = String(content || "");
|
|
65
|
+
for (const re of SECRET_PATTERNS) {
|
|
66
|
+
out = out.replace(new RegExp(re.source, re.flags.includes("g") ? re.flags : `${re.flags}g`), marker);
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { SECRET_PATTERNS, looksSecret, redactSecrets };
|
package/engine/agentlas.cjs
CHANGED
|
@@ -580,6 +580,24 @@ function directSystemPrompt(lang) {
|
|
|
580
580
|
? "당신은 Agentlas 터미널의 기본 어시스턴트입니다. 특별한 페르소나 없이 사용자의 요청에 정확하고 간결하게 바로 답하세요. 에이전트 라우팅이나 이미지 생성 능력을 스스로 언급하지 마세요."
|
|
581
581
|
: "You are the Agentlas terminal's default assistant. Answer the user's request directly and concisely, with no special persona. Do not bring up agent routing or image-generation capabilities on your own.";
|
|
582
582
|
}
|
|
583
|
+
// 어휘 스코어링 — 설치 에이전트 전원을 점수순으로 나열한다. 하우스 룰에 따라 이 점수는
|
|
584
|
+
// (1) resolveAutoRoute의 후보 모집과 (2) 라벨 붙은 오프라인 폴백(autoRouteAgent)으로만
|
|
585
|
+
// 쓰인다 — 연결 모델이 있으면 최종 라우트 결정은 상주 판정 서비스가 의미로 내린다.
|
|
586
|
+
function rankRouteAgents(db, prompt, lang) {
|
|
587
|
+
const resolvedLang = lang || prefsLang();
|
|
588
|
+
const agents = listRoutableAgents(db).filter((agent) => !NON_GENERIC_ROUTE_SLUGS.has(agent.slug));
|
|
589
|
+
if (!agents.length) return [];
|
|
590
|
+
let terms = routeTokenize(prompt);
|
|
591
|
+
// 헤이스택은 한 번만 계산해 IDF와 스코어링 양쪽에서 재사용한다.
|
|
592
|
+
const hays = agents.map((agent) => ({ identityHay: routeIdentityHaystack(agent), haystack: routeHaystack(agent) }));
|
|
593
|
+
// IDF 근사 — 설치 에이전트 절반 이상의 haystack에 나오는 단어("ai","도구" 등)는 판별력이 없어 제외.
|
|
594
|
+
if (agents.length >= 3) {
|
|
595
|
+
terms = terms.filter((term) => hays.filter((h) => h.haystack.includes(term)).length * 2 <= agents.length);
|
|
596
|
+
}
|
|
597
|
+
return agents
|
|
598
|
+
.map((agent, i) => scoreRouteAgent(prompt, terms, agent, resolvedLang, hays[i]))
|
|
599
|
+
.sort((a, b) => b.score - a.score);
|
|
600
|
+
}
|
|
583
601
|
function autoRouteAgent(db, prompt, lang) {
|
|
584
602
|
const resolvedLang = lang || prefsLang();
|
|
585
603
|
// 명확한 "에이전트/팀/회사 만들기" 의도 → 메타-빌더로 직행 (약한 키워드 점수에 밀리지 않게).
|
|
@@ -598,35 +616,123 @@ function autoRouteAgent(db, prompt, lang) {
|
|
|
598
616
|
};
|
|
599
617
|
}
|
|
600
618
|
}
|
|
601
|
-
const
|
|
602
|
-
if (!
|
|
603
|
-
let terms = routeTokenize(prompt);
|
|
604
|
-
// 헤이스택은 한 번만 계산해 IDF와 스코어링 양쪽에서 재사용한다.
|
|
605
|
-
const hays = agents.map((agent) => ({ identityHay: routeIdentityHaystack(agent), haystack: routeHaystack(agent) }));
|
|
606
|
-
// IDF 근사 — 설치 에이전트 절반 이상의 haystack에 나오는 단어("ai","도구" 등)는 판별력이 없어 제외.
|
|
607
|
-
if (agents.length >= 3) {
|
|
608
|
-
terms = terms.filter((term) => hays.filter((h) => h.haystack.includes(term)).length * 2 <= agents.length);
|
|
609
|
-
}
|
|
610
|
-
const ranked = agents
|
|
611
|
-
.map((agent, i) => scoreRouteAgent(prompt, terms, agent, resolvedLang, hays[i]))
|
|
612
|
-
.sort((a, b) => b.score - a.score);
|
|
619
|
+
const ranked = rankRouteAgents(db, prompt, resolvedLang);
|
|
620
|
+
if (!ranked.length) return directRouteChoice(resolvedLang);
|
|
613
621
|
// 1위가 아니라 "임계값+strong을 모두 만족하는 최고 순위"를 뽑는다 — 장황한 프롬프트의
|
|
614
622
|
// 약한 단어 적중이 점수 1위를 먹어도, 자격 있는 전문 에이전트가 직답으로 밀려나지 않는다.
|
|
615
623
|
const pick = ranked.find((r) => r.score >= MIN_ROUTE_SCORE && r.strong);
|
|
616
624
|
if (pick) return pick;
|
|
617
625
|
return directRouteChoice(resolvedLang);
|
|
618
626
|
}
|
|
627
|
+
// ── 모델 최종 라우팅 판정 ──────────────────────────────────
|
|
628
|
+
// 하우스 룰: 단어목록/정규식은 최종 라우트 결정을 내리지 않는다. 연결 모델이 있으면
|
|
629
|
+
// judgeLabels가 후보(어휘 모집 + 설치 전원, 상한) + 합성 라벨(meta-builder/app-builder/
|
|
630
|
+
// direct) 중에서 의미로 하나를 고른다 — 어휘 점수 0점인 에이전트(아랍어 등 어떤 언어의
|
|
631
|
+
// 요청이든)도 모델은 뽑을 수 있다. 닫힌형 가드는 결정적으로 유지: 잡담 short-circuit
|
|
632
|
+
// (isTrivialRoutePrompt)과 경로 스트리핑(ROUTE_PATH_RE)은 모델 호출 전에 그대로 적용.
|
|
633
|
+
// 러너 없음/타임아웃/정크 → 기존 어휘 라우팅(autoRouteAgent)을 routeSource:"deterministic"
|
|
634
|
+
// 으로 라벨해 반환한다 — 조용한 폴백 금지(라우트 노트에 판정 주체가 찍힌다).
|
|
635
|
+
const ROUTE_JUDGE_CANDIDATE_CAP = 30;
|
|
636
|
+
const ROUTE_JUDGE_DIRECT_LABEL = "direct";
|
|
637
|
+
const ROUTE_JUDGE_META_LABEL = "meta-builder";
|
|
638
|
+
const ROUTE_JUDGE_APP_LABEL = "app-builder";
|
|
639
|
+
const APP_BUILDER_ROUTE_SLUG = "agentlas-app-builder";
|
|
640
|
+
async function resolveAutoRoute(db, prompt, lang) {
|
|
641
|
+
const resolvedLang = lang || prefsLang();
|
|
642
|
+
const prefilter = autoRouteAgent(db, prompt, resolvedLang);
|
|
643
|
+
let judgment;
|
|
644
|
+
try {
|
|
645
|
+
judgment = require("./agentlas-judgment.cjs");
|
|
646
|
+
} catch {
|
|
647
|
+
judgment = null;
|
|
648
|
+
}
|
|
649
|
+
if (!judgment || !judgment.hasJudgmentRunner()) return { ...prefilter, routeSource: "deterministic" };
|
|
650
|
+
const promptText = routeNormalize(routeStripPaths(prompt));
|
|
651
|
+
// 잡담("hi")까지 모델에 물으면 매 턴이 느려진다 — 정확 매칭 가드는 닫힌형이라 결정적 유지.
|
|
652
|
+
if (!promptText.trim() || isTrivialRoutePrompt(promptText)) return { ...prefilter, routeSource: "deterministic" };
|
|
653
|
+
const ranked = rankRouteAgents(db, prompt, resolvedLang);
|
|
654
|
+
const meta = resolveMetaBuilder(db);
|
|
655
|
+
if (!ranked.length && !meta) return { ...prefilter, routeSource: "deterministic" };
|
|
656
|
+
// App Builder는 합성 라벨로만 제시한다(동의 핸드셰이크가 걸린 특수 라우트임을 모델에 명시).
|
|
657
|
+
const appBuilder = ranked.find((r) => r.agent.slug === APP_BUILDER_ROUTE_SLUG) || null;
|
|
658
|
+
const candidates = ranked.filter((r) => r.agent.slug !== APP_BUILDER_ROUTE_SLUG).slice(0, ROUTE_JUDGE_CANDIDATE_CAP);
|
|
659
|
+
const bySlug = new Map(candidates.map((r) => [r.agent.slug, r]));
|
|
660
|
+
const labels = [...bySlug.keys()];
|
|
661
|
+
const hints = {};
|
|
662
|
+
const roster = [];
|
|
663
|
+
for (const r of candidates) {
|
|
664
|
+
const a = r.agent;
|
|
665
|
+
const name = [...new Set([a.name, a.name_en].filter(Boolean))].join(" / ");
|
|
666
|
+
const tagline = [...new Set([a.tagline, a.tagline_en].filter(Boolean))].join(" / ");
|
|
667
|
+
roster.push(`- ${a.slug}: ${String(name).slice(0, 80)}${tagline ? ` — ${String(tagline).slice(0, 120)}` : ""}`);
|
|
668
|
+
// 옛 단어목록은 힌트로 강등: 큐레이션 힌트 용어 + 이 프롬프트에서 어휘 스코어러가 맞춘 용어.
|
|
669
|
+
const curated = ROUTE_HINTS.find((h) => h.slug === a.slug);
|
|
670
|
+
const hintTerms = [...new Set([...(curated ? curated.terms : []), ...(r.terms || [])])];
|
|
671
|
+
if (hintTerms.length) hints[a.slug] = hintTerms;
|
|
672
|
+
}
|
|
673
|
+
if (appBuilder) {
|
|
674
|
+
labels.push(ROUTE_JUDGE_APP_LABEL);
|
|
675
|
+
roster.push(
|
|
676
|
+
`- ${ROUTE_JUDGE_APP_LABEL}: build a dedicated internal Agentlas App (recurring workflow, durable state, editing surfaces); explicit user consent is asked separately before anything is created`,
|
|
677
|
+
);
|
|
678
|
+
hints[ROUTE_JUDGE_APP_LABEL] = APP_BUILDER_EXPLICIT_TERMS;
|
|
679
|
+
}
|
|
680
|
+
if (meta) {
|
|
681
|
+
labels.push(ROUTE_JUDGE_META_LABEL);
|
|
682
|
+
roster.push(`- ${ROUTE_JUDGE_META_LABEL}: build a NEW agent/team/company itself (the meta-builder)`);
|
|
683
|
+
hints[ROUTE_JUDGE_META_LABEL] = AGENT_BUILD_TERMS;
|
|
684
|
+
}
|
|
685
|
+
labels.push(ROUTE_JUDGE_DIRECT_LABEL);
|
|
686
|
+
roster.push(`- ${ROUTE_JUDGE_DIRECT_LABEL}: no installed agent clearly fits; answer as the plain assistant`);
|
|
687
|
+
const verdict = await judgment.judgeLabels({
|
|
688
|
+
// 라벨 집합(설치 상태)이 바뀌면 캐시 키도 바뀌어야 한다 — kind에 라벨 지문을 넣는다.
|
|
689
|
+
kind: `terminal-auto-route:${crypto.createHash("sha256").update(labels.join("\n")).digest("hex").slice(0, 12)}`,
|
|
690
|
+
question: "Which installed agent should own this user request, if any?",
|
|
691
|
+
labels,
|
|
692
|
+
input: routeStripPaths(String(prompt || "")),
|
|
693
|
+
hints,
|
|
694
|
+
guidance: [
|
|
695
|
+
"Installed agents:",
|
|
696
|
+
...roster,
|
|
697
|
+
"Mentioning a word is not intent — judge what the user actually asks to be done, in any language.",
|
|
698
|
+
`Pick "${ROUTE_JUDGE_META_LABEL}" only when the user asks to create a new agent/team/company itself.`,
|
|
699
|
+
`Pick "${ROUTE_JUDGE_DIRECT_LABEL}" when no installed agent clearly fits the request.`,
|
|
700
|
+
].join("\n"),
|
|
701
|
+
multi: false,
|
|
702
|
+
fallback: [],
|
|
703
|
+
});
|
|
704
|
+
if (verdict.source !== "llm" || !verdict.labels.length) return { ...prefilter, routeSource: "deterministic" };
|
|
705
|
+
const picked = verdict.labels[0];
|
|
706
|
+
const reason =
|
|
707
|
+
verdict.reason ||
|
|
708
|
+
(resolvedLang === "ko" ? "연결 모델이 요청의 의미로 판정했습니다" : "the connected model judged the request by meaning");
|
|
709
|
+
if (picked === ROUTE_JUDGE_DIRECT_LABEL) return { ...directRouteChoice(resolvedLang), reason, routeSource: "llm" };
|
|
710
|
+
if (picked === ROUTE_JUDGE_META_LABEL && meta) {
|
|
711
|
+
return { agent: meta, score: 1000, strong: true, terms: [], reason, routeSource: "llm" };
|
|
712
|
+
}
|
|
713
|
+
const chosen = picked === ROUTE_JUDGE_APP_LABEL ? appBuilder : bySlug.get(picked);
|
|
714
|
+
if (!chosen) return { ...prefilter, routeSource: "deterministic" };
|
|
715
|
+
// 모델 확답은 strong 계약을 충족한다 — 어휘 근거(terms/score)는 참고로 보존.
|
|
716
|
+
return { ...chosen, strong: true, reason, routeSource: "llm" };
|
|
717
|
+
}
|
|
718
|
+
// 라우트 영수증 라벨 — 누가 최종 판정했는지 반드시 찍는다(조용한 폴백 금지 하우스 룰).
|
|
719
|
+
function routeJudgeSourceNote(choice, lang) {
|
|
720
|
+
if (!choice || !choice.routeSource) return "";
|
|
721
|
+
if (choice.routeSource === "llm") return lang === "ko" ? " (판정: 연결 모델)" : " (judged by the connected model)";
|
|
722
|
+
return lang === "ko" ? " (판정: 결정적 폴백 — 연결 모델 없음)" : " (deterministic fallback — no connected-model verdict)";
|
|
723
|
+
}
|
|
619
724
|
function autoRouteNote(choice, lang) {
|
|
620
725
|
const resolvedLang = lang || prefsLang();
|
|
726
|
+
const sourceNote = routeJudgeSourceNote(choice, resolvedLang);
|
|
621
727
|
if (choice.direct) {
|
|
622
728
|
return resolvedLang === "ko"
|
|
623
|
-
? `사용 에이전트: 없음 — 바로 답합니다. 이유: ${choice.reason}
|
|
624
|
-
: `Selected agent: none — answering directly. Reason: ${choice.reason}
|
|
729
|
+
? `사용 에이전트: 없음 — 바로 답합니다. 이유: ${choice.reason}.${sourceNote}`
|
|
730
|
+
: `Selected agent: none — answering directly. Reason: ${choice.reason}.${sourceNote}`;
|
|
625
731
|
}
|
|
626
732
|
const name = resolvedLang === "ko" ? choice.agent.name : choice.agent.name_en || choice.agent.name;
|
|
627
733
|
return resolvedLang === "ko"
|
|
628
|
-
? `사용 에이전트: ${name}. 이유: ${choice.reason}
|
|
629
|
-
: `Selected agent: ${name}. Reason: ${choice.reason}
|
|
734
|
+
? `사용 에이전트: ${name}. 이유: ${choice.reason}.${sourceNote}`
|
|
735
|
+
: `Selected agent: ${name}. Reason: ${choice.reason}.${sourceNote}`;
|
|
630
736
|
}
|
|
631
737
|
function autoRoutePreamble(choice, lang) {
|
|
632
738
|
const resolvedLang = lang || prefsLang();
|
|
@@ -8507,6 +8613,48 @@ const RUNTIME_BIN = {
|
|
|
8507
8613
|
};
|
|
8508
8614
|
|
|
8509
8615
|
// 활성 런타임 → 실행 방식 결정. CLI(claude/codex/gemini) 또는 API(BYOK/Ollama).
|
|
8616
|
+
/**
|
|
8617
|
+
* Wire the resident judgment service to whatever runtime the user actually has connected.
|
|
8618
|
+
* This is what lets classification be decided by MEANING instead of by a wordlist: the
|
|
8619
|
+
* judge asks the connected model, and the old keyword maps are passed only as hints. It is
|
|
8620
|
+
* invisible to the user (no output, read-only, no tools) and every judged call falls back
|
|
8621
|
+
* to the deterministic prefilter when no runtime answers.
|
|
8622
|
+
*/
|
|
8623
|
+
function installJudgmentRunner(db, rt) {
|
|
8624
|
+
let judgment;
|
|
8625
|
+
try {
|
|
8626
|
+
judgment = require("./agentlas-judgment.cjs");
|
|
8627
|
+
} catch {
|
|
8628
|
+
return;
|
|
8629
|
+
}
|
|
8630
|
+
if (!rt || rt.mode !== "cli" || !RUNTIME_BIN[rt.kind]) {
|
|
8631
|
+
judgment.setJudgmentRunner(null);
|
|
8632
|
+
return;
|
|
8633
|
+
}
|
|
8634
|
+
judgment.setJudgmentRunner(async ({ system, prompt, signal }) => {
|
|
8635
|
+
const { runNativeTurn } = require("./agentlas-native-host.cjs");
|
|
8636
|
+
const { Ui } = require("./agentlas-ui.cjs");
|
|
8637
|
+
const silent = new Ui({ lang: prefsLang(), quiet: true });
|
|
8638
|
+
for (const method of ["write", "warn", "info", "tool", "result", "status", "line"]) {
|
|
8639
|
+
if (typeof silent[method] === "function") silent[method] = () => {};
|
|
8640
|
+
}
|
|
8641
|
+
const res = await runNativeTurn({
|
|
8642
|
+
kind: rt.kind,
|
|
8643
|
+
bin: RUNTIME_BIN[rt.kind],
|
|
8644
|
+
prompt,
|
|
8645
|
+
systemPrompt: system,
|
|
8646
|
+
cwd: projectCwd(),
|
|
8647
|
+
permission: "read",
|
|
8648
|
+
session: {},
|
|
8649
|
+
ui: silent,
|
|
8650
|
+
env: process.env,
|
|
8651
|
+
signal,
|
|
8652
|
+
});
|
|
8653
|
+
if (res && res.error) return "";
|
|
8654
|
+
return (res && res.text) || "";
|
|
8655
|
+
});
|
|
8656
|
+
}
|
|
8657
|
+
|
|
8510
8658
|
function resolveRuntime(db, override) {
|
|
8511
8659
|
const ar = activeRuntime(db);
|
|
8512
8660
|
const activeCli = ar && RUNTIME_BIN[ar.kind]
|
|
@@ -8850,6 +8998,9 @@ async function executeOnce(db, system, prompt, override, ctx) {
|
|
|
8850
8998
|
}
|
|
8851
8999
|
const rt = resolveRuntime(db, override);
|
|
8852
9000
|
memoryRuntime = rt;
|
|
9001
|
+
// The resident judge uses the same connected runtime, so wordlist-based classification is
|
|
9002
|
+
// replaced by meaning-based judgment for this turn.
|
|
9003
|
+
installJudgmentRunner(db, rt);
|
|
8853
9004
|
if (rt.mode === "cli") {
|
|
8854
9005
|
// 네이티브 CLI에도 같은 Memory emitter를 주입하되 guard가 화면의 JSON 블록을 숨긴다.
|
|
8855
9006
|
// 큐레이터가 만든 구조화 Memory만 성공 RunReceipt 이후 Experience intake로 전달된다.
|
|
@@ -9502,6 +9653,7 @@ function buildHelpers(db) {
|
|
|
9502
9653
|
listFirms,
|
|
9503
9654
|
firmSystemPrompt,
|
|
9504
9655
|
autoRouteAgent: (db_, prompt, lang) => autoRouteAgent(db_, prompt, lang),
|
|
9656
|
+
resolveAutoRoute: (db_, prompt, lang) => resolveAutoRoute(db_, prompt, lang),
|
|
9505
9657
|
autoRouteNote: (choice, lang) => autoRouteNote(choice, lang),
|
|
9506
9658
|
autoRoutePreamble: (choice, lang) => autoRoutePreamble(choice, lang),
|
|
9507
9659
|
directSystemPrompt: (lang) => directSystemPrompt(lang),
|
|
@@ -10185,7 +10337,8 @@ async function cmdRun(db, query, prompt, runtimeOverride, runtimeExperience = nu
|
|
|
10185
10337
|
|
|
10186
10338
|
async function cmdAutoRun(db, prompt, runtimeOverride, runtimeExperience = null) {
|
|
10187
10339
|
const lang = prefsLang();
|
|
10188
|
-
|
|
10340
|
+
// 연결 모델이 라우트를 최종 판정한다 — 어휘 스코어는 후보 모집/라벨 붙은 폴백 전용.
|
|
10341
|
+
const choice = await resolveAutoRoute(db, prompt, lang);
|
|
10189
10342
|
if (!choice) fail("No agent is available for automatic routing. Check installation with agentlas list.");
|
|
10190
10343
|
if (choice.direct) {
|
|
10191
10344
|
// 전문 에이전트 확신 없음 → 페르소나/능력 라우팅 없이 현재 런타임으로 직답.
|
|
@@ -11982,6 +12135,8 @@ module.exports = {
|
|
|
11982
12135
|
ANTHROPIC_COMPAT_API,
|
|
11983
12136
|
// 자동 라우팅 회귀 테스트 표면 — 약한 매치 직답/오라우팅 방지 규칙 검증용.
|
|
11984
12137
|
autoRouteAgent,
|
|
12138
|
+
// 모델 최종 라우팅 — 어휘 점수는 후보 모집 전용, 연결 모델이 의미로 판정.
|
|
12139
|
+
resolveAutoRoute,
|
|
11985
12140
|
autoRouteNote,
|
|
11986
12141
|
autoRoutePreamble,
|
|
11987
12142
|
directSystemPrompt,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentlas",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.6",
|
|
4
4
|
"description": "Agentlas agent terminal — chat with your installed AI agents and teams from the terminal, Claude Code style. Standalone: no desktop app required.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"agentlas": "bin/agentlas.cjs"
|