agentlas 0.5.5 → 0.6.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/engine/agentlas-composer.cjs +3 -0
- package/engine/agentlas-i18n.cjs +16 -0
- package/engine/agentlas-input.cjs +2 -0
- package/engine/agentlas-repl.cjs +140 -25
- package/engine/agentlas-ui.cjs +5 -1
- package/engine/agentlas.cjs +80 -16
- package/package.json +1 -1
- package/test/route-regression.cjs +121 -0
- package/test/smoke.sh +1 -0
- package/test/terminal-ui-regression.cjs +19 -1
|
@@ -110,6 +110,9 @@ function buildComposerFrame(state, ctx = {}, palette, width = 80) {
|
|
|
110
110
|
const rest = ctx.status && available > 0 ? c.faint(" · " + truncateWidth(ctx.status, available)) : "";
|
|
111
111
|
lines.push(permissionPresentation(ctx, c) + rest);
|
|
112
112
|
}
|
|
113
|
+
// 연결 LLM 세션 사용량 상시 표시줄 — 입력박스 바로 아래에 항상 유지된다.
|
|
114
|
+
const usageText = typeof ctx.usage === "function" ? ctx.usage() : ctx.usage;
|
|
115
|
+
if (usageText) lines.push(c.faint(truncateWidth(String(usageText), w)));
|
|
113
116
|
if (ctx.confirmation) {
|
|
114
117
|
const prefix = ctx.confirmationTone === "danger" ? "! " : "✓ ";
|
|
115
118
|
const paint = ctx.confirmationTone === "danger" && c.paw ? c.paw : c.green || c.emerald || ((value) => value);
|
package/engine/agentlas-i18n.cjs
CHANGED
|
@@ -111,6 +111,14 @@ 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
|
+
"usageBar": "tokens",
|
|
115
|
+
"config.title": "Engine auto engagement — explicit on/off (default: off)",
|
|
116
|
+
"config.storm": "Stormbreaker auto-engage on direct-routed real work",
|
|
117
|
+
"config.network": "hep-network auto-borrow on direct-routed real work",
|
|
118
|
+
"config.usage": "usage: /config storm|network on|off · explicit /storm and /network always work",
|
|
119
|
+
"config.set": "%s → %s",
|
|
120
|
+
"config.autoEngage": "auto-engaging %s (opted in via /config — turn off with /config %s off)",
|
|
121
|
+
"help.config": "explicit on/off for Stormbreaker/hep-network auto engagement",
|
|
114
122
|
"guard.imageWarn": "this agent works with images, but %s can't generate them — try /runtime gemini (or codex), or /team to pin one",
|
|
115
123
|
"multimodal.title": "Multimodal fallback",
|
|
116
124
|
"multimodal.set": "multimodal %s → %s",
|
|
@@ -317,6 +325,14 @@ const STRINGS = {
|
|
|
317
325
|
"team.usage": "사용법: /team · /team <에이전트> <claude-code|codex|gemini|auto>",
|
|
318
326
|
"team.set": "%s → %s",
|
|
319
327
|
"routedImage": "이미지 지원을 위해 %s로 라우팅",
|
|
328
|
+
"usageBar": "토큰",
|
|
329
|
+
"config.title": "엔진 자동 개입 — 명시적 on/off (기본: off)",
|
|
330
|
+
"config.storm": "직답 라우팅된 실작업에 Stormbreaker 자동 개입",
|
|
331
|
+
"config.network": "직답 라우팅된 실작업에 hep-network 자동 빌림",
|
|
332
|
+
"config.usage": "사용법: /config storm|network on|off · /storm·/network 명시 실행은 항상 가능",
|
|
333
|
+
"config.set": "%s → %s",
|
|
334
|
+
"config.autoEngage": "%s 자동 개입 (/config로 켜져 있음 — 해제: /config %s off)",
|
|
335
|
+
"help.config": "Stormbreaker/hep-network 자동 개입 명시적 on/off",
|
|
320
336
|
"guard.imageWarn": "이 에이전트는 이미지 작업인데 %s는 이미지 생성 불가 — /runtime gemini(또는 codex), 또는 /team으로 고정",
|
|
321
337
|
"multimodal.title": "멀티모달 fallback",
|
|
322
338
|
"multimodal.set": "멀티모달 %s → %s",
|
|
@@ -89,6 +89,7 @@ const SLASH_COMMAND_META = [
|
|
|
89
89
|
{ command: "/permission", description: "Set read/write/full permission", category: "Settings", usage: "/permission full", detail: "No argument shows what read, write, and full mean.", aliases: ["/perm"] },
|
|
90
90
|
{ command: "/permissions", description: "Show or set current permission", category: "Settings", usage: "/permissions", detail: "Codex-style permission screen for Agentlas read/write/full." },
|
|
91
91
|
{ command: "/setup", description: "Run first-time setup again", category: "Settings", usage: "/setup", detail: "Re-run language, runtime, and default permission setup in-place." },
|
|
92
|
+
{ command: "/config", description: "Explicit on/off for Stormbreaker and hep-network auto engagement", category: "Settings", usage: "/config [storm|network] [on|off]", detail: "Both engines default to off — no automatic activation. Turn one on to let direct-routed real-work prompts engage it; /storm and /network stay available explicitly either way.", examples: ["/config", "/config storm on", "/config network off"], aliases: ["/toggles"] },
|
|
92
93
|
{ command: "/cwd", description: "Show or change the working folder", category: "Files", usage: "/cwd <path>", detail: "Change the folder used for tools, file mentions, and local commands." },
|
|
93
94
|
{ command: "/memory", description: "Show the memory injected into this run", category: "Context", usage: "/memory", detail: "Print the project memory that Agentlas adds to agent turns." },
|
|
94
95
|
{ command: "/side", description: "Ask a side question without saving it to chat context", category: "Context", usage: "/side <question>", detail: "Runs a one-off answer using current context, then returns without appending to chat history.", aliases: ["/btw"] },
|
|
@@ -137,6 +138,7 @@ const HELP_KEY_BY_COMMAND = {
|
|
|
137
138
|
"/permission": "help.permission",
|
|
138
139
|
"/permissions": "help.permissions",
|
|
139
140
|
"/setup": "help.setup",
|
|
141
|
+
"/config": "help.config",
|
|
140
142
|
"/cwd": "help.cwd",
|
|
141
143
|
"/memory": "help.memory",
|
|
142
144
|
"/side": "help.side",
|
package/engine/agentlas-repl.cjs
CHANGED
|
@@ -301,6 +301,7 @@ function startRepl(opts) {
|
|
|
301
301
|
try {
|
|
302
302
|
ui.beginTurn({
|
|
303
303
|
...composerMeta(),
|
|
304
|
+
usage: () => usageSummaryLine(), // 턴 중에도 최신 사용량을 footer에 라이브 반영
|
|
304
305
|
onInterrupt: () => {
|
|
305
306
|
if (!currentAbort || currentAbort.signal.aborted) return;
|
|
306
307
|
currentAbort.abort();
|
|
@@ -466,6 +467,23 @@ function startRepl(opts) {
|
|
|
466
467
|
state.routePreambleOnce = null;
|
|
467
468
|
applyRuntimeFor(state.subject);
|
|
468
469
|
}
|
|
470
|
+
// 직답 모드 — 전문 에이전트 확신이 없을 때. 페르소나 없음 + 능력(이미지) 라우팅 없음:
|
|
471
|
+
// 런타임은 세션 기본(baseRuntime) 그대로라, 일반 질문이 gemini 등으로 끌려가지 않는다.
|
|
472
|
+
function setSubjectDirect() {
|
|
473
|
+
if (state.subject && state.subject.kind === "direct") return; // 연속 직답 — 세션/히스토리 유지
|
|
474
|
+
state.subject = {
|
|
475
|
+
kind: "direct",
|
|
476
|
+
id: null,
|
|
477
|
+
slug: "agentlas-direct",
|
|
478
|
+
label: ui.lang === "ko" ? "Agentlas 직답" : "Agentlas direct",
|
|
479
|
+
system: H.directSystemPrompt ? H.directSystemPrompt(ui.lang) : "You are the Agentlas terminal's default assistant.",
|
|
480
|
+
capAgent: null,
|
|
481
|
+
};
|
|
482
|
+
state.history = [];
|
|
483
|
+
state.routePreambleOnce = null;
|
|
484
|
+
state.runtime = baseRuntime;
|
|
485
|
+
state.native = {};
|
|
486
|
+
}
|
|
469
487
|
function setSubjectFirm(firm) {
|
|
470
488
|
const sys = H.firmSystemPrompt(db, firm);
|
|
471
489
|
state.subject = {
|
|
@@ -721,6 +739,31 @@ function startRepl(opts) {
|
|
|
721
739
|
case "runtime":
|
|
722
740
|
setRuntime(arg);
|
|
723
741
|
return true;
|
|
742
|
+
case "config":
|
|
743
|
+
case "toggles": {
|
|
744
|
+
// 클로드코드 /config 스타일 — 자동 엔진 개입은 전부 명시적 opt-in (기본 off).
|
|
745
|
+
const ENGINE_FLAGS = [
|
|
746
|
+
{ key: "storm", pref: "autoStorm", label: ui.t("config.storm") },
|
|
747
|
+
{ key: "network", pref: "autoNetwork", label: ui.t("config.network") },
|
|
748
|
+
];
|
|
749
|
+
const showConfig = () => {
|
|
750
|
+
ui.line("");
|
|
751
|
+
ui.rule(ui.t("config.title"));
|
|
752
|
+
for (const f of ENGINE_FLAGS) {
|
|
753
|
+
const on = !!prefs[f.pref];
|
|
754
|
+
ui.line(" " + (on ? ui.c.green("● on ") : ui.c.dim("○ off")) + " " + ui.c.emerald(f.key.padEnd(9)) + ui.c.dim(f.label));
|
|
755
|
+
}
|
|
756
|
+
ui.line(" " + ui.c.faint(ui.t("config.usage")));
|
|
757
|
+
};
|
|
758
|
+
const [cfgKey, cfgVal] = arg.trim().split(/\s+/);
|
|
759
|
+
if (!cfgKey) return showConfig(), true;
|
|
760
|
+
const flag = ENGINE_FLAGS.find((f) => f.key === cfgKey.toLowerCase());
|
|
761
|
+
if (!flag || !/^(on|off)$/i.test(cfgVal || "")) return ui.warn(ui.t("config.usage")), true;
|
|
762
|
+
prefs[flag.pref] = /^on$/i.test(cfgVal);
|
|
763
|
+
if (opts.savePrefs) opts.savePrefs(prefs);
|
|
764
|
+
ui.ok(ui.t("config.set", flag.key, prefs[flag.pref] ? "on" : "off"));
|
|
765
|
+
return showConfig(), true;
|
|
766
|
+
}
|
|
724
767
|
case "storm": {
|
|
725
768
|
if (!arg) return ui.warn("usage: /storm <goal> [--research]"), true;
|
|
726
769
|
let goal = arg;
|
|
@@ -976,8 +1019,13 @@ function startRepl(opts) {
|
|
|
976
1019
|
const s = n >= 1 && n <= list.length ? list[n - 1] : null;
|
|
977
1020
|
if (!s) return ui.warn(ui.t("resume.noNum")), true;
|
|
978
1021
|
const agent = H.resolveAgent(db, s.agentSlug);
|
|
979
|
-
if (
|
|
980
|
-
|
|
1022
|
+
if (agent) {
|
|
1023
|
+
setSubjectAgent(agent);
|
|
1024
|
+
} else if (s.agentSlug === "agentlas-direct") {
|
|
1025
|
+
setSubjectDirect(); // 직답 세션도 이어서 재개
|
|
1026
|
+
} else {
|
|
1027
|
+
return ui.error(ui.t("noAgent", s.agentSlug)), true;
|
|
1028
|
+
}
|
|
981
1029
|
if (s.kind && H.RUNTIME_BIN[s.kind] && H.which(H.RUNTIME_BIN[s.kind])) {
|
|
982
1030
|
state.runtime = { mode: "cli", kind: s.kind };
|
|
983
1031
|
baseRuntime = state.runtime;
|
|
@@ -1097,10 +1145,14 @@ function startRepl(opts) {
|
|
|
1097
1145
|
if (H.autoRouteAgent) {
|
|
1098
1146
|
const choice = H.autoRouteAgent(db, t, ui.lang);
|
|
1099
1147
|
if (choice) {
|
|
1100
|
-
|
|
1148
|
+
if (choice.direct) {
|
|
1149
|
+
setSubjectDirect();
|
|
1150
|
+
} else {
|
|
1151
|
+
setSubjectAgent(choice.agent);
|
|
1152
|
+
}
|
|
1101
1153
|
state.routePreambleOnce = H.autoRoutePreamble ? H.autoRoutePreamble(choice, ui.lang) : null;
|
|
1102
|
-
ui.info(H.autoRouteNote ? H.autoRouteNote(choice, ui.lang) : `auto-routed to ${
|
|
1103
|
-
routingNote(state.subject);
|
|
1154
|
+
ui.info(H.autoRouteNote ? H.autoRouteNote(choice, ui.lang) : `auto-routed to ${state.subject.label}`);
|
|
1155
|
+
if (!choice.direct) routingNote(state.subject);
|
|
1104
1156
|
await runTurn(t);
|
|
1105
1157
|
return ask();
|
|
1106
1158
|
}
|
|
@@ -1120,37 +1172,97 @@ function startRepl(opts) {
|
|
|
1120
1172
|
await handleSlash(t); // /exit 는 내부에서 process.exit
|
|
1121
1173
|
return;
|
|
1122
1174
|
}
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1175
|
+
// 직답 모드는 대상 고정이 아니라 "자동 라우팅 유지" — 매 메시지 재라우팅해,
|
|
1176
|
+
// 나중에 전문 요청이 오면 해당 에이전트로 자연스럽게 넘어간다.
|
|
1177
|
+
const directMode = !!(state.subject && state.subject.kind === "direct");
|
|
1178
|
+
if (!state.subject || directMode) {
|
|
1179
|
+
if (!state.subject) {
|
|
1180
|
+
const ags = H.listAgents(db);
|
|
1181
|
+
const single = !/\s/.test(t);
|
|
1182
|
+
if (/^\d+$/.test(t)) {
|
|
1183
|
+
const n = parseInt(t, 10);
|
|
1184
|
+
if (n >= 1 && n <= ags.length) {
|
|
1185
|
+
setSubjectAgent(ags[n - 1]);
|
|
1186
|
+
ui.ok(ui.t("switched", state.subject.label));
|
|
1187
|
+
routingNote(state.subject);
|
|
1188
|
+
return;
|
|
1189
|
+
}
|
|
1190
|
+
}
|
|
1191
|
+
if (single) {
|
|
1192
|
+
const a = H.resolveAgent(db, t);
|
|
1193
|
+
if (a) { setSubjectAgent(a); ui.ok(ui.t("switched", state.subject.label)); routingNote(state.subject); return; }
|
|
1194
|
+
const f = H.resolveFirm(db, t);
|
|
1195
|
+
if (f) { setSubjectFirm(f); ui.ok(ui.t("switched", state.subject.label)); routingNote(state.subject); return; }
|
|
1133
1196
|
}
|
|
1134
|
-
}
|
|
1135
|
-
if (single) {
|
|
1136
|
-
const a = H.resolveAgent(db, t);
|
|
1137
|
-
if (a) { setSubjectAgent(a); ui.ok(ui.t("switched", state.subject.label)); routingNote(state.subject); return; }
|
|
1138
|
-
const f = H.resolveFirm(db, t);
|
|
1139
|
-
if (f) { setSubjectFirm(f); ui.ok(ui.t("switched", state.subject.label)); routingNote(state.subject); return; }
|
|
1140
1197
|
}
|
|
1141
1198
|
if (H.autoRouteAgent) {
|
|
1142
1199
|
const choice = H.autoRouteAgent(db, t, ui.lang);
|
|
1143
1200
|
if (choice) {
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
|
|
1147
|
-
|
|
1201
|
+
if (choice.direct) {
|
|
1202
|
+
setSubjectDirect();
|
|
1203
|
+
if (!directMode) {
|
|
1204
|
+
// 첫 직답 진입에만 알림 — 연속 직답 대화에서는 조용히.
|
|
1205
|
+
state.routePreambleOnce = H.autoRoutePreamble ? H.autoRoutePreamble(choice, ui.lang) : null;
|
|
1206
|
+
ui.info(H.autoRouteNote ? H.autoRouteNote(choice, ui.lang) : `direct answer (no agent)`);
|
|
1207
|
+
}
|
|
1208
|
+
} else {
|
|
1209
|
+
setSubjectAgent(choice.agent);
|
|
1210
|
+
state.routePreambleOnce = H.autoRoutePreamble ? H.autoRoutePreamble(choice, ui.lang) : null;
|
|
1211
|
+
ui.info(H.autoRouteNote ? H.autoRouteNote(choice, ui.lang) : `auto-routed to ${choice.agent.name}`);
|
|
1212
|
+
routingNote(state.subject);
|
|
1213
|
+
}
|
|
1214
|
+
}
|
|
1215
|
+
}
|
|
1216
|
+
// 자동 엔진 개입은 명시적 opt-in 전용 (/config, 기본 off): direct 판정 + 실작업형 프롬프트일 때만.
|
|
1217
|
+
if (state.subject && state.subject.kind === "direct" && goalLikePrompt(t)) {
|
|
1218
|
+
if (prefs.autoStorm && H.stormRun) {
|
|
1219
|
+
ui.info(ui.t("config.autoEngage", "stormbreaker", "storm"));
|
|
1220
|
+
await H.stormRun(db, t, { ui, cwd: state.cwd, research: false });
|
|
1221
|
+
return;
|
|
1222
|
+
}
|
|
1223
|
+
if (prefs.autoNetwork && H.hepRun) {
|
|
1224
|
+
ui.info(ui.t("config.autoEngage", "hep-network", "network"));
|
|
1225
|
+
ui.line("");
|
|
1226
|
+
await H.hepRun(["hep-network", t, "--project", state.cwd, "--runtime", "terminal"], { cwd: state.cwd });
|
|
1227
|
+
return;
|
|
1148
1228
|
}
|
|
1149
1229
|
}
|
|
1150
1230
|
}
|
|
1151
1231
|
await runTurn(expandMentions(t));
|
|
1152
1232
|
}
|
|
1153
1233
|
|
|
1234
|
+
// 실작업(goal)형 프롬프트 감지 — 질문/잡담에는 자동 엔진(storm/network)을 절대 걸지 않는다.
|
|
1235
|
+
function goalLikePrompt(t) {
|
|
1236
|
+
const s = String(t || "").trim();
|
|
1237
|
+
if (s.length < 12) return false;
|
|
1238
|
+
if (/[??]\s*$/.test(s)) return false;
|
|
1239
|
+
return /(해줘|해라|만들|구현|수정|배포|정리|작성|분석|리팩터|고쳐|추가|빌드|테스트|돌려|실행|자동화|automate|build|implement|fix|refactor|deploy|create|write|run|ship)/i.test(s);
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
// ── 연결 LLM 세션 사용량 요약 (챗 입력창 아래 상시 표시) ──
|
|
1243
|
+
// 호스트 이점: 단일 모델 CLI는 못 보여주는 멀티 LLM 합산 사용량을 항상 노출한다.
|
|
1244
|
+
function fmtTok(n) {
|
|
1245
|
+
if (!n) return "0";
|
|
1246
|
+
if (n >= 1e6) return (n >= 1e7 ? Math.round(n / 1e6) : (n / 1e6).toFixed(1)) + "m";
|
|
1247
|
+
if (n >= 1e3) return (n >= 1e5 ? Math.round(n / 1e3) : (n / 1e3).toFixed(1)) + "k";
|
|
1248
|
+
return String(n);
|
|
1249
|
+
}
|
|
1250
|
+
function usageSummaryLine() {
|
|
1251
|
+
const shortLabel = (label) => (label === "claude-code" ? "claude" : label);
|
|
1252
|
+
const labels = [];
|
|
1253
|
+
const push = (label) => { if (label && label !== "(none)" && !labels.includes(label)) labels.push(label); };
|
|
1254
|
+
for (const kind of installedKinds()) push(kind); // 연결(설치)된 CLI 런타임 — 미사용이어도 표시
|
|
1255
|
+
push(runtimeLabel(state.runtime)); // 현재 런타임 (BYOK/Ollama 포함)
|
|
1256
|
+
for (const label of Object.keys(state.cost)) push(label); // 세션 중 사용한 나머지
|
|
1257
|
+
const parts = labels.map((label) => {
|
|
1258
|
+
const e = state.cost[label];
|
|
1259
|
+
if (!e) return `${shortLabel(label)} 0`;
|
|
1260
|
+
if (e.in || e.out) return `${shortLabel(label)} ${fmtTok(e.in)}→${fmtTok(e.out)}`;
|
|
1261
|
+
return `${shortLabel(label)} ${e.turns}${ui.lang === "ko" ? "턴" : "t"}`;
|
|
1262
|
+
});
|
|
1263
|
+
return `${ui.t("usageBar")} ${parts.join(" · ")}`;
|
|
1264
|
+
}
|
|
1265
|
+
|
|
1154
1266
|
// ── composer (raw-mode bottom box) main loop ──
|
|
1155
1267
|
function composerMeta() {
|
|
1156
1268
|
const rt = runtimeLabel(state.runtime);
|
|
@@ -1162,6 +1274,7 @@ function startRepl(opts) {
|
|
|
1162
1274
|
permission: state.permission,
|
|
1163
1275
|
permissionLabel,
|
|
1164
1276
|
status: `${rt}${eff} · ${subj} · ${ui.t("permCycleHint")} · ${ui.t("composer.hint")} · ↑↓ history`,
|
|
1277
|
+
usage: usageSummaryLine(), // 챗 입력창 아래 상시 LLM 사용량 표시줄
|
|
1165
1278
|
onCyclePermission: () => {
|
|
1166
1279
|
const cycle = permissionCycle.step(state.permission);
|
|
1167
1280
|
if (cycle.armed) {
|
|
@@ -1228,6 +1341,8 @@ function startRepl(opts) {
|
|
|
1228
1341
|
if (closed) return process.exit(0);
|
|
1229
1342
|
if (slashPalette.setEnabled) slashPalette.setEnabled(true);
|
|
1230
1343
|
const cont = buffer != null;
|
|
1344
|
+
// 클래식 모드에도 사용량 상시 표시 (composer 모드에선 입력박스 아래 줄이 담당)
|
|
1345
|
+
if (!cont && !composer && Object.keys(state.cost).length) ui.line(ui.c.faint(" " + usageSummaryLine()));
|
|
1231
1346
|
rl.question(cont ? ui.c.dim(" … ") : "\n" + ui.promptLabel(), async (line) => {
|
|
1232
1347
|
if (input.isContinuation(line)) {
|
|
1233
1348
|
return ask((cont ? buffer + "\n" : "") + input.stripContinuation(line));
|
package/engine/agentlas-ui.cjs
CHANGED
|
@@ -280,7 +280,11 @@ class Ui {
|
|
|
280
280
|
}
|
|
281
281
|
}
|
|
282
282
|
}
|
|
283
|
-
|
|
283
|
+
// 턴 중에도 사용량 표시줄 유지 (chrome.usage: 문자열 또는 라이브 getter)
|
|
284
|
+
const usageSrc = this._turnChrome.usage;
|
|
285
|
+
const usageText = typeof usageSrc === "function" ? usageSrc() : usageSrc;
|
|
286
|
+
const usageLine = usageText ? this.c.faint(truncateCells(String(usageText), width)) : null;
|
|
287
|
+
return [...taskLines, rule, prompt, rule, statusLine, ...(usageLine ? [usageLine] : [])];
|
|
284
288
|
}
|
|
285
289
|
|
|
286
290
|
_eraseFooter() {
|
package/engine/agentlas.cjs
CHANGED
|
@@ -300,7 +300,9 @@ function resolveMetaBuilder(db) {
|
|
|
300
300
|
}
|
|
301
301
|
return null;
|
|
302
302
|
}
|
|
303
|
-
|
|
303
|
+
// "ai"/"llm" 같은 초범용 토큰은 모든 에이전트 프롬프트에 나오므로 판별력이 0이다 —
|
|
304
|
+
// 이런 단어 하나로 전문 에이전트가 선택되던 오라우팅(예: 일반 맥 질문 → Pitch Deck Architect)을 막는다.
|
|
305
|
+
const ROUTE_STOP_WORDS = new Set(["the", "and", "for", "with", "this", "that", "from", "into", "make", "build", "create", "agent", "agents", "please", "ai", "llm", "인공지능", "에이아이", "좀", "해주세요", "해줘", "만들어", "붙여", "연결", "작업", "요청"]);
|
|
304
306
|
const ROUTE_HINTS = [
|
|
305
307
|
{
|
|
306
308
|
slug: "agentlas-app-builder",
|
|
@@ -363,6 +365,10 @@ function routeTokenize(value) {
|
|
|
363
365
|
const expanded = matches.flatMap((term) => term.split("-").filter(Boolean).concat(term));
|
|
364
366
|
return [...new Set(expanded.filter((term) => term.length >= 2 && !ROUTE_STOP_WORDS.has(term)))];
|
|
365
367
|
}
|
|
368
|
+
// 정체성 존(slug/이름/태그라인) — 여기 적중은 강한 라우팅 신호. system_prompt 본문 적중은 약한 신호.
|
|
369
|
+
function routeIdentityHaystack(agent) {
|
|
370
|
+
return routeNormalize([agent.slug, agent.name, agent.name_en, agent.tagline, agent.tagline_en].join("\n"));
|
|
371
|
+
}
|
|
366
372
|
function routeHaystack(agent) {
|
|
367
373
|
return routeNormalize([
|
|
368
374
|
agent.slug,
|
|
@@ -448,6 +454,7 @@ function scoreRouteAgent(prompt, promptTerms, agent, lang) {
|
|
|
448
454
|
terms: [],
|
|
449
455
|
};
|
|
450
456
|
}
|
|
457
|
+
const identityHay = routeIdentityHaystack(agent);
|
|
451
458
|
const haystack = routeHaystack(agent);
|
|
452
459
|
let score = 0;
|
|
453
460
|
const terms = [];
|
|
@@ -460,7 +467,10 @@ function scoreRouteAgent(prompt, promptTerms, agent, lang) {
|
|
|
460
467
|
}
|
|
461
468
|
}
|
|
462
469
|
for (const term of promptTerms) {
|
|
463
|
-
if (
|
|
470
|
+
if (identityHay.includes(term)) {
|
|
471
|
+
score += 6; // 이름/태그라인 적중 = 그 에이전트의 정체성 자체를 부른 것
|
|
472
|
+
terms.push(term);
|
|
473
|
+
} else if (haystack.includes(term)) {
|
|
464
474
|
score += term.length >= 5 ? 3 : 2;
|
|
465
475
|
terms.push(term);
|
|
466
476
|
}
|
|
@@ -478,6 +488,30 @@ function scoreRouteAgent(prompt, promptTerms, agent, lang) {
|
|
|
478
488
|
: "no specialist matched clearly, so the default project coordinator is safest");
|
|
479
489
|
return { agent, score, reason, terms: unique };
|
|
480
490
|
}
|
|
491
|
+
// 라우팅 확신 임계값. 이름/태그라인 적중(+6), 라우트 힌트(+12↑), 이름 포함(+20)만 전문 라우트로 인정하고,
|
|
492
|
+
// system_prompt 본문의 약한 단어 적중(+2~3) 한두 개로는 에이전트를 절대 선택하지 않는다.
|
|
493
|
+
// 임계값 미만이면 "직답"(에이전트·능력 라우팅 없음) — 일반 질문이 Pitch Deck Architect 같은
|
|
494
|
+
// 무관 페르소나 + gemini 이미지 런타임으로 끌려가던 사고의 근본 수리.
|
|
495
|
+
const MIN_ROUTE_SCORE = 6;
|
|
496
|
+
function directRouteChoice(lang) {
|
|
497
|
+
const resolvedLang = lang || prefsLang();
|
|
498
|
+
return {
|
|
499
|
+
direct: true,
|
|
500
|
+
agent: null,
|
|
501
|
+
score: 0,
|
|
502
|
+
terms: [],
|
|
503
|
+
reason: resolvedLang === "ko"
|
|
504
|
+
? "특정 전문 에이전트가 필요 없는 일반 요청입니다"
|
|
505
|
+
: "this is a general request that needs no specialist agent",
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
// 직답 모드 시스템 프롬프트 — 페르소나·라우팅 오염 없이 현재 런타임 그대로 답한다.
|
|
509
|
+
function directSystemPrompt(lang) {
|
|
510
|
+
const resolvedLang = lang || prefsLang();
|
|
511
|
+
return resolvedLang === "ko"
|
|
512
|
+
? "당신은 Agentlas 터미널의 기본 어시스턴트입니다. 특별한 페르소나 없이 사용자의 요청에 정확하고 간결하게 바로 답하세요. 에이전트 라우팅이나 이미지 생성 능력을 스스로 언급하지 마세요."
|
|
513
|
+
: "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.";
|
|
514
|
+
}
|
|
481
515
|
function autoRouteAgent(db, prompt, lang) {
|
|
482
516
|
const resolvedLang = lang || prefsLang();
|
|
483
517
|
// 명확한 "에이전트/팀/회사 만들기" 의도 → 메타-빌더로 직행 (약한 키워드 점수에 밀리지 않게).
|
|
@@ -496,28 +530,40 @@ function autoRouteAgent(db, prompt, lang) {
|
|
|
496
530
|
}
|
|
497
531
|
}
|
|
498
532
|
const agents = listRoutableAgents(db).filter((agent) => !NON_GENERIC_ROUTE_SLUGS.has(agent.slug));
|
|
499
|
-
if (!agents.length) return
|
|
500
|
-
|
|
533
|
+
if (!agents.length) return directRouteChoice(resolvedLang);
|
|
534
|
+
let terms = routeTokenize(prompt);
|
|
535
|
+
// IDF 근사 — 설치 에이전트 절반 이상의 haystack에 나오는 단어("ai","도구" 등)는 판별력이 없어 제외.
|
|
536
|
+
if (agents.length >= 3) {
|
|
537
|
+
const hays = agents.map((agent) => routeHaystack(agent));
|
|
538
|
+
terms = terms.filter((term) => hays.filter((hay) => hay.includes(term)).length * 2 <= agents.length);
|
|
539
|
+
}
|
|
501
540
|
const ranked = agents.map((agent) => scoreRouteAgent(prompt, terms, agent, resolvedLang)).sort((a, b) => b.score - a.score);
|
|
502
|
-
if (ranked[0] && ranked[0].score
|
|
503
|
-
|
|
504
|
-
return {
|
|
505
|
-
agent: fallback,
|
|
506
|
-
score: 0,
|
|
507
|
-
reason: resolvedLang === "ko"
|
|
508
|
-
? "명확한 전문 에이전트가 없어 기본 프로젝트 조율 경로를 선택했습니다"
|
|
509
|
-
: "no specialist matched clearly, so Agentlas chose the default coordination route",
|
|
510
|
-
terms: [],
|
|
511
|
-
};
|
|
541
|
+
if (ranked[0] && ranked[0].score >= MIN_ROUTE_SCORE) return ranked[0];
|
|
542
|
+
return directRouteChoice(resolvedLang);
|
|
512
543
|
}
|
|
513
544
|
function autoRouteNote(choice, lang) {
|
|
514
|
-
const
|
|
515
|
-
|
|
545
|
+
const resolvedLang = lang || prefsLang();
|
|
546
|
+
if (choice.direct) {
|
|
547
|
+
return resolvedLang === "ko"
|
|
548
|
+
? `사용 에이전트: 없음 — 바로 답합니다. 이유: ${choice.reason}.`
|
|
549
|
+
: `Selected agent: none — answering directly. Reason: ${choice.reason}.`;
|
|
550
|
+
}
|
|
551
|
+
const name = resolvedLang === "ko" ? choice.agent.name : choice.agent.name_en || choice.agent.name;
|
|
552
|
+
return resolvedLang === "ko"
|
|
516
553
|
? `사용 에이전트: ${name}. 이유: ${choice.reason}.`
|
|
517
554
|
: `Selected agent: ${name}. Reason: ${choice.reason}.`;
|
|
518
555
|
}
|
|
519
556
|
function autoRoutePreamble(choice, lang) {
|
|
520
557
|
const resolvedLang = lang || prefsLang();
|
|
558
|
+
if (choice.direct) {
|
|
559
|
+
return [
|
|
560
|
+
"## Agentlas direct answer",
|
|
561
|
+
"",
|
|
562
|
+
resolvedLang === "ko"
|
|
563
|
+
? "이 요청은 전문 에이전트 라우팅 없이 처리합니다. 라우팅이나 에이전트를 언급하지 말고 사용자 요청에 바로 답하세요."
|
|
564
|
+
: "This request is handled without specialist routing. Answer the user directly, without mentioning routing or agents.",
|
|
565
|
+
].join("\n");
|
|
566
|
+
}
|
|
521
567
|
const appBuilderNeedsConsent = choice.agent && choice.agent.slug === "agentlas-app-builder";
|
|
522
568
|
const instruction = appBuilderNeedsConsent
|
|
523
569
|
? resolvedLang === "ko"
|
|
@@ -8117,6 +8163,7 @@ function buildHelpers(db) {
|
|
|
8117
8163
|
autoRouteAgent: (db_, prompt, lang) => autoRouteAgent(db_, prompt, lang),
|
|
8118
8164
|
autoRouteNote: (choice, lang) => autoRouteNote(choice, lang),
|
|
8119
8165
|
autoRoutePreamble: (choice, lang) => autoRoutePreamble(choice, lang),
|
|
8166
|
+
directSystemPrompt: (lang) => directSystemPrompt(lang),
|
|
8120
8167
|
cliMemoryContext: (db_, pp) => cliMemoryContext(db_, pp),
|
|
8121
8168
|
importLocal: (db_, p) => importLocalFolderCli(db_, p),
|
|
8122
8169
|
// REPL-safe public Hub install: fail()(process.exit) 대신 Error를 throw 해 REPL이 직접 렌더하게 한다.
|
|
@@ -8536,6 +8583,18 @@ async function cmdAutoRun(db, prompt, runtimeOverride) {
|
|
|
8536
8583
|
const lang = prefsLang();
|
|
8537
8584
|
const choice = autoRouteAgent(db, prompt, lang);
|
|
8538
8585
|
if (!choice) fail("자동 라우팅할 에이전트가 없습니다. agentlas list로 설치 상태를 확인하세요.");
|
|
8586
|
+
if (choice.direct) {
|
|
8587
|
+
// 전문 에이전트 확신 없음 → 페르소나/능력 라우팅 없이 현재 런타임으로 직답.
|
|
8588
|
+
process.stderr.write(`▸ direct (no agent)\n`);
|
|
8589
|
+
process.stderr.write(` ${autoRouteNote(choice, lang)}\n`);
|
|
8590
|
+
const sys = `${autoRoutePreamble(choice, lang)}\n\n${directSystemPrompt(lang)}`;
|
|
8591
|
+
const code = await executeOnce(db, sys, prompt.trim(), runtimeOverride, {
|
|
8592
|
+
projectPath: activeProjectPath(db),
|
|
8593
|
+
agentId: null,
|
|
8594
|
+
permission: PERMISSION,
|
|
8595
|
+
});
|
|
8596
|
+
process.exit(code);
|
|
8597
|
+
}
|
|
8539
8598
|
process.stderr.write(`▸ ${choice.agent.name} (auto)\n`);
|
|
8540
8599
|
process.stderr.write(` ${autoRouteNote(choice, lang)}\n`);
|
|
8541
8600
|
const sys = `${autoRoutePreamble(choice, lang)}\n\n${agentSystemPromptCli(choice.agent)}`;
|
|
@@ -10195,4 +10254,9 @@ module.exports = {
|
|
|
10195
10254
|
cloudPortableExecutableForFile,
|
|
10196
10255
|
DEFAULT_API_MODEL,
|
|
10197
10256
|
ANTHROPIC_COMPAT_API,
|
|
10257
|
+
// 자동 라우팅 회귀 테스트 표면 — 약한 매치 직답/오라우팅 방지 규칙 검증용.
|
|
10258
|
+
autoRouteAgent,
|
|
10259
|
+
autoRouteNote,
|
|
10260
|
+
autoRoutePreamble,
|
|
10261
|
+
directSystemPrompt,
|
|
10198
10262
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentlas",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
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"
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
/*
|
|
4
|
+
* 자동 라우팅 회귀 테스트 — 2026-07-12 오라우팅 사고 고정.
|
|
5
|
+
*
|
|
6
|
+
* 사고: 일반 맥 질문("맥이 잠금상태에서 자꾸 ai 돌릴때 안켜지게 하는법 없나")이
|
|
7
|
+
* "ai" 단어 하나(+2점)로 Pitch Deck Architect에 라우팅되고, 그 에이전트의 이미지
|
|
8
|
+
* 힌트 때문에 런타임까지 gemini로 끌려갔다.
|
|
9
|
+
* 수리: 초범용 토큰 스톱워드 + IDF 근사 필터 + 정체성 가중치 + 확신 임계값(MIN_ROUTE_SCORE)
|
|
10
|
+
* 미만이면 direct(에이전트·능력 라우팅 없음) 판정.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const assert = require("node:assert/strict");
|
|
14
|
+
const { autoRouteAgent, autoRouteNote, autoRoutePreamble, directSystemPrompt } = require("../engine/agentlas.cjs");
|
|
15
|
+
|
|
16
|
+
// 실제 설치 상태를 흉내 낸 스텁 DB — 모든 에이전트 프롬프트에 "AI"가 들어 있다(현실과 동일).
|
|
17
|
+
const AGENTS = [
|
|
18
|
+
{
|
|
19
|
+
id: "a1",
|
|
20
|
+
slug: "pitch-deck-architect",
|
|
21
|
+
name: "Pitch Deck Architect",
|
|
22
|
+
name_en: "Pitch Deck Architect",
|
|
23
|
+
tagline: "투자 유치용 피치덱 설계",
|
|
24
|
+
tagline_en: "Investor pitch deck design",
|
|
25
|
+
system_prompt: "You are an AI pitch deck architect. Design slides, visuals, images, 디자인, storytelling for investors.",
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
id: "a2",
|
|
29
|
+
slug: "thumbnail-studio",
|
|
30
|
+
name: "썸네일 스튜디오",
|
|
31
|
+
name_en: "Thumbnail Studio",
|
|
32
|
+
tagline: "유튜브 썸네일 디자인",
|
|
33
|
+
tagline_en: "YouTube thumbnail design",
|
|
34
|
+
system_prompt: "You are an AI thumbnail designer. Generate 썸네일 images with bold typography.",
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
id: "a3",
|
|
38
|
+
slug: "agentlas-pm-soul",
|
|
39
|
+
name: "PM Soul",
|
|
40
|
+
name_en: "PM Soul",
|
|
41
|
+
tagline: "프로젝트 연속성 관리",
|
|
42
|
+
tagline_en: "Project continuity",
|
|
43
|
+
system_prompt: "You are an AI project manager. Track decisions, plans, handoffs.",
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: "a4",
|
|
47
|
+
slug: "agentlas-memory-curator",
|
|
48
|
+
name: "Memory Curator",
|
|
49
|
+
name_en: "Memory Curator",
|
|
50
|
+
tagline: "기억 저장/회상 관리",
|
|
51
|
+
tagline_en: "Memory curation",
|
|
52
|
+
system_prompt: "You are an AI memory curator. Store and recall durable memory entries.",
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
const META = [
|
|
56
|
+
{
|
|
57
|
+
id: "m1",
|
|
58
|
+
slug: "agentlas-meta-agent",
|
|
59
|
+
name: "메타에이전트",
|
|
60
|
+
name_en: "Meta Agent",
|
|
61
|
+
tagline: "에이전트/팀 빌더",
|
|
62
|
+
tagline_en: "Agent/team builder",
|
|
63
|
+
system_prompt: "You build new agents and teams.",
|
|
64
|
+
},
|
|
65
|
+
];
|
|
66
|
+
const db = {
|
|
67
|
+
prepare(sql) {
|
|
68
|
+
if (/WHERE slug IN/.test(sql)) return { all: () => META };
|
|
69
|
+
return { all: () => AGENTS };
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
// 1) 사고 재현 프롬프트 → direct (어떤 에이전트도, 특히 Pitch Deck Architect도 선택 금지)
|
|
74
|
+
{
|
|
75
|
+
const choice = autoRouteAgent(db, "맥이 잠금상태에서 자꾸 ai 돌릴때 안켜지게 하는법 없나", "ko");
|
|
76
|
+
assert.equal(choice.direct, true, `일반 질문은 direct여야 함 — 실제: ${JSON.stringify(choice.agent && choice.agent.slug)}`);
|
|
77
|
+
assert.equal(choice.agent, null);
|
|
78
|
+
assert.match(autoRouteNote(choice, "ko"), /사용 에이전트: 없음/);
|
|
79
|
+
assert.match(autoRouteNote(choice, "en"), /Selected agent: none/);
|
|
80
|
+
assert.match(autoRoutePreamble(choice, "ko"), /direct answer/i);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// 2) "ai"/"llm" 단독 언급의 영어 일반 질문도 direct
|
|
84
|
+
{
|
|
85
|
+
const choice = autoRouteAgent(db, "how do I keep my mac from sleeping while ai jobs run", "en");
|
|
86
|
+
assert.equal(choice.direct, true, "영어 일반 질문도 direct여야 함");
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// 3) 정체성(이름/태그라인) 적중은 여전히 전문 라우트 — 썸네일 요청은 썸네일 에이전트로
|
|
90
|
+
{
|
|
91
|
+
const choice = autoRouteAgent(db, "유튜브 썸네일 하나 뽑아줘", "ko");
|
|
92
|
+
assert.equal(choice.direct, undefined, "썸네일 요청이 direct로 새면 안 됨");
|
|
93
|
+
assert.equal(choice.agent.slug, "thumbnail-studio");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// 4) 에이전트 이름을 직접 부르면 그 에이전트로
|
|
97
|
+
{
|
|
98
|
+
const choice = autoRouteAgent(db, "pitch deck 초안 잡아줘", "ko");
|
|
99
|
+
assert.equal(choice.direct, undefined);
|
|
100
|
+
assert.equal(choice.agent.slug, "pitch-deck-architect");
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// 5) 빌드 의도는 메타빌더 직행 (기존 동작 유지)
|
|
104
|
+
{
|
|
105
|
+
const choice = autoRouteAgent(db, "인스타 카드뉴스 에이전트 하나 만들어줘", "ko");
|
|
106
|
+
assert.equal(choice.agent.slug, "agentlas-meta-agent");
|
|
107
|
+
assert.equal(choice.score, 1000);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// 6) 설치 에이전트가 없어도 direct로 답한다 (픽커 오류 대신)
|
|
111
|
+
{
|
|
112
|
+
const empty = { prepare: (sql) => ({ all: () => (/WHERE slug IN/.test(sql) ? [] : []) }) };
|
|
113
|
+
const choice = autoRouteAgent(empty, "안녕 오늘 날씨 어때", "ko");
|
|
114
|
+
assert.equal(choice.direct, true);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// 7) 직답 시스템 프롬프트 — 페르소나 없음, 양 언어 모두 존재
|
|
118
|
+
assert.match(directSystemPrompt("ko"), /기본 어시스턴트/);
|
|
119
|
+
assert.match(directSystemPrompt("en"), /default assistant/);
|
|
120
|
+
|
|
121
|
+
console.log("route-regression: PASS");
|
package/test/smoke.sh
CHANGED
|
@@ -43,6 +43,7 @@ check "bootstrap-race" node "$SCRIPT_DIR/bootstrap-race.cjs"
|
|
|
43
43
|
check "login-loopback-security" node "$SCRIPT_DIR/login-loopback-security.cjs"
|
|
44
44
|
check "timeout-regression" node "$SCRIPT_DIR/timeout-regression.cjs"
|
|
45
45
|
check "terminal-ui-regression" node "$SCRIPT_DIR/terminal-ui-regression.cjs"
|
|
46
|
+
check "route-regression" node "$SCRIPT_DIR/route-regression.cjs"
|
|
46
47
|
check "permission-mapping" node "$SCRIPT_DIR/permission-mapping.cjs"
|
|
47
48
|
check "sqlite-driver-probe" node "$SCRIPT_DIR/sqlite-driver-probe.cjs"
|
|
48
49
|
check "capture-runtime-guard" node "$SCRIPT_DIR/capture-runtime-guard.cjs"
|
|
@@ -95,6 +95,23 @@ function testComposerHierarchy() {
|
|
|
95
95
|
confirmationTone: "danger",
|
|
96
96
|
}, palette(), 48);
|
|
97
97
|
for (const line of warning.lines) assert.ok(visWidth(line) <= 48, "permission warning must not wrap and corrupt redraw rows");
|
|
98
|
+
|
|
99
|
+
// 연결 LLM 사용량 표시줄 — ctx.usage가 있으면 상태줄 아래 한 줄로 항상 렌더된다.
|
|
100
|
+
const withUsage = buildComposerFrame({ buf: "", cur: 0, scroll: 0, suggest: [], suggestSel: 0 }, {
|
|
101
|
+
lang: "ko",
|
|
102
|
+
permission: "write",
|
|
103
|
+
permissionLabel: "읽기 + 쓰기",
|
|
104
|
+
status: "claude-code · 자동 라우팅",
|
|
105
|
+
usage: "토큰 claude 12.3k→4.5k · codex 0 · gemini 0",
|
|
106
|
+
}, palette(), 76);
|
|
107
|
+
assert.equal(withUsage.lines.length, 5, "usage bar must add exactly one line under the status line");
|
|
108
|
+
assert.match(withUsage.lines[4], /토큰 {2}claude 12\.3k→4\.5k · codex 0 · gemini 0/);
|
|
109
|
+
for (const line of withUsage.lines) assert.ok(visWidth(line) <= 76, "usage bar must never overflow the box width");
|
|
110
|
+
const usageFn = buildComposerFrame({ buf: "", cur: 0, scroll: 0, suggest: [], suggestSel: 0 }, {
|
|
111
|
+
permission: "write",
|
|
112
|
+
usage: () => "tokens claude 1.0k→200",
|
|
113
|
+
}, palette(), 76);
|
|
114
|
+
assert.match(usageFn.lines[usageFn.lines.length - 1], /tokens {2}claude 1\.0k→200/, "usage may be a live getter");
|
|
98
115
|
}
|
|
99
116
|
|
|
100
117
|
function testCompactLocalizedStartup() {
|
|
@@ -170,7 +187,7 @@ function testCompactToolActivity() {
|
|
|
170
187
|
function testPersistentTurnFooter() {
|
|
171
188
|
const stream = captureStream({ tty: true, columns: 84 });
|
|
172
189
|
const ui = new Ui({ color: false, lang: "ko", stream });
|
|
173
|
-
ui.beginTurn({ permission: "write", permissionLabel: "읽기 + 쓰기", status: "codex · 자동 라우팅" });
|
|
190
|
+
ui.beginTurn({ permission: "write", permissionLabel: "읽기 + 쓰기", status: "codex · 자동 라우팅", usage: () => "토큰 codex 2.0k→800" });
|
|
174
191
|
ui.status("Codex로 생각 중");
|
|
175
192
|
ui.tool("Read", "/Users/mason/Documents/Agentlas_F/README.md");
|
|
176
193
|
ui.toolResult("first\nsecond\nthird", true);
|
|
@@ -193,6 +210,7 @@ function testPersistentTurnFooter() {
|
|
|
193
210
|
assert.match(output, /읽기 \+ 쓰기/);
|
|
194
211
|
assert.match(output, /● Read/);
|
|
195
212
|
assert.match(output, /└ ✓ 3 lines read/);
|
|
213
|
+
assert.match(output, /토큰 {2}codex 2\.0k→800/, "usage bar must stay visible in the persistent turn footer");
|
|
196
214
|
}
|
|
197
215
|
|
|
198
216
|
function testRuntimeTaskPanelAndCtrlT() {
|