@noobdemon/noob-cli 1.12.14 → 1.12.16
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 +6 -0
- package/package.json +1 -1
- package/src/agent.js +24 -2
- package/src/api.js +7 -3
- package/src/diff.js +13 -4
- package/src/i18n.js +2 -1
- package/src/repl/agent-dispatch.js +30 -21
- package/src/repl/complete.js +1 -0
- package/src/repl/permission.js +1 -2
- package/src/repl/state.js +1 -0
- package/src/repl/stream-printer.js +2 -11
- package/src/repl/workflow-commands.js +3 -1
- package/src/repl.js +220 -56
- package/src/tokens.js +13 -6
- package/src/tools.js +47 -8
- package/src/tui.js +173 -22
- package/src/ui.js +8 -5
- package/src/workflow-bg.js +33 -5
- package/src/workflow-runs.js +4 -1
package/src/repl.js
CHANGED
|
@@ -5,8 +5,22 @@ import chalk from 'chalk';
|
|
|
5
5
|
import { createTui } from './tui.js';
|
|
6
6
|
import { runAgent, maybeSummarize, buildSystem, buildUserMessage } from './agent.js';
|
|
7
7
|
import { spawnAgentToolsDoc, MAX_SUBAGENT_DEPTH } from './subagent.js';
|
|
8
|
-
import {
|
|
9
|
-
|
|
8
|
+
import {
|
|
9
|
+
TokenMeter,
|
|
10
|
+
countMessages,
|
|
11
|
+
CONTEXT_WINDOW,
|
|
12
|
+
SEND_LIMIT_TOKENS,
|
|
13
|
+
countTokens,
|
|
14
|
+
} from './tokens.js';
|
|
15
|
+
import {
|
|
16
|
+
stream,
|
|
17
|
+
usage,
|
|
18
|
+
cachedUsage,
|
|
19
|
+
resetUsageCache,
|
|
20
|
+
evaluateQuotaWarning,
|
|
21
|
+
ApiError,
|
|
22
|
+
resetMemoryToken,
|
|
23
|
+
} from './api.js';
|
|
10
24
|
import {
|
|
11
25
|
runTool,
|
|
12
26
|
describe,
|
|
@@ -26,10 +40,7 @@ import {
|
|
|
26
40
|
askAddRoot as _askAddRoot,
|
|
27
41
|
askWorkflowAgentMode as _askWorkflowAgentMode,
|
|
28
42
|
} from './repl/permission.js';
|
|
29
|
-
import {
|
|
30
|
-
runImprove as _runImprove,
|
|
31
|
-
runKarpathy as _runKarpathy,
|
|
32
|
-
} from './repl/commands/prompts.js';
|
|
43
|
+
import { runImprove as _runImprove, runKarpathy as _runKarpathy } from './repl/commands/prompts.js';
|
|
33
44
|
import { config } from './config.js';
|
|
34
45
|
import { loadMemory, memoryPath, memoryStats } from './memory.js';
|
|
35
46
|
import { t } from './i18n.js';
|
|
@@ -79,7 +90,14 @@ import {
|
|
|
79
90
|
} from './repl/workflow-commands.js';
|
|
80
91
|
import { createState } from './repl/state.js';
|
|
81
92
|
import {
|
|
82
|
-
shortCwd,
|
|
93
|
+
shortCwd,
|
|
94
|
+
shortPath,
|
|
95
|
+
relTime,
|
|
96
|
+
firstLine,
|
|
97
|
+
truncate,
|
|
98
|
+
fmtTime,
|
|
99
|
+
fmtK,
|
|
100
|
+
preview,
|
|
83
101
|
} from './repl/utils.js';
|
|
84
102
|
import { createAgentDispatcher } from './repl/agent-dispatch.js';
|
|
85
103
|
import { createBgRegistry } from './workflow-bg.js';
|
|
@@ -142,6 +160,14 @@ export async function startRepl(opts = {}) {
|
|
|
142
160
|
tui.print(state.yolo ? c.err(' ' + t.yoloOn) : c.ok(' ' + t.yoloOff));
|
|
143
161
|
tui.setPrompt(promptStr(false));
|
|
144
162
|
},
|
|
163
|
+
onCtrlT: () => {
|
|
164
|
+
// Cycle build → plan → compose. UI hint mode (chỉ system-prompt hint).
|
|
165
|
+
const order = ['build', 'plan', 'compose'];
|
|
166
|
+
const next = order[(order.indexOf(state.agentUiMode) + 1) % order.length];
|
|
167
|
+
state.agentUiMode = next;
|
|
168
|
+
tui.setAgentMode(next);
|
|
169
|
+
tui.print(c.accent(' ◆ mode: ') + next);
|
|
170
|
+
},
|
|
145
171
|
completer: completeInput,
|
|
146
172
|
});
|
|
147
173
|
|
|
@@ -165,8 +191,7 @@ export async function startRepl(opts = {}) {
|
|
|
165
191
|
_askPermission(name, { tui, ask, pending, c, t, truncate, targetPath });
|
|
166
192
|
const askAddRoot = (root, targetPath) =>
|
|
167
193
|
_askAddRoot(root, targetPath, { tui, ask, pending, c, t, truncate });
|
|
168
|
-
const askWorkflowAgentMode = () =>
|
|
169
|
-
_askWorkflowAgentMode({ tui, ask, pending, c, t, truncate });
|
|
194
|
+
const askWorkflowAgentMode = () => _askWorkflowAgentMode({ tui, ask, pending, c, t, truncate });
|
|
170
195
|
|
|
171
196
|
// ── quota soft-cap guard ───────────────────────────────────────────
|
|
172
197
|
// Trước khi start ULTRA / loop / workflow (3 lệnh ăn nhiều request nhất),
|
|
@@ -232,6 +257,7 @@ export async function startRepl(opts = {}) {
|
|
|
232
257
|
abort = null;
|
|
233
258
|
if (state.ultra) {
|
|
234
259
|
state.ultra = false; // Ctrl+C cũng dừng vòng tự hành, không chỉ lượt hiện tại
|
|
260
|
+
tui.setUltra(false); // tắt badge ULTRA trên status bar
|
|
235
261
|
console.log(c.tool(' ' + t.ultraStopped));
|
|
236
262
|
}
|
|
237
263
|
if (pending.length) {
|
|
@@ -256,7 +282,14 @@ export async function startRepl(opts = {}) {
|
|
|
256
282
|
// Bg workflow đang chạy → abort + mark interrupted (journal resume-able).
|
|
257
283
|
const swept = bgRegistry.sweepOnExit();
|
|
258
284
|
if (swept > 0)
|
|
259
|
-
console.log(
|
|
285
|
+
console.log(
|
|
286
|
+
c.dim(
|
|
287
|
+
' ' +
|
|
288
|
+
(t.bgWorkflowSweep
|
|
289
|
+
? t.bgWorkflowSweep(swept)
|
|
290
|
+
: `${swept} workflow nền bị ngắt — resume bằng /workflow resume <id>`)
|
|
291
|
+
)
|
|
292
|
+
);
|
|
260
293
|
persist();
|
|
261
294
|
console.log(c.dim('\n ' + t.bye));
|
|
262
295
|
tui.close(); // khôi phục terminal (raw mode/paste/stdout) trước khi thoát
|
|
@@ -313,6 +346,7 @@ export async function startRepl(opts = {}) {
|
|
|
313
346
|
state.history = s.history || [];
|
|
314
347
|
state.mode = 'chat';
|
|
315
348
|
state.goal = s.goal || null; // khôi phục HARD GOAL nếu phiên cũ có
|
|
349
|
+
tui.setGoal(state.goal || '');
|
|
316
350
|
if (s.model) {
|
|
317
351
|
const m = findModel(s.model);
|
|
318
352
|
if (m) state.model = m;
|
|
@@ -608,19 +642,29 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
|
|
|
608
642
|
const active = bgRegistry.activeRuns();
|
|
609
643
|
if (!active.length) return console.log(c.dim(' không có workflow nền nào đang chạy.'));
|
|
610
644
|
console.log(c.err(' cần id: /workflow stop <id> | all'));
|
|
611
|
-
for (const r of active)
|
|
645
|
+
for (const r of active)
|
|
646
|
+
console.log(' ' + c.accent(r.id) + (r.name ? c.dim(' · ' + r.name) : ''));
|
|
612
647
|
return;
|
|
613
648
|
}
|
|
614
649
|
if (id === 'all') {
|
|
615
650
|
const n = bgRegistry.sweepOnExit();
|
|
616
651
|
return console.log(
|
|
617
|
-
c.tool(
|
|
652
|
+
c.tool(
|
|
653
|
+
' 🎼 ' +
|
|
654
|
+
(n
|
|
655
|
+
? `đã huỷ ${n} workflow nền — resume bằng /workflow resume <id>`
|
|
656
|
+
: 'không có workflow nền nào đang chạy.')
|
|
657
|
+
)
|
|
618
658
|
);
|
|
619
659
|
}
|
|
620
660
|
const ok = bgRegistry.stopBg(id);
|
|
621
661
|
console.log(
|
|
622
662
|
ok
|
|
623
|
-
? c.tool(
|
|
663
|
+
? c.tool(
|
|
664
|
+
' 🎼 đã huỷ workflow nền ' +
|
|
665
|
+
c.accent(id) +
|
|
666
|
+
c.dim(' — resume bằng /workflow resume ' + id)
|
|
667
|
+
)
|
|
624
668
|
: c.err(' không tìm thấy workflow nền đang chạy với id: ' + id)
|
|
625
669
|
);
|
|
626
670
|
}
|
|
@@ -633,7 +677,9 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
|
|
|
633
677
|
const now = Date.now();
|
|
634
678
|
for (const r of active) {
|
|
635
679
|
const secs = Math.round((now - r.startedAt) / 1000);
|
|
636
|
-
console.log(
|
|
680
|
+
console.log(
|
|
681
|
+
' ' + c.accent(r.id) + (r.name ? c.dim(' · ' + r.name) : '') + c.dim(` · ${secs}s`)
|
|
682
|
+
);
|
|
637
683
|
}
|
|
638
684
|
}
|
|
639
685
|
|
|
@@ -802,16 +848,23 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
|
|
|
802
848
|
const items = listWorkflowRuns(20);
|
|
803
849
|
if (!items.length) {
|
|
804
850
|
console.log(c.dim(' (chưa có workflow run nào trong workspace này)'));
|
|
805
|
-
console.log(
|
|
851
|
+
console.log(
|
|
852
|
+
c.dim(' Run đầu tiên được tạo khi bạn /workflow <yêu cầu> hoặc /workflow run <name>.')
|
|
853
|
+
);
|
|
806
854
|
return;
|
|
807
855
|
}
|
|
808
|
-
console.log(
|
|
856
|
+
console.log(
|
|
857
|
+
'\n' + chalk.bold(' ' + (t.workflowRunsTitle || '📓 Workflow runs (workspace này)'))
|
|
858
|
+
);
|
|
809
859
|
for (const r of items) {
|
|
810
860
|
const statusColor =
|
|
811
|
-
r.status === 'done'
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
861
|
+
r.status === 'done'
|
|
862
|
+
? c.ok
|
|
863
|
+
: r.status === 'interrupted'
|
|
864
|
+
? c.tool
|
|
865
|
+
: r.status === 'failed'
|
|
866
|
+
? c.err
|
|
867
|
+
: c.accent;
|
|
815
868
|
console.log(
|
|
816
869
|
c.dim(' ') +
|
|
817
870
|
c.accent(r.id.padEnd(40)) +
|
|
@@ -820,18 +873,25 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
|
|
|
820
873
|
);
|
|
821
874
|
}
|
|
822
875
|
console.log(
|
|
823
|
-
c.dim(
|
|
876
|
+
c.dim(
|
|
877
|
+
'\n /workflow log <id> xem chi tiết\n /workflow resume <id> chạy lại, skip task đã done\n'
|
|
878
|
+
)
|
|
824
879
|
);
|
|
825
880
|
}
|
|
826
881
|
|
|
827
882
|
// /workflow log <id> — xem chi tiết 1 run: prompt, tasks, kết quả mỗi sub-agent.
|
|
828
883
|
function workflowLog(arg) {
|
|
829
|
-
if (!arg)
|
|
884
|
+
if (!arg)
|
|
885
|
+
return console.log(c.err(' Cách dùng: /workflow log <id> (xem id bằng /workflow runs)'));
|
|
830
886
|
const r = loadWorkflowRun(arg);
|
|
831
887
|
if (!r.ok) return console.log(c.err(' Không tìm thấy run: ' + arg));
|
|
832
888
|
const d = r.data;
|
|
833
889
|
console.log('\n' + chalk.bold(' 📓 ' + d.id));
|
|
834
|
-
console.log(
|
|
890
|
+
console.log(
|
|
891
|
+
c.dim(
|
|
892
|
+
` name: ${d.name || '(adhoc)'} · status: ${d.status} · started: ${relTime(d.startedAt)} · updated: ${relTime(d.updatedAt)}`
|
|
893
|
+
)
|
|
894
|
+
);
|
|
835
895
|
console.log(c.dim(' workflow prompt: ' + truncate(d.workflowPrompt || '', 120)));
|
|
836
896
|
if (!d.tasks?.length) {
|
|
837
897
|
console.log(c.dim(' (chưa có sub-agent task nào được ghi nhận)'));
|
|
@@ -839,10 +899,7 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
|
|
|
839
899
|
}
|
|
840
900
|
console.log('\n' + c.dim(' ── sub-agent tasks ──'));
|
|
841
901
|
d.tasks.forEach((tk, i) => {
|
|
842
|
-
const statusColor =
|
|
843
|
-
tk.status === 'done' ? c.ok :
|
|
844
|
-
tk.status === 'failed' ? c.err :
|
|
845
|
-
c.tool;
|
|
902
|
+
const statusColor = tk.status === 'done' ? c.ok : tk.status === 'failed' ? c.err : c.tool;
|
|
846
903
|
console.log(
|
|
847
904
|
c.dim(` #${i + 1} `) +
|
|
848
905
|
statusColor(tk.status.padEnd(8)) +
|
|
@@ -851,7 +908,8 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
|
|
|
851
908
|
);
|
|
852
909
|
if (tk.context) console.log(c.dim(' context: ' + truncate(tk.context, 80)));
|
|
853
910
|
if (tk.model) console.log(c.dim(' model: ' + tk.model));
|
|
854
|
-
if (tk.result)
|
|
911
|
+
if (tk.result)
|
|
912
|
+
console.log(c.dim(' result: ' + truncate(tk.result.replace(/\s+/g, ' '), 120)));
|
|
855
913
|
if (tk.error) console.log(c.err(' error: ' + truncate(tk.error, 120)));
|
|
856
914
|
});
|
|
857
915
|
console.log('');
|
|
@@ -860,13 +918,19 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
|
|
|
860
918
|
// /workflow resume <id> — chạy lại workflow với cùng prompt; dispatchTool tự
|
|
861
919
|
// hit cache trong journal cho mọi task đã done lần trước → tiết kiệm token.
|
|
862
920
|
async function workflowResume(arg) {
|
|
863
|
-
if (!arg)
|
|
921
|
+
if (!arg)
|
|
922
|
+
return console.log(c.err(' Cách dùng: /workflow resume <id> (xem id bằng /workflow runs)'));
|
|
864
923
|
const r = loadWorkflowRun(arg);
|
|
865
924
|
if (!r.ok) return console.log(c.err(' Không tìm thấy run: ' + arg));
|
|
866
925
|
const d = r.data;
|
|
867
|
-
if (!d.workflowPrompt)
|
|
926
|
+
if (!d.workflowPrompt)
|
|
927
|
+
return console.log(c.err(' Run này không có workflow prompt — không thể resume.'));
|
|
868
928
|
if (d.status === 'done') {
|
|
869
|
-
console.log(
|
|
929
|
+
console.log(
|
|
930
|
+
c.tool(
|
|
931
|
+
' ⚠ run đã ở trạng thái DONE — vẫn resume nhưng có thể chạy lại task mới nếu model spawn khác.'
|
|
932
|
+
)
|
|
933
|
+
);
|
|
870
934
|
}
|
|
871
935
|
await workflowExecute(d.workflowPrompt, { resumeRun: d });
|
|
872
936
|
}
|
|
@@ -884,6 +948,7 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
|
|
|
884
948
|
if (!(await checkQuotaBeforeHeavy('/ultra'))) return;
|
|
885
949
|
state.mode = 'chat'; // tự hành chỉ chạy ở chế độ agent
|
|
886
950
|
state.ultra = true;
|
|
951
|
+
tui.setUltra(true); // badge 🚀 ULTRA trên status bar
|
|
887
952
|
console.log(c.accent(' 🚀 ' + t.ultraOn));
|
|
888
953
|
// Mốc history TRƯỚC khi ULTRA bơm prompt — kết thúc thì cắt về để các lượt sau không bị "dính" mục tiêu cũ.
|
|
889
954
|
const baseLen = state.history.length;
|
|
@@ -929,6 +994,7 @@ Thực thi THEO ĐÚNG THỨ TỰ (BẮT BUỘC):
|
|
|
929
994
|
// token <<ULTRA_DONE>>…) để các yêu cầu SAU đó không bị model coi như vẫn
|
|
930
995
|
// đang tự hành / vẫn theo đuổi mục tiêu cũ.
|
|
931
996
|
state.ultra = false;
|
|
997
|
+
tui.setUltra(false); // tắt badge khi thoát ULTRA
|
|
932
998
|
if (state.history.length > baseLen) state.history.length = baseLen;
|
|
933
999
|
state.history.push({
|
|
934
1000
|
role: 'user',
|
|
@@ -1211,7 +1277,13 @@ NGUYÊN TẮC:
|
|
|
1211
1277
|
const stats = memoryStats() || { rules: 0, notes: 0, mtime: st.mtimeMs };
|
|
1212
1278
|
console.log(
|
|
1213
1279
|
c.dim(
|
|
1214
|
-
t.memoryStatsMain(
|
|
1280
|
+
t.memoryStatsMain(
|
|
1281
|
+
mainLines,
|
|
1282
|
+
fmtBytes(mainBytes),
|
|
1283
|
+
stats.rules,
|
|
1284
|
+
stats.notes,
|
|
1285
|
+
relTime(stats.mtime)
|
|
1286
|
+
)
|
|
1215
1287
|
)
|
|
1216
1288
|
);
|
|
1217
1289
|
} catch {
|
|
@@ -1255,6 +1327,8 @@ NGUYÊN TẮC:
|
|
|
1255
1327
|
}
|
|
1256
1328
|
|
|
1257
1329
|
tui.start();
|
|
1330
|
+
tui.setAgentMode(state.agentUiMode);
|
|
1331
|
+
tui.setGoal(state.goal || '');
|
|
1258
1332
|
banner();
|
|
1259
1333
|
printStatus(state);
|
|
1260
1334
|
// noob.md status line — cho user thấy model có memory gì (số dòng, rules/notes,
|
|
@@ -1345,6 +1419,7 @@ NGUYÊN TẮC:
|
|
|
1345
1419
|
execTool,
|
|
1346
1420
|
spawnAgentToolsDoc,
|
|
1347
1421
|
cwd: process.cwd(),
|
|
1422
|
+
tui, // route log sub-agent nền → ring buffer (Ctrl+O xem), cập nhật badge số agent nền
|
|
1348
1423
|
});
|
|
1349
1424
|
|
|
1350
1425
|
// Main loop — runs until /exit, double Ctrl+C, or EOF. Never exits after a task.
|
|
@@ -1356,12 +1431,23 @@ NGUYÊN TẮC:
|
|
|
1356
1431
|
for (const f of fin) {
|
|
1357
1432
|
const label = f.name ? `${f.name} (${f.id})` : f.id;
|
|
1358
1433
|
if (f.status === 'done') {
|
|
1359
|
-
console.log(
|
|
1434
|
+
console.log(
|
|
1435
|
+
c.tool(
|
|
1436
|
+
' 🎼 ' + (t.bgWorkflowDone ? t.bgWorkflowDone(label) : `workflow nền ${label} đã xong`)
|
|
1437
|
+
)
|
|
1438
|
+
);
|
|
1360
1439
|
pending.push(
|
|
1361
1440
|
`[Kết quả workflow nền "${label}"]\n${f.result}\n\nTổng hợp kết quả trên cho user (ngắn gọn, nêu điểm chính). Đây là output từ workflow chạy nền — KHÔNG chạy lại.`
|
|
1362
1441
|
);
|
|
1363
1442
|
} else {
|
|
1364
|
-
console.log(
|
|
1443
|
+
console.log(
|
|
1444
|
+
c.err(
|
|
1445
|
+
' 🎼 ' +
|
|
1446
|
+
(t.bgWorkflowFailed
|
|
1447
|
+
? t.bgWorkflowFailed(label, f.error)
|
|
1448
|
+
: `workflow nền ${label} lỗi: ${f.error}`)
|
|
1449
|
+
)
|
|
1450
|
+
);
|
|
1365
1451
|
}
|
|
1366
1452
|
}
|
|
1367
1453
|
let input;
|
|
@@ -1475,7 +1561,8 @@ NGUYÊN TẮC:
|
|
|
1475
1561
|
state.history,
|
|
1476
1562
|
state.agentMode ? spawnAgentToolsDoc(0) : '',
|
|
1477
1563
|
state.goal,
|
|
1478
|
-
sessions.list(5, process.cwd()).filter((s) => s.id !== session?.id)
|
|
1564
|
+
sessions.list(5, process.cwd()).filter((s) => s.id !== session?.id),
|
|
1565
|
+
state.agentUiMode
|
|
1479
1566
|
);
|
|
1480
1567
|
const userMessage = buildUserMessage(state.history);
|
|
1481
1568
|
tokenMeter.setContext(countTokens(systemPrompt) + countTokens(userMessage));
|
|
@@ -1491,7 +1578,15 @@ NGUYÊN TẮC:
|
|
|
1491
1578
|
// src/repl/agent-dispatch.js (v1.12.x). Factory được gọi MỖI turn vì abort
|
|
1492
1579
|
// được rebind trong handle() — không cache.
|
|
1493
1580
|
const dispatchTool = createAgentDispatcher({
|
|
1494
|
-
state,
|
|
1581
|
+
state,
|
|
1582
|
+
abort,
|
|
1583
|
+
tokenMeter,
|
|
1584
|
+
stopSpin,
|
|
1585
|
+
startSpin,
|
|
1586
|
+
execTool,
|
|
1587
|
+
tui,
|
|
1588
|
+
c,
|
|
1589
|
+
bgRegistry,
|
|
1495
1590
|
});
|
|
1496
1591
|
|
|
1497
1592
|
const answer = await runAgent({
|
|
@@ -1502,6 +1597,7 @@ NGUYÊN TẮC:
|
|
|
1502
1597
|
goal: state.goal,
|
|
1503
1598
|
recentSessions: sessions.list(5, process.cwd()).filter((s) => s.id !== session?.id),
|
|
1504
1599
|
extraToolsDoc: state.agentMode ? spawnAgentToolsDoc(0) : '',
|
|
1600
|
+
uiMode: state.agentUiMode,
|
|
1505
1601
|
// Pending tasks: todo items chưa hoàn thành từ lượt trước → model tiếp tục ngay.
|
|
1506
1602
|
pendingTasks: (state.todos || []).filter((t) => !t.done).map((t) => t.text),
|
|
1507
1603
|
onStatus: () => tick(t.thinking),
|
|
@@ -1558,20 +1654,31 @@ NGUYÊN TẮC:
|
|
|
1558
1654
|
// Lý do: auto-compact gián đoạn workflow giữa chừng, summary có thể mất chi
|
|
1559
1655
|
// tiết user cần. Giữ 2 mốc CẢNH BÁO (60% / 80%) để user biết khi nào nên
|
|
1560
1656
|
// chạy /compact, nhưng KHÔNG tự động chạy nữa.
|
|
1561
|
-
//
|
|
1657
|
+
// QUAN TRỌNG: key off SEND_LIMIT_TOKENS (ngưỡng gửi THẬT ~200k), KHÔNG phải
|
|
1658
|
+
// CONTEXT_WINDOW (1M, chỉ để hiển thị ctx%). Nếu dùng CONTEXT_WINDOW thì cảnh
|
|
1659
|
+
// báo 80% = 800k token — gateway đã reject ở ~200k từ lâu, cảnh báo thành
|
|
1660
|
+
// code chết. Với SEND_LIMIT_TOKENS = 200k:
|
|
1562
1661
|
// 60% (120k) → nhắc nhẹ một lần
|
|
1563
1662
|
// 80% (160k) → cảnh báo mạnh — nên /compact ngay trước khi provider reject
|
|
1564
1663
|
try {
|
|
1565
1664
|
const totalTokens = countMessages(state.history);
|
|
1566
1665
|
const k = Math.round(totalTokens / 1000);
|
|
1567
|
-
const pct = Math.round((totalTokens /
|
|
1568
|
-
if (totalTokens >=
|
|
1666
|
+
const pct = Math.round((totalTokens / SEND_LIMIT_TOKENS) * 100);
|
|
1667
|
+
if (totalTokens >= SEND_LIMIT_TOKENS * 0.8) {
|
|
1569
1668
|
// Mốc 2 (≥80% — 160k tokens): cảnh báo mạnh, gợi ý /compact ngay.
|
|
1570
|
-
console.log(
|
|
1669
|
+
console.log(
|
|
1670
|
+
c.err(
|
|
1671
|
+
` ⚠ ${t.veryLongSession(k)} (${pct}% giới hạn gửi) — gõ /compact để tóm tắt, tránh provider reject ở ~200k.`
|
|
1672
|
+
)
|
|
1673
|
+
);
|
|
1571
1674
|
state._longSessionWarned = true;
|
|
1572
|
-
} else if (totalTokens >=
|
|
1675
|
+
} else if (totalTokens >= SEND_LIMIT_TOKENS * 0.6 && !state._longSessionWarned) {
|
|
1573
1676
|
// Mốc 1 (≥60% — 120k tokens): nhắc nhẹ một lần.
|
|
1574
|
-
console.log(
|
|
1677
|
+
console.log(
|
|
1678
|
+
c.dim(
|
|
1679
|
+
` ⓘ ${t.longSession(k)} (${pct}% giới hạn gửi) — cân nhắc /compact nếu phiên còn dài.`
|
|
1680
|
+
)
|
|
1681
|
+
);
|
|
1575
1682
|
state._longSessionWarned = true;
|
|
1576
1683
|
}
|
|
1577
1684
|
} catch {}
|
|
@@ -1589,7 +1696,11 @@ NGUYÊN TẮC:
|
|
|
1589
1696
|
|
|
1590
1697
|
// Diff preview — show user CHÍNH XÁC dòng nào bị xoá/thêm trước khi approve.
|
|
1591
1698
|
// Tránh tin model mù: bắt được hallucinate nội dung sớm.
|
|
1592
|
-
if (
|
|
1699
|
+
if (
|
|
1700
|
+
name === 'edit_file' &&
|
|
1701
|
+
typeof input.old_string === 'string' &&
|
|
1702
|
+
typeof input.new_string === 'string'
|
|
1703
|
+
) {
|
|
1593
1704
|
try {
|
|
1594
1705
|
const filePath = path.resolve(process.cwd(), input.path || '');
|
|
1595
1706
|
let oldText = input.old_string;
|
|
@@ -1616,7 +1727,11 @@ NGUYÊN TẮC:
|
|
|
1616
1727
|
console.log(renderUnifiedDiff(oldText, newText, { label: input.path || '(unknown path)' }));
|
|
1617
1728
|
} catch (e) {
|
|
1618
1729
|
// Fallback nếu đọc file lỗi: ít nhất show old vs new raw.
|
|
1619
|
-
console.log(
|
|
1730
|
+
console.log(
|
|
1731
|
+
renderUnifiedDiff(input.old_string, input.new_string, {
|
|
1732
|
+
label: input.path || '(unknown path)',
|
|
1733
|
+
})
|
|
1734
|
+
);
|
|
1620
1735
|
}
|
|
1621
1736
|
} else if (name === 'write_file' && typeof input.content === 'string') {
|
|
1622
1737
|
try {
|
|
@@ -1624,13 +1739,28 @@ NGUYÊN TẮC:
|
|
|
1624
1739
|
if (fs.existsSync(filePath)) {
|
|
1625
1740
|
// File đã tồn tại → write_file là OVERWRITE → show diff old vs new.
|
|
1626
1741
|
const oldContent = fs.readFileSync(filePath, 'utf8');
|
|
1627
|
-
console.log(
|
|
1742
|
+
console.log(
|
|
1743
|
+
renderUnifiedDiff(oldContent, input.content, {
|
|
1744
|
+
label: input.path + ' (OVERWRITE)',
|
|
1745
|
+
maxLines: 60,
|
|
1746
|
+
})
|
|
1747
|
+
);
|
|
1628
1748
|
} else {
|
|
1629
1749
|
// File mới → preview top 20 dòng + tổng số dòng.
|
|
1630
|
-
console.log(
|
|
1750
|
+
console.log(
|
|
1751
|
+
renderNewFilePreview(input.content, {
|
|
1752
|
+
label: input.path || '(unknown path)',
|
|
1753
|
+
maxLines: 20,
|
|
1754
|
+
})
|
|
1755
|
+
);
|
|
1631
1756
|
}
|
|
1632
1757
|
} catch {
|
|
1633
|
-
console.log(
|
|
1758
|
+
console.log(
|
|
1759
|
+
renderNewFilePreview(input.content, {
|
|
1760
|
+
label: input.path || '(unknown path)',
|
|
1761
|
+
maxLines: 20,
|
|
1762
|
+
})
|
|
1763
|
+
);
|
|
1634
1764
|
}
|
|
1635
1765
|
}
|
|
1636
1766
|
|
|
@@ -1640,7 +1770,11 @@ NGUYÊN TẮC:
|
|
|
1640
1770
|
const rawPath = typeof input?.path === 'string' && input.path.length > 0 ? input.path : null;
|
|
1641
1771
|
let absPath = null;
|
|
1642
1772
|
if (rawPath) {
|
|
1643
|
-
try {
|
|
1773
|
+
try {
|
|
1774
|
+
absPath = path.resolve(process.cwd(), rawPath);
|
|
1775
|
+
} catch {
|
|
1776
|
+
absPath = null;
|
|
1777
|
+
}
|
|
1644
1778
|
}
|
|
1645
1779
|
const fileKey = absPath ? name + ':' + absPath : null;
|
|
1646
1780
|
|
|
@@ -1753,6 +1887,20 @@ NGUYÊN TẮC:
|
|
|
1753
1887
|
);
|
|
1754
1888
|
break;
|
|
1755
1889
|
}
|
|
1890
|
+
case 'mode': {
|
|
1891
|
+
const v = arg.toLowerCase().trim();
|
|
1892
|
+
if (!v) {
|
|
1893
|
+
console.log(c.accent(' ◆ mode: ') + state.agentUiMode);
|
|
1894
|
+
console.log(c.dim(' cú pháp: /mode build|plan|compose · hoặc Ctrl+T để cycle'));
|
|
1895
|
+
} else if (v === 'build' || v === 'plan' || v === 'compose') {
|
|
1896
|
+
state.agentUiMode = v;
|
|
1897
|
+
tui.setAgentMode(v);
|
|
1898
|
+
console.log(c.accent(' ◆ mode: ') + v);
|
|
1899
|
+
} else {
|
|
1900
|
+
console.log(c.err(' mode không hợp lệ: ') + v + c.dim(' (chọn build|plan|compose)'));
|
|
1901
|
+
}
|
|
1902
|
+
break;
|
|
1903
|
+
}
|
|
1756
1904
|
case 'kg': {
|
|
1757
1905
|
// Knowledge graph CRUD — port từ mcp-knowledge-graph, lưu .noob/kg.jsonl.
|
|
1758
1906
|
// Sub-cmd: list, path, add, obs, link, unlink, unobs, get, search, rm.
|
|
@@ -1777,11 +1925,18 @@ NGUYÊN TẮC:
|
|
|
1777
1925
|
} else {
|
|
1778
1926
|
const [, name, etype, obsRaw] = m2;
|
|
1779
1927
|
const observations = obsRaw
|
|
1780
|
-
? obsRaw
|
|
1928
|
+
? obsRaw
|
|
1929
|
+
.split(';')
|
|
1930
|
+
.map((s) => s.trim())
|
|
1931
|
+
.filter(Boolean)
|
|
1781
1932
|
: [];
|
|
1782
1933
|
const created = await kgCreateEntities([{ name, entityType: etype, observations }]);
|
|
1783
|
-
if (!created.length)
|
|
1784
|
-
|
|
1934
|
+
if (!created.length)
|
|
1935
|
+
console.log(c.dim(` Entity '${name}' đã tồn tại — không tạo lại`));
|
|
1936
|
+
else
|
|
1937
|
+
console.log(
|
|
1938
|
+
c.ok(' ✓ ') + `đã thêm ${name} [${etype}] (${observations.length} obs)`
|
|
1939
|
+
);
|
|
1785
1940
|
}
|
|
1786
1941
|
} else if (sub === 'obs') {
|
|
1787
1942
|
const m2 = rest.match(/^(\S+)\s+([\s\S]+)$/);
|
|
@@ -1791,7 +1946,8 @@ NGUYÊN TẮC:
|
|
|
1791
1946
|
const [, name, obs] = m2;
|
|
1792
1947
|
const out = await kgAddObservations([{ entityName: name, contents: [obs] }]);
|
|
1793
1948
|
const added = out[0]?.addedObservations || [];
|
|
1794
|
-
if (!added.length)
|
|
1949
|
+
if (!added.length)
|
|
1950
|
+
console.log(c.dim(` Observation đã có cho '${name}' — không thêm lại`));
|
|
1795
1951
|
else console.log(c.ok(' ✓ ') + `đã thêm obs cho ${name}: ${added[0]}`);
|
|
1796
1952
|
}
|
|
1797
1953
|
} else if (sub === 'link') {
|
|
@@ -1831,7 +1987,8 @@ NGUYÊN TẮC:
|
|
|
1831
1987
|
} else {
|
|
1832
1988
|
const names = rest.split(/\s+/).filter(Boolean);
|
|
1833
1989
|
const g = await kgOpenNodes(names);
|
|
1834
|
-
if (!g.entities.length)
|
|
1990
|
+
if (!g.entities.length)
|
|
1991
|
+
console.log(c.dim(` Không tìm thấy entity: ${names.join(', ')}`));
|
|
1835
1992
|
else console.log(kgFormat(g));
|
|
1836
1993
|
}
|
|
1837
1994
|
} else if (sub === 'search') {
|
|
@@ -1882,10 +2039,12 @@ NGUYÊN TẮC:
|
|
|
1882
2039
|
v.toLowerCase() === 'xoa'
|
|
1883
2040
|
) {
|
|
1884
2041
|
state.goal = null;
|
|
2042
|
+
tui.setGoal('');
|
|
1885
2043
|
console.log(c.dim(' đã xoá goal'));
|
|
1886
2044
|
persist();
|
|
1887
2045
|
} else {
|
|
1888
2046
|
state.goal = v;
|
|
2047
|
+
tui.setGoal(v);
|
|
1889
2048
|
console.log(c.accent(' 🎯 đã đặt goal: ') + v);
|
|
1890
2049
|
persist();
|
|
1891
2050
|
}
|
|
@@ -2073,7 +2232,14 @@ NGUYÊN TẮC:
|
|
|
2073
2232
|
case 'q': {
|
|
2074
2233
|
const swept = bgRegistry.sweepOnExit();
|
|
2075
2234
|
if (swept > 0)
|
|
2076
|
-
console.log(
|
|
2235
|
+
console.log(
|
|
2236
|
+
c.dim(
|
|
2237
|
+
' ' +
|
|
2238
|
+
(t.bgWorkflowSweep
|
|
2239
|
+
? t.bgWorkflowSweep(swept)
|
|
2240
|
+
: `${swept} workflow nền bị ngắt — resume bằng /workflow resume <id>`)
|
|
2241
|
+
)
|
|
2242
|
+
);
|
|
2077
2243
|
persist();
|
|
2078
2244
|
exiting = true;
|
|
2079
2245
|
console.log(c.dim(' ' + t.bye));
|
|
@@ -2294,5 +2460,3 @@ function listModels() {
|
|
|
2294
2460
|
}
|
|
2295
2461
|
console.log('\n' + lines.join('\n') + c.dim('\n\n ' + t.modelListHint) + '\n');
|
|
2296
2462
|
}
|
|
2297
|
-
|
|
2298
|
-
|
package/src/tokens.js
CHANGED
|
@@ -57,12 +57,19 @@ export function countMessages(messages = []) {
|
|
|
57
57
|
// window đủ rộng (256 chars) để qua mọi ranh giới token thực tế của cl100k/o200k
|
|
58
58
|
// (token dài nhất ~ vài chục byte).
|
|
59
59
|
const TAIL_WINDOW = 256;
|
|
60
|
-
// Context window
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
|
|
60
|
+
// Context window cho HIỂN THỊ (ctx% ở status bar). Đặt 1M tokens.
|
|
61
|
+
// QUAN TRỌNG — đây là con số DISPLAY-ONLY: ctx% = contextTokens / CONTEXT_WINDOW.
|
|
62
|
+
// Ngưỡng nén/tóm tắt THỰC TẾ (MAX_PROMPT_CHARS, SUMMARIZE_THRESHOLD_CHARS trong
|
|
63
|
+
// src/agent.js) GIỮ NGUYÊN ở mức an toàn (~200k token) để gateway/upstream không
|
|
64
|
+
// reject prompt TRƯỚC khi compact kịp chạy. Nếu sau này xác nhận gateway thật sự
|
|
65
|
+
// nhận ~1M token context, mới nâng các ngưỡng đó cho khớp.
|
|
66
|
+
export const CONTEXT_WINDOW = 1_000_000;
|
|
67
|
+
|
|
68
|
+
// Ngưỡng GỬI THẬT (real ceiling). Cảnh báo phiên dài + logic compact key off CON
|
|
69
|
+
// SỐ NÀY, không phải CONTEXT_WINDOW. Tách 2 hằng: CONTEXT_WINDOW chỉ để hiển thị
|
|
70
|
+
// ctx%; SEND_LIMIT_TOKENS là mức mà vượt qua thì gateway/upstream bắt đầu reject.
|
|
71
|
+
// Nếu nâng mức gửi thật (gateway nhận nhiều hơn), đổi DUY NHẤT hằng này.
|
|
72
|
+
export const SEND_LIMIT_TOKENS = 200_000;
|
|
66
73
|
|
|
67
74
|
export class TokenMeter {
|
|
68
75
|
constructor() {
|