@noobdemon/noob-cli 1.12.5 → 1.12.7
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 +8 -0
- package/package.json +1 -1
- package/src/agent.js +11 -8
- package/src/repl/agent-dispatch.js +52 -1
- package/src/repl.js +23 -45
- package/src/tokens.js +6 -2
- package/src/tools.js +16 -0
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
Tất cả thay đổi đáng kể của `@noobdemon/noob-cli` được ghi vào file này.
|
|
4
4
|
|
|
5
|
+
## [1.12.7] - 2026-06-12
|
|
6
|
+
|
|
7
|
+
### Changed
|
|
8
|
+
- **Gỡ auto-compact, chuyển sang MANUAL only** (`src/repl.js` + `src/tokens.js` + `src/agent.js`): trước đây CLI tự gọi `maybeSummarize({force:true})` khi context đạt 75% — gián đoạn workflow giữa chừng và summary có thể mất chi tiết user cần. Giờ user toàn quyền quyết định khi nào tóm tắt bằng `/compact`. Chỉ còn 2 mốc CẢNH BÁO (không auto-action): **60% (120k tokens)** nhắc nhẹ một lần, **80% (160k tokens)** cảnh báo mạnh gợi ý gõ `/compact` trước khi provider reject ở ~200k. Đồng thời sửa bug `CONTEXT_WINDOW=2_000_000` → `200_000` (khớp Claude Opus 4.7 + GPT-4o); ngưỡng cũ 2M khiến 75% = 1.5M token không bao giờ chạm → user báo `/compact không hoạt động`. `SUMMARIZE_THRESHOLD_CHARS` 6M → 600k, `MAX_PROMPT_CHARS` 1.2M → 800k, `keepTail` 16/24 → 12/16 cho khớp window thực.
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- **Tool `write_todos`** (`src/repl/agent-dispatch.js` + `src/tools.js` + `src/agent.js`): tool ẢO để model declare structured todo list thay vì viết markdown `- [ ]`. Shape `{todos: [{text, done}]}` — REPLACE toàn bộ list mỗi lần gọi (no patch). Dispatcher intercept TRƯỚC `execTool`: set `state.todos` + `tui.setTodos` trực tiếp, set flag `state._todosFromTool=true` để `repl.js` skip parse markdown sau turn (tránh overwrite structured state). In compact box lần đầu, diff (chỉ dòng đổi) các lần sau. SYSTEM prompt rule TODO-BASED EXECUTION đã update: model PHẢI dùng `write_todos`, không viết markdown. Lý do: parser markdown cũ (`parseTodosFromHistory`) fragile khi model format sai (sai indent, dùng `*` thay `-`, thiếu space). Structured tool call → CLI render luôn đúng, progress bar trên status line cập nhật ngay. Stub trong `TOOLS.write_todos` làm fail-safe nếu lỡ qua `runTool` trực tiếp. Smoke `scripts/smoke-write-todos.mjs` 27/27 pass + regression `smoke-dispatch.mjs` 23/23 pass.
|
|
12
|
+
|
|
5
13
|
## [1.12.5] - 2026-06-12
|
|
6
14
|
|
|
7
15
|
### Added
|
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -29,12 +29,13 @@ Available tools (each is self-contained; pick the SMALLEST tool that answers the
|
|
|
29
29
|
- run_command {"command": str, "timeout"?: int, "background"?: bool} — run a shell command in the cwd. Foreground commands are killed after ~60s (override with "timeout" ms). For long-running processes — dev servers, watchers, \`python -m http.server\`, \`npm run dev\`, \`flask run\` — set "background": true: starts the process, returns immediately, keeps running WITHOUT blocking next steps. Never start a server in the foreground (it will hang then be killed).
|
|
30
30
|
- bg_output {"id"?: int} — no id: list background processes + status; with id: show that process's captured output so far (poll after starting a server to confirm it came up).
|
|
31
31
|
- kill_bg {"id": int} — stop a background process started with run_command background:true.
|
|
32
|
+
- write_todos {"todos": [{"text": str, "done": bool}, ...]} — declare/update structured TODO list. REPLACES the entire list every call (no patching individual items). To check an item off, resend the FULL list with done:true on that item. Use this INSTEAD of writing markdown \`- [ ]\` lines: the CLI renders it as a progress bar on the status line AND prints a compact box, no fragile markdown parsing. Call ONCE at the start of any multi-step task with all items done:false, then call AGAIN after each step with the just-finished item flipped to done:true.
|
|
32
33
|
|
|
33
34
|
# Retrieval strategy (just-in-time, not bulk)
|
|
34
35
|
Context is finite. Don't slurp the whole repo up front. Discover information progressively: list_dir/glob to map → grep to locate → read_file (with offset+limit for big files) to inspect only what matters. Each tool result spends your attention budget — make every call earn it. When a tool returns a huge blob, extract the few facts you need, then move on; don't re-read it later (the result stays in history).
|
|
35
36
|
|
|
36
37
|
# Rules
|
|
37
|
-
- TODO-BASED EXECUTION: For multi-step
|
|
38
|
+
- TODO-BASED EXECUTION: For any multi-step task (3+ actions), you MUST call \`write_todos\` FIRST with all items done:false, then call it AGAIN after every completed step with that item flipped to done:true (resend the full list). NEVER write markdown \`- [ ]\` lines — the runtime parses \`write_todos\` calls, not markdown. Your response is NOT finished until all items are done:true. The ONLY valid reason to stop is: (a) all items done, or (b) you are WAITING for a user reply. If you just got a tool result, you MUST continue — do NOT output a summary, do NOT ask "what next", do NOT stop. After write_file/edit_file returns, call write_todos to tick the just-finished item, then immediately start the next.
|
|
38
39
|
- GROUND TRUTH = real TOOL RESULTs in this conversation, not your memory or what you intended to do. A file changed only if a write_file/edit_file result confirms it (see the FILES CHANGED list). A test passed / build succeeded / command worked only if a run_command result above shows it. Never narrate outcomes you didn't observe; if you haven't checked, say so and check now (read_file / list_dir / run the command). Before any "done/summary" reply, reconcile every file and result you're about to claim against the actual tool results above — if it isn't there, you didn't do it yet.
|
|
39
40
|
- VERIFY BEFORE DISMISSING: never declare a TOOL RESULT "fake", "spurious", "injected", "unrelated", or "from a previous turn" without first verifying with a fresh tool call. If a result looks off (unexpected content, output you didn't ask for, weird command), your DEFAULT is: treat it as REAL runtime output, then run a small verification (read_file the affected path, grep for the symbol, list_dir, re-run the command) to confirm actual state. Only after the verification tool result contradicts the suspicious one may you call it stale/leftover — and even then, work from the FRESH result, never from your guess. Trusting your own skepticism over the runtime is the same over-confidence bug as hallucinating success: both substitute memory for evidence.
|
|
40
41
|
- Investigate before editing: read the relevant files first; never invent file contents.
|
|
@@ -145,10 +146,12 @@ const MAX_STEPS = 10000;
|
|
|
145
146
|
// loop detection cũ bằng cách xen kẽ 2-3 tool call khác nhau.
|
|
146
147
|
const LOOP_DETECT_WINDOW = 6;
|
|
147
148
|
const LOOP_DETECT_THRESHOLD = 2;
|
|
148
|
-
const MAX_PROMPT_CHARS =
|
|
149
|
+
const MAX_PROMPT_CHARS = 800000; // ~200k tokens (ngang CONTEXT_WINDOW) — compact() là safety net cuối, repl.js auto-compact ở 75% (150k token) chạy trước.
|
|
149
150
|
// 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
|
|
150
151
|
// → giữ được "trí nhớ dài hạn" trong phiên mà không nổ context.
|
|
151
|
-
|
|
152
|
+
// 600k chars ≈ 150k tokens = trùng ngưỡng auto-compact 75% của repl.js. Khi
|
|
153
|
+
// /compact thủ công hoặc auto-compact gọi với force=true thì ngưỡng này bị bypass.
|
|
154
|
+
const SUMMARIZE_THRESHOLD_CHARS = 600000;
|
|
152
155
|
|
|
153
156
|
// HARD GOAL block (do /goal <text> set): chèn ngay sau memoryBlock, attention
|
|
154
157
|
// cao. Mục đích — chống 3 failure mode bài "dynamic workflows" của Anthropic
|
|
@@ -261,11 +264,11 @@ export async function maybeSummarize(history, { model, signal, force = false } =
|
|
|
261
264
|
const totalChars = history.reduce((s, m) => s + (m.content?.length || 0), 0);
|
|
262
265
|
if (!force && totalChars < SUMMARIZE_THRESHOLD_CHARS) return false;
|
|
263
266
|
// Giữ tail nguyên vẹn; tóm tắt phần trước.
|
|
264
|
-
// Với CONTEXT_WINDOW =
|
|
265
|
-
// gần nhất (
|
|
266
|
-
// force (gọi từ /compact hoặc auto-compact 75%): giữ
|
|
267
|
-
// non-force: giữ
|
|
268
|
-
const keepTail = force ?
|
|
267
|
+
// Với CONTEXT_WINDOW = 200k tokens, tail cần đủ để giữ vài lượt tool result
|
|
268
|
+
// gần nhất (chuỗi edit_file + run_command đang dở).
|
|
269
|
+
// force (gọi từ /compact hoặc auto-compact 75%): giữ 12 tail.
|
|
270
|
+
// non-force: giữ 16 tail (rộng tay hơn vì phiên dài mới trigger).
|
|
271
|
+
const keepTail = force ? 12 : 16;
|
|
269
272
|
if (history.length <= keepTail + 2) return false;
|
|
270
273
|
// Nếu lượt đầu đã là summary (role=system, name=summary) → tóm tắt thêm.
|
|
271
274
|
const head = history.slice(0, history.length - keepTail);
|
|
@@ -32,7 +32,7 @@ import { t } from '../i18n.js';
|
|
|
32
32
|
* @returns {function} dispatchTool(name, input, depth=0) → {allow, result}
|
|
33
33
|
*/
|
|
34
34
|
export function createAgentDispatcher(deps) {
|
|
35
|
-
const { state, abort, tokenMeter, stopSpin, startSpin, execTool } = deps;
|
|
35
|
+
const { state, abort, tokenMeter, stopSpin, startSpin, execTool, tui, c } = deps;
|
|
36
36
|
// Test injection points: production luôn dùng default; smoke test pass mock.
|
|
37
37
|
const runSubAgent = deps.runSubAgent || defaultRunSubAgent;
|
|
38
38
|
const findModel = deps.findModel || defaultFindModel;
|
|
@@ -44,6 +44,57 @@ export function createAgentDispatcher(deps) {
|
|
|
44
44
|
const recordWorkflowTaskFailed = j.recordTaskFailed;
|
|
45
45
|
|
|
46
46
|
const dispatchTool = async (name, input, depth = 0) => {
|
|
47
|
+
// write_todos: tool ẢO cập nhật state.todos + TUI trực tiếp. Không qua execTool
|
|
48
|
+
// vì không phải fs/shell — chỉ là cách model declare structured todo list thay
|
|
49
|
+
// vì viết markdown `- [ ]` (parser markdown fragile khi format sai). Mỗi lần
|
|
50
|
+
// gọi REPLACE toàn bộ list (không patch từng item — model gửi lại full list
|
|
51
|
+
// với done:true cho item vừa xong). State.todos được set NGAY → TUI render
|
|
52
|
+
// chính xác, không cần parse history.
|
|
53
|
+
if (name === 'write_todos') {
|
|
54
|
+
const todosIn = Array.isArray(input?.todos) ? input.todos : null;
|
|
55
|
+
if (!todosIn)
|
|
56
|
+
return { allow: true, result: 'ERROR: write_todos cần field "todos": [{text, done}].' };
|
|
57
|
+
const todos = todosIn
|
|
58
|
+
.filter((it) => it && typeof it.text === 'string' && it.text.trim())
|
|
59
|
+
.map((it) => ({ text: String(it.text).trim(), done: !!it.done }));
|
|
60
|
+
if (!todos.length)
|
|
61
|
+
return { allow: true, result: 'ERROR: todos rỗng — gửi ít nhất 1 item {text, done}.' };
|
|
62
|
+
const prev = Array.isArray(state.todos) ? state.todos : [];
|
|
63
|
+
const prevByText = new Map(prev.map((p) => [p.text, !!p.done]));
|
|
64
|
+
state.todos = todos;
|
|
65
|
+
// Flag: lượt này model đã dùng write_todos → repl skip parse markdown để
|
|
66
|
+
// không overwrite structured state bằng parser fragile. Reset đầu mỗi turn.
|
|
67
|
+
state._todosFromTool = true;
|
|
68
|
+
try { tui?.setTodos?.(todos); } catch {}
|
|
69
|
+
const done = todos.filter((t) => t.done).length;
|
|
70
|
+
// In compact: lần đầu (prev rỗng) hoặc list thay đổi tập text → in full.
|
|
71
|
+
// Nếu cùng tập text + chỉ khác trạng thái done → in diff (dòng vừa toggle).
|
|
72
|
+
const sameSet = prev.length === todos.length && todos.every((t) => prevByText.has(t.text));
|
|
73
|
+
stopSpin?.();
|
|
74
|
+
if (!sameSet) {
|
|
75
|
+
const lines = todos.map((t) => ' ' + (t.done ? '✓ ' : '☐ ') + t.text);
|
|
76
|
+
console.log((c?.tool || ((s) => s))(` 📋 todo (${done}/${todos.length})`));
|
|
77
|
+
console.log(lines.join('\n'));
|
|
78
|
+
} else {
|
|
79
|
+
// diff: in dòng có done thay đổi (cả false→true lẫn true→false).
|
|
80
|
+
const changes = todos.filter((t) => prevByText.get(t.text) !== t.done);
|
|
81
|
+
if (changes.length === 0) {
|
|
82
|
+
console.log((c?.dim || ((s) => s))(` 📋 todo (${done}/${todos.length}) · không đổi`));
|
|
83
|
+
} else {
|
|
84
|
+
console.log((c?.tool || ((s) => s))(` 📋 todo (${done}/${todos.length})`));
|
|
85
|
+
for (const ch of changes) {
|
|
86
|
+
const mark = ch.done ? '✓' : '☐';
|
|
87
|
+
console.log(' ' + mark + ' ' + ch.text);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
startSpin?.();
|
|
92
|
+
return {
|
|
93
|
+
allow: true,
|
|
94
|
+
result: `Đã cập nhật ${todos.length} todo (${done} xong, ${todos.length - done} còn lại). Tiếp tục item chưa done; nếu tất cả done, kết thúc trả lời.`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
47
98
|
// spawn_agent / spawn_agents chỉ được phép khi agentMode bật; depth giới hạn
|
|
48
99
|
// bởi MAX_SUBAGENT_DEPTH để tránh đệ quy nổ.
|
|
49
100
|
if (name === 'spawn_agent' || name === 'spawn_agents') {
|
package/src/repl.js
CHANGED
|
@@ -1418,7 +1418,7 @@ NGUYÊN TẮC:
|
|
|
1418
1418
|
// src/repl/agent-dispatch.js (v1.12.x). Factory được gọi MỖI turn vì abort
|
|
1419
1419
|
// được rebind trong handle() — không cache.
|
|
1420
1420
|
const dispatchTool = createAgentDispatcher({
|
|
1421
|
-
state, abort, tokenMeter, stopSpin, startSpin, execTool,
|
|
1421
|
+
state, abort, tokenMeter, stopSpin, startSpin, execTool, tui, c,
|
|
1422
1422
|
});
|
|
1423
1423
|
|
|
1424
1424
|
const answer = await runAgent({
|
|
@@ -1461,8 +1461,15 @@ NGUYÊN TẮC:
|
|
|
1461
1461
|
printAnswer(answer, state.model.name, providerColor(state.model.provider));
|
|
1462
1462
|
|
|
1463
1463
|
// Parse todo từ model output → render trên status bar.
|
|
1464
|
-
state.
|
|
1465
|
-
|
|
1464
|
+
// Nếu lượt này model đã gọi write_todos (state._todosFromTool = true),
|
|
1465
|
+
// state.todos + TUI đã được set trực tiếp trong dispatcher — SKIP parse
|
|
1466
|
+
// markdown để không overwrite structured state bằng parser fragile.
|
|
1467
|
+
if (state._todosFromTool) {
|
|
1468
|
+
state._todosFromTool = false; // reset cho turn sau
|
|
1469
|
+
} else {
|
|
1470
|
+
state.todos = parseTodosFromHistory(state.history);
|
|
1471
|
+
tui.setTodos(state.todos);
|
|
1472
|
+
}
|
|
1466
1473
|
return answer; // vòng ULTRA cần text này để dò token hoàn thành
|
|
1467
1474
|
} catch (err) {
|
|
1468
1475
|
stopSpin();
|
|
@@ -1474,53 +1481,24 @@ NGUYÊN TẮC:
|
|
|
1474
1481
|
// Reset turn-scoped auto-approve — chỉ áp dụng trong runAgent vừa rồi.
|
|
1475
1482
|
// (autoApprove + autoApproveFile vẫn giữ nguyên cho phiên.)
|
|
1476
1483
|
state.autoApproveTurn.clear();
|
|
1477
|
-
//
|
|
1478
|
-
//
|
|
1479
|
-
//
|
|
1480
|
-
//
|
|
1481
|
-
//
|
|
1482
|
-
//
|
|
1483
|
-
//
|
|
1484
|
+
// [2026-06-12] GỠ AUTO-COMPACT — user kiểm soát compact thủ công bằng /compact.
|
|
1485
|
+
// Lý do: auto-compact gián đoạn workflow giữa chừng, summary có thể mất chi
|
|
1486
|
+
// tiết user cần. Giữ 2 mốc CẢNH BÁO (60% / 80%) để user biết khi nào nên
|
|
1487
|
+
// chạy /compact, nhưng KHÔNG tự động chạy nữa.
|
|
1488
|
+
// Với CONTEXT_WINDOW = 200k tokens:
|
|
1489
|
+
// 60% (120k) → nhắc nhẹ một lần
|
|
1490
|
+
// 80% (160k) → cảnh báo mạnh — nên /compact ngay trước khi provider reject
|
|
1484
1491
|
try {
|
|
1485
1492
|
const totalTokens = countMessages(state.history);
|
|
1486
1493
|
const k = Math.round(totalTokens / 1000);
|
|
1487
1494
|
const pct = Math.round((totalTokens / CONTEXT_WINDOW) * 100);
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
console.log(c.accent(` ⚡ ${t.autoCompactTrigger(k)} (${pct}% context)`));
|
|
1492
|
-
tui.setBusy(true, t.compactRunning);
|
|
1493
|
-
try {
|
|
1494
|
-
const ok = await maybeSummarize(state.history, { model: state.model, force: true });
|
|
1495
|
-
tui.setBusy(false);
|
|
1496
|
-
if (ok) {
|
|
1497
|
-
const afterTokens = countMessages(state.history);
|
|
1498
|
-
const aK = Math.round(afterTokens / 1000);
|
|
1499
|
-
const saved =
|
|
1500
|
-
totalTokens > 0 ? Math.round(((totalTokens - afterTokens) / totalTokens) * 100) : 0;
|
|
1501
|
-
console.log(
|
|
1502
|
-
c.ok(
|
|
1503
|
-
` ${t.autoCompactDone(k, aK, saved)} (${Math.round((afterTokens / CONTEXT_WINDOW) * 100)}% context)`
|
|
1504
|
-
)
|
|
1505
|
-
);
|
|
1506
|
-
state._longSessionWarned = false;
|
|
1507
|
-
persist();
|
|
1508
|
-
} else {
|
|
1509
|
-
console.log(c.err(' ' + t.autoCompactFail));
|
|
1510
|
-
}
|
|
1511
|
-
} catch (e) {
|
|
1512
|
-
tui.setBusy(false);
|
|
1513
|
-
console.log(c.err(' ' + t.autoCompactFail));
|
|
1514
|
-
} finally {
|
|
1515
|
-
state._autoCompacting = false;
|
|
1516
|
-
}
|
|
1517
|
-
} else if (totalTokens >= CONTEXT_WINDOW * 0.6) {
|
|
1518
|
-
// Mốc 2 (≥60% — 1.2M tokens): cảnh báo mạnh.
|
|
1519
|
-
console.log(c.err(` ⚠ ${t.veryLongSession(k)} (${pct}% context)`));
|
|
1495
|
+
if (totalTokens >= CONTEXT_WINDOW * 0.8) {
|
|
1496
|
+
// Mốc 2 (≥80% — 160k tokens): cảnh báo mạnh, gợi ý /compact ngay.
|
|
1497
|
+
console.log(c.err(` ⚠ ${t.veryLongSession(k)} (${pct}% context) — gõ /compact để tóm tắt, tránh provider reject ở ~200k.`));
|
|
1520
1498
|
state._longSessionWarned = true;
|
|
1521
|
-
} else if (totalTokens >= CONTEXT_WINDOW * 0.
|
|
1522
|
-
// Mốc 1 (≥
|
|
1523
|
-
console.log(c.dim(` ⓘ ${t.longSession(k)} (${pct}% context)
|
|
1499
|
+
} else if (totalTokens >= CONTEXT_WINDOW * 0.6 && !state._longSessionWarned) {
|
|
1500
|
+
// Mốc 1 (≥60% — 120k tokens): nhắc nhẹ một lần.
|
|
1501
|
+
console.log(c.dim(` ⓘ ${t.longSession(k)} (${pct}% context) — cân nhắc /compact nếu phiên còn dài.`));
|
|
1524
1502
|
state._longSessionWarned = true;
|
|
1525
1503
|
}
|
|
1526
1504
|
} catch {}
|
package/src/tokens.js
CHANGED
|
@@ -57,8 +57,12 @@ 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 tối đa của model
|
|
61
|
-
|
|
60
|
+
// Context window tối đa của model. Đặt 200k tokens — match Claude 3.5/Opus 4,
|
|
61
|
+
// GPT-4o, và an toàn cho mọi model phổ biến qua gateway (Gemini 1M, DeepSeek
|
|
62
|
+
// 128k, Grok 128k...). Đặt cao hơn 200k là vô nghĩa: provider sẽ reject prompt
|
|
63
|
+
// TRƯỚC khi auto-compact của repl.js có cơ hội trigger → user thấy 'compact
|
|
64
|
+
// không hoạt động' dù logic compact vẫn đúng.
|
|
65
|
+
export const CONTEXT_WINDOW = 200_000;
|
|
62
66
|
|
|
63
67
|
export class TokenMeter {
|
|
64
68
|
constructor() {
|
package/src/tools.js
CHANGED
|
@@ -519,6 +519,17 @@ export const TOOLS = {
|
|
|
519
519
|
return `Killed background process #${id} (${p.command}).`;
|
|
520
520
|
},
|
|
521
521
|
|
|
522
|
+
// write_todos là tool ẢO: dispatcher (src/repl/agent-dispatch.js) intercept TRƯỚC
|
|
523
|
+
// khi vào execTool, nên stub này thường không chạy. Giữ để fail-safe: nếu có
|
|
524
|
+
// code path nào lỡ gọi runTool('write_todos', ...) trực tiếp, ít nhất trả OK
|
|
525
|
+
// thay vì ném Unknown tool — và stub trả lỗi rõ hướng dẫn fix.
|
|
526
|
+
async write_todos({ todos }, { signal } = {}) {
|
|
527
|
+
if (signal?.aborted) throw new Error('aborted');
|
|
528
|
+
if (!Array.isArray(todos)) throw new Error('write_todos: todos phải là mảng');
|
|
529
|
+
const done = todos.filter((it) => it && it.done).length;
|
|
530
|
+
return `(stub) write_todos nhận ${todos.length} item (${done} done) — dispatcher đáng lẽ phải intercept trước; nếu thấy dòng này, báo bug.`;
|
|
531
|
+
},
|
|
532
|
+
|
|
522
533
|
// Knowledge graph tools — KHÔNG xin permission (user chọn tự do).
|
|
523
534
|
// Storage: <cwd>/.noob/kg.jsonl. Logic ở src/kg.js.
|
|
524
535
|
async kg_search({ query }, { signal } = {}) {
|
|
@@ -679,6 +690,11 @@ export function describe(name, input) {
|
|
|
679
690
|
return `↳ sub-agent: ${String(input.task || '').slice(0, 80)}`;
|
|
680
691
|
case 'spawn_agents':
|
|
681
692
|
return `↳ ${(input.tasks || []).length} sub-agent song song`;
|
|
693
|
+
case 'write_todos': {
|
|
694
|
+
const items = Array.isArray(input?.todos) ? input.todos : [];
|
|
695
|
+
const done = items.filter((it) => it && it.done).length;
|
|
696
|
+
return `todo ${done}/${items.length}`;
|
|
697
|
+
}
|
|
682
698
|
case 'kg_search':
|
|
683
699
|
return `kg search "${input.query || ''}"`;
|
|
684
700
|
case 'kg_add':
|