@noobdemon/noob-cli 1.5.5 → 1.7.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/package.json +4 -2
- package/src/agent.js +99 -9
- package/src/i18n.js +2 -0
- package/src/repl.js +60 -6
- package/src/subagent.js +61 -0
- package/src/tokens.js +70 -0
- package/src/tools.js +4 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@noobdemon/noob-cli",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.7.0",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"LICENSE"
|
|
17
17
|
],
|
|
18
18
|
"scripts": {
|
|
19
|
-
"start": "node bin/noob.js"
|
|
19
|
+
"start": "node bin/noob.js",
|
|
20
|
+
"postpublish": "node scripts/notify-discord.js"
|
|
20
21
|
},
|
|
21
22
|
"engines": {
|
|
22
23
|
"node": ">=18"
|
|
@@ -38,6 +39,7 @@
|
|
|
38
39
|
"boxen": "^8.0.1",
|
|
39
40
|
"chalk": "^5.4.1",
|
|
40
41
|
"cli-highlight": "^2.1.11",
|
|
42
|
+
"gpt-tokenizer": "^3.4.0",
|
|
41
43
|
"gradient-string": "^3.0.0",
|
|
42
44
|
"marked": "^15.0.12",
|
|
43
45
|
"marked-terminal": "^7.3.0",
|
package/src/agent.js
CHANGED
|
@@ -2,6 +2,7 @@ import os from "node:os";
|
|
|
2
2
|
import { stream } from "./api.js";
|
|
3
3
|
import { loadMemory } from "./memory.js";
|
|
4
4
|
import { t } from "./i18n.js";
|
|
5
|
+
import { countTokens } from "./tokens.js";
|
|
5
6
|
|
|
6
7
|
export const SYSTEM = `You are noob, an agentic coding assistant in the spirit of Claude Code. You help with software engineering tasks by reading and editing files and running commands in the user's current working directory.
|
|
7
8
|
|
|
@@ -71,8 +72,13 @@ Có — cả 12 test đều pass.
|
|
|
71
72
|
|
|
72
73
|
Follow this pattern exactly. Your very first response to a task that needs the filesystem MUST be a tool block — do not refuse or explain limitations.`;
|
|
73
74
|
|
|
74
|
-
|
|
75
|
+
// Số bước tool tối đa cho một lượt. Đặt rất cao theo yêu cầu người dùng: task
|
|
76
|
+
// dài cứ chạy, đừng tự dừng. Người dùng vẫn có thể Ctrl+C bất cứ lúc nào.
|
|
77
|
+
const MAX_STEPS = 10000;
|
|
75
78
|
const MAX_PROMPT_CHARS = 80000; // ngân sách ký tự cho phần hội thoại gửi lên model
|
|
79
|
+
// Khi history vượt ngưỡng này, gọi model phụ tóm tắt các lượt cũ thay vì cắt cụt
|
|
80
|
+
// → giữ được "trí nhớ dài hạn" trong phiên mà không nổ context.
|
|
81
|
+
const SUMMARIZE_THRESHOLD_CHARS = 60000;
|
|
76
82
|
|
|
77
83
|
// Môi trường chạy thực: model cần biết OS + shell để emit lệnh ĐÚNG. Không có
|
|
78
84
|
// khối này, trên Windows model hay emit lệnh Unix (wc/ls/cat/grep) → run_command
|
|
@@ -101,6 +107,7 @@ function runtimeContext() {
|
|
|
101
107
|
|
|
102
108
|
// Lược ngữ cảnh để không vượt context khi phiên dài. KHÔNG đụng vào history thật
|
|
103
109
|
// (vẫn lưu/đầy đủ để resume) — chỉ thu gọn BẢN SAO dùng cho prompt.
|
|
110
|
+
// Nếu history đã có summary (do summarizeHistory ghi vào _summary), dùng làm head.
|
|
104
111
|
function compact(history, budget) {
|
|
105
112
|
const len = (m) => (m.content || "").length + 24;
|
|
106
113
|
let total = history.reduce((s, m) => s + len(m), 0);
|
|
@@ -125,6 +132,68 @@ function compact(history, budget) {
|
|
|
125
132
|
return [...head, elided, ...out.slice(tailStart)];
|
|
126
133
|
}
|
|
127
134
|
|
|
135
|
+
// Bộ nhớ dài hạn cho phiên: khi history phình to, gọi model phụ TÓM TẮT các
|
|
136
|
+
// lượt cũ thành một message system gọn (giữ quyết định, file đã sửa, lý do,
|
|
137
|
+
// việc dở) rồi thay phần đầu history bằng tóm tắt đó. Mutates `history` in place.
|
|
138
|
+
// Trả về true nếu có tóm tắt (để caller persist phiên ngay).
|
|
139
|
+
export async function maybeSummarize(history, { model, signal }) {
|
|
140
|
+
if (!history?.length) return false;
|
|
141
|
+
const totalChars = history.reduce((s, m) => s + (m.content?.length || 0), 0);
|
|
142
|
+
if (totalChars < SUMMARIZE_THRESHOLD_CHARS) return false;
|
|
143
|
+
// Giữ 8 message cuối nguyên vẹn; tóm tắt phần trước.
|
|
144
|
+
const keepTail = 8;
|
|
145
|
+
if (history.length <= keepTail + 2) return false;
|
|
146
|
+
// Nếu lượt đầu đã là summary (role=system, name=summary) → tóm tắt thêm.
|
|
147
|
+
const head = history.slice(0, history.length - keepTail);
|
|
148
|
+
const tail = history.slice(history.length - keepTail);
|
|
149
|
+
const transcript = head.map((m) => {
|
|
150
|
+
const role = m.role === "tool" ? `TOOL(${m.name || "?"})` : m.role.toUpperCase();
|
|
151
|
+
return `## ${role}\n${(m.content || "").slice(0, 2000)}`;
|
|
152
|
+
}).join("\n\n");
|
|
153
|
+
const ask = `Tóm tắt phần hội thoại sau thành BẢN GHI NGẮN GỌN (~25-40 dòng, tiếng Việt) để bạn — chính bạn — đọc lại sau và tiếp tục công việc mà không quên ngữ cảnh quan trọng.
|
|
154
|
+
|
|
155
|
+
BẮT BUỘC giữ lại:
|
|
156
|
+
- Mục tiêu/nhiệm vụ tổng người dùng giao (nguyên văn nếu ngắn).
|
|
157
|
+
- Các quyết định thiết kế đã chốt và LÝ DO.
|
|
158
|
+
- Danh sách file đã tạo/sửa kèm mô tả NGẮN (1 dòng/file) — vai trò của file.
|
|
159
|
+
- Các phát hiện quan trọng từ tool (cấu trúc dự án, lệnh build/test, lỗi đã gặp & cách fix).
|
|
160
|
+
- Việc CÒN DANG DỞ và bước kế tiếp đã dự định.
|
|
161
|
+
- Sở thích/yêu cầu đặc biệt của người dùng.
|
|
162
|
+
|
|
163
|
+
LOẠI BỎ: chi tiết nội dung file đọc được, output dài của tool, các lượt khảo sát lặp lại.
|
|
164
|
+
|
|
165
|
+
Định dạng:
|
|
166
|
+
# Tóm tắt phiên (đến lượt thứ ${head.length})
|
|
167
|
+
## Mục tiêu
|
|
168
|
+
- ...
|
|
169
|
+
## Quyết định & lý do
|
|
170
|
+
- ...
|
|
171
|
+
## File đã đụng
|
|
172
|
+
- path — vai trò
|
|
173
|
+
## Phát hiện & lệnh quan trọng
|
|
174
|
+
- ...
|
|
175
|
+
## Đang làm dở / bước tiếp
|
|
176
|
+
- ...
|
|
177
|
+
|
|
178
|
+
--- HỘI THOẠI CẦN TÓM TẮT ---
|
|
179
|
+
${transcript}`;
|
|
180
|
+
try {
|
|
181
|
+
const { text } = await stream({ mode: "chat", model, message: ask, signal });
|
|
182
|
+
const summary = (text || "").trim();
|
|
183
|
+
if (!summary || summary.length < 50) return false;
|
|
184
|
+
// Thay head bằng MỘT message tool tên "session_summary".
|
|
185
|
+
const summaryMsg = {
|
|
186
|
+
role: "tool",
|
|
187
|
+
name: "session_summary",
|
|
188
|
+
content: `[BỘ NHỚ DÀI HẠN — tóm tắt ${head.length} lượt đầu để không quên ngữ cảnh]\n\n${summary}`,
|
|
189
|
+
};
|
|
190
|
+
history.splice(0, history.length, summaryMsg, ...tail);
|
|
191
|
+
return true;
|
|
192
|
+
} catch {
|
|
193
|
+
return false; // tóm tắt lỗi → cứ để compact() cắt cụt như cũ
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
128
197
|
// GROUND TRUTH: liệt kê những file ĐÃ THỰC SỰ được ghi/sửa, suy ra từ KẾT QUẢ
|
|
129
198
|
// tool có thật (không phải từ lời model tự kể). Chống lỗi model "tưởng đã tạo
|
|
130
199
|
// file" (chỉ kể trong văn xuôi, quên gọi write_file) rồi khăng khăng "file bị
|
|
@@ -148,21 +217,31 @@ function filesLedger(history) {
|
|
|
148
217
|
|
|
149
218
|
// Chèn bộ nhớ noob.md (nếu có) vào prompt — đây là phần "tự học" mà noob đọc
|
|
150
219
|
// lại mỗi lượt. Không có file → nhắc model tự tạo khi rút ra điều đáng nhớ.
|
|
220
|
+
// Framing MẠNH: phần `## Rules` là binding (luật dự án), `## Notes` mới là
|
|
221
|
+
// tham khảo. Đặt ngay sau SYSTEM trong buildPrompt() để không bị lost-in-the-middle.
|
|
151
222
|
function memoryBlock() {
|
|
152
223
|
const mem = loadMemory();
|
|
153
224
|
if (!mem)
|
|
154
|
-
return "# PROJECT MEMORY (noob.md)\n(Chưa có noob.md trong thư mục này. Khi rút ra điều đáng nhớ lâu dài — lệnh build/test/run, quy ước, kiến trúc, sở thích người dùng, tiến độ — hãy TẠO noob.md bằng write_file và ghi vào đó.)";
|
|
225
|
+
return "# PROJECT RULES & MEMORY (noob.md)\n(Chưa có noob.md trong thư mục này. Khi rút ra điều đáng nhớ lâu dài — lệnh build/test/run, quy ước, kiến trúc, sở thích người dùng, tiến độ — hãy TẠO noob.md bằng write_file và ghi vào đó.)";
|
|
155
226
|
return (
|
|
156
|
-
"# PROJECT MEMORY (noob.md —
|
|
227
|
+
"# PROJECT RULES & MEMORY (noob.md) — BINDING\n" +
|
|
228
|
+
"Phần `## Rules` dưới đây là LUẬT DỰ ÁN bạn PHẢI tuân theo trong mọi hành động ở lượt này — coi như mở rộng của SYSTEM, không phải gợi ý. Phần `## Notes` là quan sát tham khảo, có thể xác minh lại với filesystem nếu nghi ngờ.\n\n" +
|
|
157
229
|
mem +
|
|
158
|
-
"\n
|
|
230
|
+
"\n\nTrước khi emit hành động, đối chiếu với `## Rules` ở trên. Học thêm điều đáng nhớ → cập nhật noob.md bằng edit_file/write_file (Notes mới, promote lên Rules khi đã chứng minh)."
|
|
159
231
|
);
|
|
160
232
|
}
|
|
161
233
|
|
|
162
234
|
// The proxy is stateless, so we serialize the whole transcript into one prompt.
|
|
163
|
-
|
|
235
|
+
// extraToolsDoc: chuỗi mô tả thêm tool (vd spawn_agent khi agent mode bật) được
|
|
236
|
+
// chèn ngay sau SYSTEM để model biết và dùng được.
|
|
237
|
+
function buildPrompt(history, extraToolsDoc) {
|
|
164
238
|
const msgs = compact(history, MAX_PROMPT_CHARS);
|
|
165
|
-
|
|
239
|
+
// Thứ tự CÓ CHỦ ĐÍCH: SYSTEM → memoryBlock (Rules dự án, vị trí mạnh thứ 2,
|
|
240
|
+
// tránh lost-in-the-middle) → extraToolsDoc → runtimeContext → filesLedger →
|
|
241
|
+
// CONVERSATION. noob.md (đặc biệt phần `## Rules`) phải nằm sát SYSTEM để model coi là luật.
|
|
242
|
+
const parts = [SYSTEM, "", memoryBlock()];
|
|
243
|
+
if (extraToolsDoc) parts.push("", extraToolsDoc);
|
|
244
|
+
parts.push("", runtimeContext(), "", filesLedger(history), "", "=".repeat(60), "# CONVERSATION", "");
|
|
166
245
|
for (const m of msgs) {
|
|
167
246
|
if (m.role === "user") parts.push(`## USER\n${m.content}`);
|
|
168
247
|
else if (m.role === "assistant") parts.push(`## ASSISTANT\n${m.content}`);
|
|
@@ -230,14 +309,21 @@ function extractJsonObject(s, from) {
|
|
|
230
309
|
* @param {(msg:string)=>void} opts.onStatus thinking/streaming status
|
|
231
310
|
* @returns {Promise<string>} the final assistant answer (no tool block)
|
|
232
311
|
*/
|
|
233
|
-
export async function runAgent({ history, model, signal, onTool, onStatus, onDelta, onSteer }) {
|
|
312
|
+
export async function runAgent({ history, model, signal, onTool, onStatus, onDelta, onSteer, tokenMeter, extraToolsDoc }) {
|
|
234
313
|
for (let step = 0; step < MAX_STEPS; step++) {
|
|
314
|
+
// Mỗi 100 bước log một mốc để người dùng biết noob vẫn đang chạy (task dài).
|
|
315
|
+
if (step > 0 && step % 100 === 0) onStatus?.(`đã chạy ${step} bước…`);
|
|
316
|
+
|
|
235
317
|
// Steering: tin nhắn người dùng gõ GIỮA CHỪNG được chèn vào hội thoại TRƯỚC
|
|
236
318
|
// lần gọi model kế tiếp → model thấy và điều chỉnh ngay trong cùng task.
|
|
237
319
|
const steer = onSteer?.() || [];
|
|
238
320
|
for (const msg of steer) history.push({ role: "user", content: msg });
|
|
239
321
|
|
|
240
|
-
|
|
322
|
+
// Bộ nhớ dài hạn: thử tóm tắt nếu history đã phình. Im lặng nếu không cần.
|
|
323
|
+
try { await maybeSummarize(history, { model, signal }); } catch {}
|
|
324
|
+
|
|
325
|
+
const prompt = buildPrompt(history, extraToolsDoc);
|
|
326
|
+
tokenMeter?.addInput(countTokens(prompt));
|
|
241
327
|
onStatus?.("thinking");
|
|
242
328
|
onDelta?.({ type: "step-start" });
|
|
243
329
|
const { text } = await stream({
|
|
@@ -245,8 +331,12 @@ export async function runAgent({ history, model, signal, onTool, onStatus, onDel
|
|
|
245
331
|
model,
|
|
246
332
|
message: prompt,
|
|
247
333
|
signal,
|
|
248
|
-
onDelta: (d) =>
|
|
334
|
+
onDelta: (d) => {
|
|
335
|
+
tokenMeter?.pushOutputDelta(d);
|
|
336
|
+
onDelta?.({ type: "delta", text: d });
|
|
337
|
+
},
|
|
249
338
|
});
|
|
339
|
+
tokenMeter?.endOutput();
|
|
250
340
|
onDelta?.({ type: "step-end" });
|
|
251
341
|
history.push({ role: "assistant", content: text });
|
|
252
342
|
|
package/src/i18n.js
CHANGED
|
@@ -57,6 +57,8 @@ export const t = {
|
|
|
57
57
|
cmdSearch: "/search bật/tắt chế độ tìm web",
|
|
58
58
|
cmdChat: "/chat quay lại chế độ chat thường",
|
|
59
59
|
cmdYolo: "/yolo bật/tắt tự duyệt (hoặc nhấn Shift+Tab)",
|
|
60
|
+
cmdAgent: "/agent on|off bật/tắt agent mode (model đẻ sub-agent song song/tuần tự/phân cấp)",
|
|
61
|
+
cmdTokens: "/tokens xem số token đã dùng trong phiên",
|
|
60
62
|
cmdAutoYolo: "/auto-yolo lưu/bỏ yolo làm mặc định mỗi lần chạy (cần xác nhận)",
|
|
61
63
|
cmdInit: "/init quét dự án & tạo noob.md (tổng quan + quy ước, như Claude Code)",
|
|
62
64
|
cmdKarpathy: "/karpathy [path] rà soát code theo 4 nguyên tắc Karpathy (/kc)",
|
package/src/repl.js
CHANGED
|
@@ -4,6 +4,8 @@ import path from "node:path";
|
|
|
4
4
|
import chalk from "chalk";
|
|
5
5
|
import { createTui } from "./tui.js";
|
|
6
6
|
import { runAgent } from "./agent.js";
|
|
7
|
+
import { runSubAgent, spawnAgentToolsDoc, MAX_SUBAGENT_DEPTH } from "./subagent.js";
|
|
8
|
+
import { TokenMeter } from "./tokens.js";
|
|
7
9
|
import { stream, usage, ApiError } from "./api.js";
|
|
8
10
|
import { runTool, describe, DESTRUCTIVE } from "./tools.js";
|
|
9
11
|
import { MODELS, PROVIDERS, findModel, providerColor, DEFAULT_MODEL } from "./models.js";
|
|
@@ -28,6 +30,8 @@ const SLASH = [
|
|
|
28
30
|
{ name: "/init", desc: "quét dự án & tạo noob.md" },
|
|
29
31
|
{ name: "/karpathy", desc: "rà soát code (Karpathy)" },
|
|
30
32
|
{ name: "/ultra", desc: "tự hành: tự nghĩ & làm nhiệm vụ" },
|
|
33
|
+
{ name: "/agent", desc: "bật/tắt agent mode (spawn sub-agent)" },
|
|
34
|
+
{ name: "/tokens", desc: "xem số token đã dùng phiên này" },
|
|
31
35
|
{ name: "/learn", desc: "chưng cất bài học vào noob.md" },
|
|
32
36
|
{ name: "/memory", desc: "xem bộ nhớ noob.md" },
|
|
33
37
|
{ name: "/login", desc: "đăng nhập bằng API key" },
|
|
@@ -127,7 +131,9 @@ export async function startRepl(opts = {}) {
|
|
|
127
131
|
autoApprove: new Set(),
|
|
128
132
|
yolo: !!opts.yolo || config.yoloDefault, // cờ --yolo HOẶC mặc định đã lưu (/auto-yolo)
|
|
129
133
|
ultra: false, // chế độ tự hành (self-quest) đang chạy?
|
|
134
|
+
agentMode: false, // /agent on → cho phép spawn_agent / spawn_agents
|
|
130
135
|
};
|
|
136
|
+
const tokenMeter = new TokenMeter();
|
|
131
137
|
|
|
132
138
|
// Prompt = dòng trạng thái sống. Luôn phản ánh yolo + version theo thời gian
|
|
133
139
|
// thực (vẽ lại mỗi lượt và ngay khi Shift+Tab), nên không cần gõ /status.
|
|
@@ -632,10 +638,49 @@ NGUYÊN TẮC:
|
|
|
632
638
|
startSpin(t.thinking);
|
|
633
639
|
let printer = null;
|
|
634
640
|
|
|
641
|
+
const dispatchTool = async (name, input, depth = 0) => {
|
|
642
|
+
// spawn_agent / spawn_agents chỉ được phép khi agentMode bật; depth giới hạn
|
|
643
|
+
// bởi MAX_SUBAGENT_DEPTH để tránh đệ quy nổ.
|
|
644
|
+
if (name === "spawn_agent" || name === "spawn_agents") {
|
|
645
|
+
if (!state.agentMode)
|
|
646
|
+
return { allow: true, result: "ERROR: agent mode đang TẮT — gõ /agent on để bật trước khi spawn." };
|
|
647
|
+
if (depth >= MAX_SUBAGENT_DEPTH)
|
|
648
|
+
return { allow: true, result: `ERROR: đã đạt depth tối đa (${MAX_SUBAGENT_DEPTH}) — không spawn thêm.` };
|
|
649
|
+
const tasks = name === "spawn_agent" ? [input] : (Array.isArray(input?.agents) ? input.agents : []);
|
|
650
|
+
if (!tasks.length) return { allow: true, result: "ERROR: thiếu task cho sub-agent." };
|
|
651
|
+
stopSpin();
|
|
652
|
+
console.log(chalk.hex("#8b5cf6")(` ⊕ spawn ${tasks.length} sub-agent (depth ${depth + 1}/${MAX_SUBAGENT_DEPTH})`));
|
|
653
|
+
startSpin(t.thinking);
|
|
654
|
+
try {
|
|
655
|
+
const results = await Promise.all(tasks.map((task, i) =>
|
|
656
|
+
runSubAgent({
|
|
657
|
+
task: task?.task || task?.prompt || "",
|
|
658
|
+
context: task?.context || "",
|
|
659
|
+
depth: depth + 1,
|
|
660
|
+
model: state.model.id,
|
|
661
|
+
signal: abort.signal,
|
|
662
|
+
tokenMeter,
|
|
663
|
+
dispatchTool: (n, inp) => dispatchTool(n, inp, depth + 1),
|
|
664
|
+
onLog: (msg) => { stopSpin(); console.log(chalk.hex("#8b5cf6")(" " + msg)); startSpin(t.thinking); },
|
|
665
|
+
}).then((r) => `── sub-agent #${i + 1} ──\n${r}`).catch((e) => `── sub-agent #${i + 1} (LỖI) ──\n${e?.message || String(e)}`)
|
|
666
|
+
));
|
|
667
|
+
return { allow: true, result: results.join("\n\n") };
|
|
668
|
+
} catch (err) {
|
|
669
|
+
return { allow: true, result: "ERROR sub-agent: " + (err?.message || String(err)) };
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
stopSpin();
|
|
673
|
+
const res = await execTool(name, input);
|
|
674
|
+
startSpin(t.thinking);
|
|
675
|
+
return res;
|
|
676
|
+
};
|
|
677
|
+
|
|
635
678
|
const answer = await runAgent({
|
|
636
679
|
history: state.history,
|
|
637
680
|
model: state.model.id,
|
|
638
681
|
signal: abort.signal,
|
|
682
|
+
tokenMeter,
|
|
683
|
+
extraToolsDoc: state.agentMode ? spawnAgentToolsDoc(0) : "",
|
|
639
684
|
onStatus: () => tick(t.thinking),
|
|
640
685
|
onSteer: () => {
|
|
641
686
|
if (!pending.length) return [];
|
|
@@ -657,12 +702,7 @@ NGUYÊN TẮC:
|
|
|
657
702
|
printer?.flush();
|
|
658
703
|
}
|
|
659
704
|
},
|
|
660
|
-
onTool:
|
|
661
|
-
stopSpin();
|
|
662
|
-
const res = await execTool(name, input);
|
|
663
|
-
startSpin(t.thinking);
|
|
664
|
-
return res;
|
|
665
|
-
},
|
|
705
|
+
onTool: (name, input) => dispatchTool(name, input, 0),
|
|
666
706
|
});
|
|
667
707
|
|
|
668
708
|
stopSpin();
|
|
@@ -771,6 +811,18 @@ NGUYÊN TẮC:
|
|
|
771
811
|
state.yolo = !state.yolo;
|
|
772
812
|
console.log((state.yolo ? c.err : c.ok)(" " + (state.yolo ? t.yoloOn : t.yoloOff)));
|
|
773
813
|
break;
|
|
814
|
+
case "agent": {
|
|
815
|
+
const v = arg.toLowerCase();
|
|
816
|
+
if (v === "on" || v === "bật" || v === "bat") state.agentMode = true;
|
|
817
|
+
else if (v === "off" || v === "tắt" || v === "tat") state.agentMode = false;
|
|
818
|
+
else state.agentMode = !state.agentMode;
|
|
819
|
+
console.log((state.agentMode ? c.accent : c.dim)(" agent mode: " + (state.agentMode ? "BẬT (spawn_agent / spawn_agents khả dụng, depth tối đa " + MAX_SUBAGENT_DEPTH + ")" : "tắt")));
|
|
820
|
+
break;
|
|
821
|
+
}
|
|
822
|
+
case "tokens": {
|
|
823
|
+
console.log(c.dim(` tokens — input: ${tokenMeter.input.toLocaleString("vi-VN")} · output: ${tokenMeter.output.toLocaleString("vi-VN")} · tổng: ${tokenMeter.total.toLocaleString("vi-VN")} · ${tokenMeter.format()}`));
|
|
824
|
+
break;
|
|
825
|
+
}
|
|
774
826
|
case "auto-yolo":
|
|
775
827
|
case "autoyolo":
|
|
776
828
|
await toggleAutoYolo();
|
|
@@ -1016,6 +1068,8 @@ function printHelp() {
|
|
|
1016
1068
|
" " + t.cmdSearch,
|
|
1017
1069
|
" " + t.cmdChat,
|
|
1018
1070
|
" " + t.cmdYolo,
|
|
1071
|
+
" " + t.cmdAgent,
|
|
1072
|
+
" " + t.cmdTokens,
|
|
1019
1073
|
" " + t.cmdAutoYolo,
|
|
1020
1074
|
" " + t.cmdInit,
|
|
1021
1075
|
" " + t.cmdKarpathy,
|
package/src/subagent.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// Sub-agent: chạy một runAgent() con với history độc lập, dùng chung tool
|
|
2
|
+
// runtime của cha. Hỗ trợ phân cấp (sub-agent có thể đẻ sub-agent tiếp) nhưng
|
|
3
|
+
// giới hạn độ sâu để không nổ. Hỗ trợ song song qua spawn_agents (mảng).
|
|
4
|
+
import { runAgent } from "./agent.js";
|
|
5
|
+
import { TokenMeter } from "./tokens.js";
|
|
6
|
+
|
|
7
|
+
export const MAX_SUBAGENT_DEPTH = 3;
|
|
8
|
+
|
|
9
|
+
// Tài liệu tool spawn_agent — chèn vào prompt khi agent mode bật. Mô tả cho
|
|
10
|
+
// model biết WHEN dùng (task lớn, chia được) và HOW (song song vs tuần tự).
|
|
11
|
+
export function spawnAgentToolsDoc(depth = 0) {
|
|
12
|
+
const canSpawn = depth < MAX_SUBAGENT_DEPTH;
|
|
13
|
+
return `# AGENT MODE — multi-agent
|
|
14
|
+
|
|
15
|
+
Bạn đang ở chế độ AGENT. Khi gặp task LỚN có thể chia nhỏ, hãy ủy thác cho sub-agent thay vì tự làm hết một mình:
|
|
16
|
+
|
|
17
|
+
- spawn_agent {"task": str, "context"?: str} — đẻ MỘT sub-agent làm "task". "context" là phần ngữ cảnh cần truyền (file paths, quyết định đã chốt, ràng buộc). Sub-agent có TOÀN BỘ tool (read/write/edit/run/grep/glob…) và history RIÊNG (không thấy hội thoại của bạn). Nó trả về một chuỗi tóm tắt kết quả.
|
|
18
|
+
- spawn_agents {"tasks": [{"task": str, "context"?: str}, …]} — đẻ NHIỀU sub-agent CHẠY SONG SONG. Chỉ dùng khi các task ĐỘC LẬP (không phụ thuộc kết quả của nhau). Trả về mảng tóm tắt theo đúng thứ tự tasks.
|
|
19
|
+
|
|
20
|
+
Quy tắc dùng:
|
|
21
|
+
1. TUẦN TỰ (task B cần kết quả task A): gọi spawn_agent cho A, đọc kết quả, rồi spawn_agent cho B. KHÔNG dùng spawn_agents.
|
|
22
|
+
2. SONG SONG (các task không liên quan): dùng MỘT lần spawn_agents với mảng tasks. Tiết kiệm thời gian.
|
|
23
|
+
3. PHÂN CẤP (task phức tạp): sub-agent của bạn cũng có spawn_agent, nó tự chia tiếp. Độ sâu tối đa hiện tại: ${MAX_SUBAGENT_DEPTH} (bạn đang ở depth=${depth}${canSpawn ? "" : " — đã chạm trần, KHÔNG được spawn nữa, tự làm"}).
|
|
24
|
+
4. Việc NHỎ/đơn giản: cứ tự làm, đừng spawn cho có. Spawn có overhead (mỗi sub-agent là 1 phiên model riêng → tốn token).
|
|
25
|
+
5. Sau khi gom kết quả từ sub-agent, BẠN là người tổng hợp + trả lời cuối cho user. Sub-agent không nói chuyện trực tiếp với user.
|
|
26
|
+
|
|
27
|
+
Ví dụ song song: "viết test cho 3 module độc lập" → spawn_agents với 3 tasks.
|
|
28
|
+
Ví dụ tuần tự: "thiết kế schema rồi viết migration" → spawn_agent(thiết kế) → đọc → spawn_agent(viết migration với schema đó).
|
|
29
|
+
Ví dụ phân cấp: cha giao "build full app" → đẻ 1 sub-agent "build backend" → sub-agent đó tự đẻ tiếp "viết auth", "viết CRUD" song song.`;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Chạy một sub-agent. dispatchTool: hàm để thực thi tool con (chia sẻ với cha).
|
|
33
|
+
// model: dùng chung model của cha. onLog: callback để log tiến độ ra UI cha.
|
|
34
|
+
export async function runSubAgent({ task, context, model, signal, dispatchTool, depth = 1, onLog, tokenMeter }) {
|
|
35
|
+
const sys = `Bạn là SUB-AGENT (depth=${depth}) được agent cha ủy thác MỘT nhiệm vụ cụ thể. Làm xong → trả lời NGẮN GỌN bằng Markdown tóm tắt KẾT QUẢ (file đã đụng, phát hiện, lỗi nếu có). Không tán gẫu. Không hỏi lại cha — tự quyết với thông tin được cấp.
|
|
36
|
+
|
|
37
|
+
# NHIỆM VỤ
|
|
38
|
+
${task}
|
|
39
|
+
${context ? `\n# NGỮ CẢNH TỪ CHA\n${context}` : ""}`;
|
|
40
|
+
const history = [{ role: "user", content: sys }];
|
|
41
|
+
// Dùng chung meter của cha nếu được truyền vào → token sub-agent cộng dồn
|
|
42
|
+
// vào tổng phiên. Nếu không có thì tự tạo cục bộ (giữ tương thích cũ).
|
|
43
|
+
const meter = tokenMeter || new TokenMeter();
|
|
44
|
+
const before = { input: meter.input, output: meter.output };
|
|
45
|
+
onLog?.(`↳ sub-agent (depth=${depth}) bắt đầu: ${task.slice(0, 80)}${task.length > 80 ? "…" : ""}`);
|
|
46
|
+
const result = await runAgent({
|
|
47
|
+
history,
|
|
48
|
+
model,
|
|
49
|
+
signal,
|
|
50
|
+
tokenMeter: meter,
|
|
51
|
+
extraToolsDoc: spawnAgentToolsDoc(depth),
|
|
52
|
+
onTool: (name, input) => dispatchTool(name, input, depth),
|
|
53
|
+
onStatus: () => {},
|
|
54
|
+
onDelta: () => {},
|
|
55
|
+
onSteer: () => [],
|
|
56
|
+
});
|
|
57
|
+
const used = { input: meter.input - before.input, output: meter.output - before.output };
|
|
58
|
+
onLog?.(`↳ sub-agent (depth=${depth}) xong (↑${used.input} ↓${used.output})`);
|
|
59
|
+
// Trả về string sạch để cha (model) đọc dễ. Token đã cộng vào meter rồi.
|
|
60
|
+
return result;
|
|
61
|
+
}
|
package/src/tokens.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
// Đếm token cục bộ bằng gpt-tokenizer (cl100k_base - tokenizer của GPT-4).
|
|
2
|
+
// CHÍNH XÁC cho GPT, XẤP XỈ cho Claude/Gemini/khác (sai số ~5-15%) — đủ để
|
|
3
|
+
// hiển thị mang tính tham khảo realtime trong CLI.
|
|
4
|
+
import { encode } from "gpt-tokenizer";
|
|
5
|
+
|
|
6
|
+
export function countTokens(text) {
|
|
7
|
+
if (!text) return 0;
|
|
8
|
+
try {
|
|
9
|
+
return encode(String(text)).length;
|
|
10
|
+
} catch {
|
|
11
|
+
// Fallback heuristic nếu encoder lỗi: ~4 ký tự / token.
|
|
12
|
+
return Math.ceil(String(text).length / 4);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
// Đếm token cho cả mảng messages dạng {role, content}. Cộng overhead nhỏ
|
|
17
|
+
// (~4 token/message cho role + format) để gần đúng cách provider tính prompt.
|
|
18
|
+
export function countMessages(messages = []) {
|
|
19
|
+
let n = 0;
|
|
20
|
+
for (const m of messages) {
|
|
21
|
+
n += countTokens(m?.content || "") + 4;
|
|
22
|
+
}
|
|
23
|
+
return n;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Bộ đếm cộng dồn cho 1 phiên: input (prompt gửi đi) + output (text stream về).
|
|
27
|
+
// Hỗ trợ cộng dồn theo delta để hiển thị realtime trong lúc stream.
|
|
28
|
+
export class TokenMeter {
|
|
29
|
+
constructor() {
|
|
30
|
+
this.input = 0;
|
|
31
|
+
this.output = 0;
|
|
32
|
+
this._outBuf = ""; // gom delta để đếm theo batch (đỡ tốn CPU)
|
|
33
|
+
this._outBufN = 0; // số token đã đếm từ _outBuf (để cộng dồn chính xác)
|
|
34
|
+
}
|
|
35
|
+
addInput(n) {
|
|
36
|
+
this.input += Math.max(0, n | 0);
|
|
37
|
+
}
|
|
38
|
+
// Mỗi delta text từ stream: gom vào buffer, định kỳ encode lại để cập nhật
|
|
39
|
+
// số token. Encode toàn buffer thay vì delta riêng lẻ → chính xác hơn (BPE
|
|
40
|
+
// gộp các byte qua ranh giới delta).
|
|
41
|
+
pushOutputDelta(text) {
|
|
42
|
+
if (!text) return;
|
|
43
|
+
this._outBuf += text;
|
|
44
|
+
// Re-encode toàn bộ buffer hiện tại; cập nhật delta vào this.output.
|
|
45
|
+
const total = countTokens(this._outBuf);
|
|
46
|
+
if (total > this._outBufN) {
|
|
47
|
+
this.output += total - this._outBufN;
|
|
48
|
+
this._outBufN = total;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Kết thúc một lượt output → reset buffer (bắt đầu lượt mới).
|
|
52
|
+
endOutput() {
|
|
53
|
+
this._outBuf = "";
|
|
54
|
+
this._outBufN = 0;
|
|
55
|
+
}
|
|
56
|
+
get total() {
|
|
57
|
+
return this.input + this.output;
|
|
58
|
+
}
|
|
59
|
+
// Định dạng ngắn để hiển thị ở status bar: "↑1.2k ↓340 (1.5k)".
|
|
60
|
+
format() {
|
|
61
|
+
const fmt = (n) => (n >= 1000 ? (n / 1000).toFixed(1) + "k" : String(n));
|
|
62
|
+
return `↑${fmt(this.input)} ↓${fmt(this.output)} (${fmt(this.total)})`;
|
|
63
|
+
}
|
|
64
|
+
reset() {
|
|
65
|
+
this.input = 0;
|
|
66
|
+
this.output = 0;
|
|
67
|
+
this._outBuf = "";
|
|
68
|
+
this._outBufN = 0;
|
|
69
|
+
}
|
|
70
|
+
}
|
package/src/tools.js
CHANGED
|
@@ -348,6 +348,10 @@ export function describe(name, input) {
|
|
|
348
348
|
return input.id != null ? `xem tiến trình nền #${input.id}` : "liệt kê tiến trình nền";
|
|
349
349
|
case "kill_bg":
|
|
350
350
|
return `dừng tiến trình nền #${input.id}`;
|
|
351
|
+
case "spawn_agent":
|
|
352
|
+
return `↳ sub-agent: ${String(input.task || "").slice(0, 80)}`;
|
|
353
|
+
case "spawn_agents":
|
|
354
|
+
return `↳ ${(input.tasks || []).length} sub-agent song song`;
|
|
351
355
|
default:
|
|
352
356
|
return name;
|
|
353
357
|
}
|