@noobdemon/noob-cli 1.9.1 → 1.9.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/package.json +1 -1
- package/src/agent.js +29 -1
- package/src/api.js +7 -7
package/package.json
CHANGED
package/src/agent.js
CHANGED
|
@@ -43,6 +43,7 @@ Context is finite. Don't slurp the whole repo up front. Discover information pro
|
|
|
43
43
|
• A server/service the USER wants to keep using (they asked to "run the app/server") → leave it running, and tell the user it is up: its id and the URL/port, and that they can ask you to stop it when done (you will kill_bg it). It also stops automatically when noob exits.
|
|
44
44
|
• If bg_output shows it exited with a non-zero code or an error, treat it like a failed command: read the output and fix, don't silently move on.
|
|
45
45
|
- Keep prose tight. Explain what you did and why, not how to use a tool.
|
|
46
|
+
- Do NOT call the same tool with the same input repeatedly. The runtime detects loops and will force a step change. If you need to re-read a file, use the data already in your history.
|
|
46
47
|
- JSON in the tool block must be valid: escape newlines as \\n inside string values.
|
|
47
48
|
- LANGUAGE: Always write your prose answers to the user in Vietnamese (tiếng Việt), unless the user explicitly writes in another language. Keep code, file paths, commands, and tool JSON unchanged.
|
|
48
49
|
|
|
@@ -77,6 +78,11 @@ Follow this pattern exactly. Your very first response to a task that needs the f
|
|
|
77
78
|
// 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
|
|
78
79
|
// 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.
|
|
79
80
|
const MAX_STEPS = 10000;
|
|
81
|
+
|
|
82
|
+
// Loop detection: nếu model gọi cùng 1 tool với input giống nhau liên tiếp
|
|
83
|
+
// quá ngưỡng này → coi là bị kẹt, inject thông báo để model chuyển bước.
|
|
84
|
+
const LOOP_DETECT_WINDOW = 3;
|
|
85
|
+
const LOOP_DETECT_THRESHOLD = 2;
|
|
80
86
|
const MAX_PROMPT_CHARS = 80000; // ngân sách ký tự cho phần hội thoại gửi lên model
|
|
81
87
|
// 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
|
|
82
88
|
// → giữ được "trí nhớ dài hạn" trong phiên mà không nổ context.
|
|
@@ -408,6 +414,7 @@ export async function runAgent({ history, model, signal, onTool, onStatus, onDel
|
|
|
408
414
|
// [GỠ BUDGET 2026-06-06] Không còn token budget enforcement. Agent/loop/sub-agent
|
|
409
415
|
// chạy không giới hạn token. Dừng theo: GOAL đạt, <<LOOP_DONE>>, <<ULTRA_DONE>>,
|
|
410
416
|
// model tự kết thúc reply không có tool block, hoặc user Ctrl+C.
|
|
417
|
+
const recentCalls = []; // {name, inputStr} — theo dõi vòng lặp
|
|
411
418
|
for (let step = 0; step < MAX_STEPS; step++) {
|
|
412
419
|
// 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).
|
|
413
420
|
if (step > 0 && step % 100 === 0) onStatus?.(`đã chạy ${step} bước…`);
|
|
@@ -460,6 +467,27 @@ export async function runAgent({ history, model, signal, onTool, onStatus, onDel
|
|
|
460
467
|
name: call.name,
|
|
461
468
|
content: allow ? result : t.toolDenied,
|
|
462
469
|
});
|
|
470
|
+
|
|
471
|
+
// ── Loop detection ──────────────────────────────────────────────────
|
|
472
|
+
// Theo dõi N tool call gần nhất. Nếu cùng tên + input giống nhau liên tiếp
|
|
473
|
+
// quá ngưỡng → model bị kẹt. Inject thông báo nhắc model chuyển bước,
|
|
474
|
+
// KHÔNG ngắt task (vẫn có thể Ctrl+C thủ công).
|
|
475
|
+
const inputStr = JSON.stringify(call.input || {});
|
|
476
|
+
recentCalls.push({ name: call.name, inputStr });
|
|
477
|
+
if (recentCalls.length > LOOP_DETECT_WINDOW) recentCalls.shift();
|
|
478
|
+
if (recentCalls.length >= LOOP_DETECT_THRESHOLD + 1) {
|
|
479
|
+
const lastN = recentCalls.slice(-LOOP_DETECT_THRESHOLD);
|
|
480
|
+
const allSame = lastN.every((c) => c.name === lastN[0].name && c.inputStr === lastN[0].inputStr);
|
|
481
|
+
if (allSame) {
|
|
482
|
+
// Đã lặp — inject cảnh báo, xoá calls để tránh trigger liên tục
|
|
483
|
+
recentCalls.length = 0;
|
|
484
|
+
history.push({
|
|
485
|
+
role: "tool",
|
|
486
|
+
name: "loop_detection",
|
|
487
|
+
content: `[LOOP DETECTED] Bạn vừa gọi ${lastN[0].name} ${LOOP_DETECT_THRESHOLD} lần liên tiếp với cùng input — có vẻ bạn đang bị kẹt trong vòng lặp. HÃY CHUYỂN BƯỚC: gọi tool khác, hoặc trả lời Markdown nếu đã xong. KHÔNG được gọi cùng tool cùng input lần nữa.`,
|
|
488
|
+
});
|
|
489
|
+
}
|
|
490
|
+
}
|
|
463
491
|
}
|
|
464
492
|
return t.maxSteps;
|
|
465
493
|
}
|
|
@@ -494,7 +522,7 @@ async function streamWithRetry({ model, message, system, signal, tokenMeter, onD
|
|
|
494
522
|
lastErr = err;
|
|
495
523
|
if (attempt >= MAX_RETRIES) break;
|
|
496
524
|
const backoff = Math.min(30000, 1000 * Math.pow(2, attempt));
|
|
497
|
-
onStatus?.(`
|
|
525
|
+
onStatus?.(`Lỗi kết nối — thử lại sau ${(backoff/1000)|0}s [${attempt+1}/${MAX_RETRIES}]…`);
|
|
498
526
|
await sleep(backoff, signal);
|
|
499
527
|
}
|
|
500
528
|
}
|
package/src/api.js
CHANGED
|
@@ -116,7 +116,7 @@ function hasUnclosedToolBlock(text) {
|
|
|
116
116
|
*
|
|
117
117
|
* @returns {Promise<{text:string, reasoning:string}>}
|
|
118
118
|
*/
|
|
119
|
-
export async function stream({ mode = "chat", message, model, system, conversation, effort, signal, onDelta, onReasoning, onStatus, idleMs =
|
|
119
|
+
export async function stream({ mode = "chat", message, model, system, conversation, effort, signal, onDelta, onReasoning, onStatus, idleMs = 45000, maxContinues = Infinity }) {
|
|
120
120
|
const endpoint = mode === "search" ? "/api/search" : mode === "merge" ? "/api/merge" : "/api/chat";
|
|
121
121
|
|
|
122
122
|
let fullText = "";
|
|
@@ -209,9 +209,9 @@ async function streamOnce({ endpoint, mode, message, model, system, conversation
|
|
|
209
209
|
let warnTimer = null;
|
|
210
210
|
let probeTimer = null;
|
|
211
211
|
let probeInFlight = false;
|
|
212
|
-
const WARN_MS = Math.min(
|
|
213
|
-
const PROBE_MS = Math.min(
|
|
214
|
-
const WIRE_MS = idleMs *
|
|
212
|
+
const WARN_MS = Math.min(15000, idleMs / 3);
|
|
213
|
+
const PROBE_MS = Math.min(25000, (idleMs * 2) / 3);
|
|
214
|
+
const WIRE_MS = idleMs * 3; // socket dead-cứng (mất cả heartbeat) — ngưỡng rất rộng
|
|
215
215
|
const clearContentTimers = () => {
|
|
216
216
|
clearTimeout(warnTimer); warnTimer = null;
|
|
217
217
|
clearTimeout(probeTimer); probeTimer = null;
|
|
@@ -220,7 +220,7 @@ async function streamOnce({ endpoint, mode, message, model, system, conversation
|
|
|
220
220
|
const armContent = () => {
|
|
221
221
|
clearContentTimers();
|
|
222
222
|
warnTimer = setTimeout(() => {
|
|
223
|
-
if (onStatus) onStatus("
|
|
223
|
+
if (onStatus) onStatus("Model đang suy nghĩ… (đợi thêm nếu task phức tạp)");
|
|
224
224
|
}, WARN_MS);
|
|
225
225
|
probeTimer = setTimeout(async () => {
|
|
226
226
|
if (probeInFlight || ctrl.signal.aborted) return;
|
|
@@ -234,7 +234,7 @@ async function streamOnce({ endpoint, mode, message, model, system, conversation
|
|
|
234
234
|
} catch {
|
|
235
235
|
if (!ctrl.signal.aborted) {
|
|
236
236
|
timedOut = true;
|
|
237
|
-
if (onStatus) onStatus("
|
|
237
|
+
if (onStatus) onStatus("Model không phản hồi — đang kết nối lại…");
|
|
238
238
|
ctrl.abort();
|
|
239
239
|
}
|
|
240
240
|
} finally {
|
|
@@ -328,7 +328,7 @@ async function streamOnce({ endpoint, mode, message, model, system, conversation
|
|
|
328
328
|
return { text, reasoning, truncated };
|
|
329
329
|
} catch (err) {
|
|
330
330
|
if (signal?.aborted) throw err; // người dùng bấm Ctrl+C → huỷ thật, không nối tiếp
|
|
331
|
-
if (timedOut) throw new ApiError("Kết nối tới máy chủ quá thời gian chờ (treo).", { code: "timeout" });
|
|
331
|
+
if (timedOut) throw new ApiError("Kết nối tới máy chủ quá thời gian chờ (treo). Đang thử lại…", { code: "timeout" });
|
|
332
332
|
// Rớt mạng giữa chừng (không phải huỷ, không phải treo): với chat, nếu đã có
|
|
333
333
|
// chữ thì trả phần đã nhận + cờ truncated để lớp trên nối tiếp.
|
|
334
334
|
if (mode === "chat" && text) return { text, reasoning, truncated: true };
|