@noobdemon/noob-cli 1.12.1 → 1.12.3
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 +14 -0
- package/package.json +1 -1
- package/src/agent.js +5 -0
- package/src/api.js +48 -0
- package/src/i18n.js +28 -1
- package/src/kg.js +300 -0
- package/src/prompts/system.md +57 -0
- package/src/repl/agent-dispatch.js +168 -0
- package/src/repl/permission.js +23 -11
- package/src/repl/state.js +3 -1
- package/src/repl.js +256 -137
- package/src/sessions.js +89 -0
- package/src/skills.js +59 -0
- package/src/tools.js +56 -0
- package/src/tui.js +21 -3
- package/src/ui.js +105 -85
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
// Agent tool dispatcher — xử lý spawn_agent / spawn_agents (sub-agent recursion +
|
|
2
|
+
// workflow journal cache) hoặc forward sang execTool cho các tool thường.
|
|
3
|
+
//
|
|
4
|
+
// Tách khỏi src/repl.js (v1.12.x) để:
|
|
5
|
+
// - giảm closure-coupling trong startRepl (rule noob.md "pure logic tách khỏi
|
|
6
|
+
// closure để testable")
|
|
7
|
+
// - smoke test 6 nhánh chính bằng mock thay vì E2E spawn process
|
|
8
|
+
//
|
|
9
|
+
// Pattern dùng: factory createAgentDispatcher(deps) trả về function dispatchTool.
|
|
10
|
+
// Factory được gọi MỖI turn trong handle() vì abort/printer được rebind theo turn —
|
|
11
|
+
// không cache singleton ở scope startRepl.
|
|
12
|
+
|
|
13
|
+
import chalk from 'chalk';
|
|
14
|
+
import { runSubAgent as defaultRunSubAgent, MAX_SUBAGENT_DEPTH } from '../subagent.js';
|
|
15
|
+
import { findModel as defaultFindModel } from '../models.js';
|
|
16
|
+
import * as defaultJournal from '../workflow-runs.js';
|
|
17
|
+
import { t } from '../i18n.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Tạo dispatcher cho 1 turn agent.
|
|
21
|
+
*
|
|
22
|
+
* @param {object} deps
|
|
23
|
+
* @param {object} deps.state — state object (cần state.agentMode, state.model, state.workflowRun)
|
|
24
|
+
* @param {AbortController} deps.abort — controller hiện tại của turn (đọc abort.signal)
|
|
25
|
+
* @param {object} deps.tokenMeter — TokenMeter instance, forward xuống sub-agent
|
|
26
|
+
* @param {function} deps.stopSpin — dừng spinner UI trước khi log
|
|
27
|
+
* @param {function} deps.startSpin — khởi động lại spinner sau log
|
|
28
|
+
* @param {function} deps.execTool — async (name, input) → {allow, result} cho tool thường
|
|
29
|
+
* @param {function} [deps.runSubAgent] — (chỉ dùng cho test) override sub-agent runner
|
|
30
|
+
* @param {function} [deps.findModel] — (chỉ dùng cho test) override model resolver
|
|
31
|
+
* @param {object} [deps.journal] — (chỉ dùng cho test) override workflow journal helpers
|
|
32
|
+
* @returns {function} dispatchTool(name, input, depth=0) → {allow, result}
|
|
33
|
+
*/
|
|
34
|
+
export function createAgentDispatcher(deps) {
|
|
35
|
+
const { state, abort, tokenMeter, stopSpin, startSpin, execTool } = deps;
|
|
36
|
+
// Test injection points: production luôn dùng default; smoke test pass mock.
|
|
37
|
+
const runSubAgent = deps.runSubAgent || defaultRunSubAgent;
|
|
38
|
+
const findModel = deps.findModel || defaultFindModel;
|
|
39
|
+
const j = deps.journal || defaultJournal;
|
|
40
|
+
const hashWorkflowTask = j.hashTask;
|
|
41
|
+
const lookupWorkflowTaskResult = j.lookupTaskResult;
|
|
42
|
+
const recordWorkflowTaskStart = j.recordTaskStart;
|
|
43
|
+
const recordWorkflowTaskDone = j.recordTaskDone;
|
|
44
|
+
const recordWorkflowTaskFailed = j.recordTaskFailed;
|
|
45
|
+
|
|
46
|
+
const dispatchTool = async (name, input, depth = 0) => {
|
|
47
|
+
// spawn_agent / spawn_agents chỉ được phép khi agentMode bật; depth giới hạn
|
|
48
|
+
// bởi MAX_SUBAGENT_DEPTH để tránh đệ quy nổ.
|
|
49
|
+
if (name === 'spawn_agent' || name === 'spawn_agents') {
|
|
50
|
+
if (!state.agentMode)
|
|
51
|
+
return {
|
|
52
|
+
allow: true,
|
|
53
|
+
result: 'ERROR: agent mode đang TẮT — gõ /agent on để bật trước khi spawn.',
|
|
54
|
+
};
|
|
55
|
+
if (depth >= MAX_SUBAGENT_DEPTH)
|
|
56
|
+
return {
|
|
57
|
+
allow: true,
|
|
58
|
+
result: `ERROR: đã đạt depth tối đa (${MAX_SUBAGENT_DEPTH}) — không spawn thêm.`,
|
|
59
|
+
};
|
|
60
|
+
const tasks =
|
|
61
|
+
name === 'spawn_agent' ? [input] : Array.isArray(input?.agents) ? input.agents : [];
|
|
62
|
+
if (!tasks.length) return { allow: true, result: 'ERROR: thiếu task cho sub-agent.' };
|
|
63
|
+
stopSpin();
|
|
64
|
+
console.log(
|
|
65
|
+
chalk.hex('#8b5cf6')(
|
|
66
|
+
` ⊕ spawn ${tasks.length} sub-agent (depth ${depth + 1}/${MAX_SUBAGENT_DEPTH})`
|
|
67
|
+
)
|
|
68
|
+
);
|
|
69
|
+
startSpin(t.thinking);
|
|
70
|
+
try {
|
|
71
|
+
const runData = state.workflowRun?.data || null;
|
|
72
|
+
const results = await Promise.all(
|
|
73
|
+
tasks.map((task, i) => {
|
|
74
|
+
// Per-sub-agent model routing: task.model có thể là id model hoặc tên thân thiện.
|
|
75
|
+
// findModel() resolve cả hai; nếu không match thì fallback model của cha.
|
|
76
|
+
let subModel = state.model.id;
|
|
77
|
+
let modelTag = '';
|
|
78
|
+
if (task?.model) {
|
|
79
|
+
const m = findModel(task.model);
|
|
80
|
+
if (m) {
|
|
81
|
+
subModel = m.id;
|
|
82
|
+
modelTag = ` [${m.name}]`;
|
|
83
|
+
} else
|
|
84
|
+
modelTag = ` [model "${task.model}" không khớp — dùng ${state.model.name}]`;
|
|
85
|
+
}
|
|
86
|
+
const taskBody = task?.task || task?.prompt || '';
|
|
87
|
+
const taskCtx = task?.context || '';
|
|
88
|
+
// Workflow journal: nếu đang trong run + task đã done lần trước → return
|
|
89
|
+
// cached result, tiết kiệm token. Hash = crc32(task+ctx+model).
|
|
90
|
+
if (runData) {
|
|
91
|
+
const hash = hashWorkflowTask({ task: taskBody, context: taskCtx, model: subModel });
|
|
92
|
+
const cached = lookupWorkflowTaskResult(runData, hash);
|
|
93
|
+
if (cached !== null) {
|
|
94
|
+
stopSpin();
|
|
95
|
+
console.log(
|
|
96
|
+
chalk.hex('#8b5cf6')(
|
|
97
|
+
` ⊘ sub-agent #${i + 1}${modelTag} skip — đã done trong run trước (cached)`
|
|
98
|
+
)
|
|
99
|
+
);
|
|
100
|
+
startSpin(t.thinking);
|
|
101
|
+
return Promise.resolve(
|
|
102
|
+
`── sub-agent #${i + 1}${modelTag} (cached) ──\n${cached}`
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
recordWorkflowTaskStart(runData, {
|
|
106
|
+
hash,
|
|
107
|
+
task: taskBody,
|
|
108
|
+
context: taskCtx,
|
|
109
|
+
model: subModel,
|
|
110
|
+
});
|
|
111
|
+
// [GỠ BUDGET 2026-06-06] Sub-agent chạy không giới hạn token.
|
|
112
|
+
return runSubAgent({
|
|
113
|
+
task: taskBody,
|
|
114
|
+
context: taskCtx,
|
|
115
|
+
depth: depth + 1,
|
|
116
|
+
model: subModel,
|
|
117
|
+
signal: abort.signal,
|
|
118
|
+
tokenMeter,
|
|
119
|
+
dispatchTool: (n, inp) => dispatchTool(n, inp, depth + 1),
|
|
120
|
+
onLog: (msg) => {
|
|
121
|
+
stopSpin();
|
|
122
|
+
console.log(chalk.hex('#8b5cf6')(' ' + msg + modelTag));
|
|
123
|
+
startSpin(t.thinking);
|
|
124
|
+
},
|
|
125
|
+
})
|
|
126
|
+
.then((r) => {
|
|
127
|
+
recordWorkflowTaskDone(runData, hash, r);
|
|
128
|
+
return `── sub-agent #${i + 1}${modelTag} ──\n${r}`;
|
|
129
|
+
})
|
|
130
|
+
.catch((e) => {
|
|
131
|
+
recordWorkflowTaskFailed(runData, hash, e);
|
|
132
|
+
return `── sub-agent #${i + 1}${modelTag} (LỖI) ──\n${e?.message || String(e)}`;
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
// Không có active workflow run → behavior cũ.
|
|
136
|
+
return runSubAgent({
|
|
137
|
+
task: taskBody,
|
|
138
|
+
context: taskCtx,
|
|
139
|
+
depth: depth + 1,
|
|
140
|
+
model: subModel,
|
|
141
|
+
signal: abort.signal,
|
|
142
|
+
tokenMeter,
|
|
143
|
+
dispatchTool: (n, inp) => dispatchTool(n, inp, depth + 1),
|
|
144
|
+
onLog: (msg) => {
|
|
145
|
+
stopSpin();
|
|
146
|
+
console.log(chalk.hex('#8b5cf6')(' ' + msg + modelTag));
|
|
147
|
+
startSpin(t.thinking);
|
|
148
|
+
},
|
|
149
|
+
})
|
|
150
|
+
.then((r) => `── sub-agent #${i + 1}${modelTag} ──\n${r}`)
|
|
151
|
+
.catch(
|
|
152
|
+
(e) => `── sub-agent #${i + 1}${modelTag} (LỖI) ──\n${e?.message || String(e)}`
|
|
153
|
+
);
|
|
154
|
+
})
|
|
155
|
+
);
|
|
156
|
+
return { allow: true, result: results.join('\n\n') };
|
|
157
|
+
} catch (err) {
|
|
158
|
+
return { allow: true, result: 'ERROR sub-agent: ' + (err?.message || String(err)) };
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
stopSpin();
|
|
162
|
+
const res = await execTool(name, input);
|
|
163
|
+
startSpin(t.thinking);
|
|
164
|
+
return res;
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
return dispatchTool;
|
|
168
|
+
}
|
package/src/repl/permission.js
CHANGED
|
@@ -20,30 +20,42 @@
|
|
|
20
20
|
// - truncate: helper cắt chuỗi dài.
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
|
-
* Hỏi quyền chung cho 1 tool call. Trả về 'y' | 'n' | 'a'.
|
|
24
|
-
*
|
|
23
|
+
* Hỏi quyền chung cho 1 tool call. Trả về 'y' | 'n' | 'a' | 't' | 'f'.
|
|
24
|
+
* 'y' = đồng ý lần này
|
|
25
|
+
* 'n' = từ chối
|
|
26
|
+
* 'a' = always — auto-approve tool này tới hết phiên
|
|
27
|
+
* 't' = this turn — auto-approve tool này tới hết turn hiện tại
|
|
28
|
+
* 'f' = this file — auto-approve mọi tool destructive trên path này tới hết phiên
|
|
29
|
+
* (chỉ hiện khi có `targetPath`, vd edit_file/write_file)
|
|
30
|
+
*
|
|
31
|
+
* `targetPath` (optional) = path file đang bị thao tác — nếu có, hiện kèm option `f`.
|
|
25
32
|
*/
|
|
26
|
-
export async function askPermission(name, { tui, ask, pending, c, t, truncate }) {
|
|
33
|
+
export async function askPermission(name, { tui, ask, pending, c, t, truncate, targetPath } = {}) {
|
|
27
34
|
tui.setBusy(false);
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
35
|
+
const hasFile = typeof targetPath === 'string' && targetPath.length > 0;
|
|
36
|
+
const headerHint = hasFile
|
|
37
|
+
? ' — y (đồng ý) / n (từ chối) / a (luôn cho phép) / t (turn này) / f (file này)'
|
|
38
|
+
: ' — y (đồng ý) / n (từ chối) / a (luôn cho phép) / t (turn này)';
|
|
39
|
+
console.log(c.tool(' ⏸ Cần quyền: ' + name) + c.dim(headerHint));
|
|
40
|
+
if (hasFile) console.log(c.dim(' file: ' + targetPath));
|
|
41
|
+
const promptHint = hasFile
|
|
42
|
+
? '[y] có / [n] không / [a] luôn ' + name + ' / [t] hết turn / [f] file này › '
|
|
43
|
+
: '[y] có / [n] không / [a] luôn ' + name + ' / [t] hết turn › ';
|
|
32
44
|
try {
|
|
33
45
|
while (true) {
|
|
34
|
-
const raw = await ask(
|
|
35
|
-
c.tool(' cho phép? ') + c.dim('[y] có / [n] không / [a] luôn ' + name + ' › ')
|
|
36
|
-
);
|
|
46
|
+
const raw = await ask(c.tool(' cho phép? ') + c.dim(promptHint));
|
|
37
47
|
if (raw == null) return 'n';
|
|
38
48
|
const a = raw.trim().toLowerCase();
|
|
39
49
|
if (a === '' || a === 'y' || a === 'yes' || a === 'có') return 'y';
|
|
40
50
|
if (a === 'n' || a === 'no' || a === 'không') return 'n';
|
|
41
51
|
if (a === 'a' || a === 'always' || a === 'luôn') return 'a';
|
|
52
|
+
if (a === 't' || a === 'turn') return 't';
|
|
53
|
+
if (hasFile && (a === 'f' || a === 'file')) return 'f';
|
|
42
54
|
if (raw.trim().length > 3) {
|
|
43
55
|
pending.push(raw);
|
|
44
56
|
console.log(c.dim(' ' + t.queued(pending.length, truncate(raw, 60))));
|
|
45
57
|
}
|
|
46
|
-
console.log(c.dim(' ' + t.permRetry));
|
|
58
|
+
console.log(c.dim(' ' + (t.permRetryExtended || t.permRetry)));
|
|
47
59
|
}
|
|
48
60
|
} finally {
|
|
49
61
|
tui.setBusy(true, t.thinking);
|
package/src/repl/state.js
CHANGED
|
@@ -19,7 +19,9 @@ export function createState(opts = {}, config) {
|
|
|
19
19
|
model: findModel(opts.model) || findModel(config.model) || findModel(DEFAULT_MODEL),
|
|
20
20
|
mode: 'chat', // chat | merge | search
|
|
21
21
|
history: [],
|
|
22
|
-
autoApprove: new Set(),
|
|
22
|
+
autoApprove: new Set(), // tool name → 'a' (always, phiên)
|
|
23
|
+
autoApproveTurn: new Set(), // tool name → 't' (this turn, reset sau mỗi runAgent)
|
|
24
|
+
autoApproveFile: new Set(), // 'name:absPath' → 'f' (this file, phiên)
|
|
23
25
|
yolo: !!opts.yolo || config.yoloDefault,
|
|
24
26
|
ultra: false, // /ultra chế độ tự hành đang chạy
|
|
25
27
|
agentMode: false, // /agent on → cho phép spawn_agent / spawn_agents
|