@noobdemon/noob-cli 1.10.14 → 1.10.15

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/agent.js +41 -19
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@noobdemon/noob-cli",
3
- "version": "1.10.14",
3
+ "version": "1.10.15",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/src/agent.js CHANGED
@@ -144,7 +144,9 @@ const MAX_STEPS = 10000;
144
144
 
145
145
  // Loop detection: nếu model gọi cùng 1 tool với input giống nhau liên tiếp
146
146
  // quá ngưỡng này → coi là bị kẹt, inject thông báo để model chuyển bước.
147
- const LOOP_DETECT_WINDOW = 3;
147
+ // Cũng phát hiện pattern vòng lặp (A-B-A-B, A-B-C-A-B-C) — model hay thoát
148
+ // loop detection cũ bằng cách xen kẽ 2-3 tool call khác nhau.
149
+ const LOOP_DETECT_WINDOW = 6;
148
150
  const LOOP_DETECT_THRESHOLD = 2;
149
151
  const MAX_PROMPT_CHARS = 1200000; // ~300k tokens (ngang context window) — compact() KHÔNG chạy trước auto-compact 80% (240k token) của repl.js
150
152
  // 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
@@ -588,36 +590,56 @@ export async function runAgent({ history, model, signal, onTool, onStatus, onDel
588
590
  }
589
591
 
590
592
  // ── Loop detection ──────────────────────────────────────────────────
591
- // Theo dõi N tool call gần nhất. Nếu cùng tên + input giống nhau liên tiếp
592
- // quá ngưỡng model bị kẹt. Inject thông báo nhắc model chuyển bước.
593
- // Nếu model vẫn lặp force stop sau MAX_LOOP_DETECTIONS lần.
593
+ // Theo dõi N tool call gần nhất. Phát hiện 2 dạng kẹt:
594
+ // (A) Cùng tool + input liên tiếp (A-A-A) dễ, như model hay né.
595
+ // (B) Pattern vòng (A-B-A-B, A-B-C-A-B-C) model xen kẽ 2-3 tool
596
+ // khác nhau để tránh phát hiện cũ. So nửa đầu vs nửa cuối window.
597
+ // Nếu phát hiện → inject cảnh báo. Nếu tái diễn → force stop.
594
598
  const inputStr = JSON.stringify(call.input || {});
595
599
  recentCalls.push({ name: call.name, inputStr });
596
600
  if (recentCalls.length > LOOP_DETECT_WINDOW) recentCalls.shift();
601
+ let loopType = null; // 'consecutive' | 'pattern' | null
597
602
  if (recentCalls.length >= LOOP_DETECT_THRESHOLD + 1) {
603
+ // (A) Same consecutive check
598
604
  const lastN = recentCalls.slice(-LOOP_DETECT_THRESHOLD);
599
605
  const allSame = lastN.every((c) => c.name === lastN[0].name && c.inputStr === lastN[0].inputStr);
600
- if (allSame) {
601
- loopDetectedCount++;
602
- // Đã lặp quá nhiều lần liên tiếp force stop
603
- if (loopDetectedCount >= MAX_LOOP_DETECTIONS) {
604
- history.push({
605
- role: "tool",
606
- name: "loop_detection",
607
- content: `[LOOP DETECTED × ${loopDetectedCount}] Bạn đã gọi ${lastN[0].name} nhiều lần liên tiếp với cùng input — KHÔNG THỂ TIẾP TỤC. Dừng ngay lập tức.`,
608
- });
609
- return `[LOOP STOPPED] Đã dừng model bị kẹt trong vòng lặp gọi ${lastN[0].name} liên tục.`;
606
+ if (allSame) loopType = 'consecutive';
607
+ }
608
+ // (B) Pattern cycle check: tìm chu kỳ lặp trong window (độ dài 2-3)
609
+ if (!loopType && recentCalls.length >= 4) {
610
+ for (let cycleLen = 2; cycleLen <= Math.min(3, Math.floor(recentCalls.length / 2)); cycleLen++) {
611
+ const half = Math.floor(recentCalls.length / cycleLen) * cycleLen;
612
+ const first = recentCalls.slice(-half, -half + cycleLen);
613
+ const rest = recentCalls.slice(-half + cycleLen);
614
+ let matched = true;
615
+ for (let i = 0; i < rest.length; i++) {
616
+ if (rest[i].name !== first[i % cycleLen].name || rest[i].inputStr !== first[i % cycleLen].inputStr) {
617
+ matched = false;
618
+ break;
619
+ }
610
620
  }
611
- // Đã lặp inject cảnh báo, xoá calls để tránh trigger liên tục
612
- recentCalls.length = 0;
621
+ if (matched) { loopType = 'pattern'; break; }
622
+ }
623
+ }
624
+ if (loopType) {
625
+ const label = loopType === 'consecutive' ? lastN[0].name : `pattern [${recentCalls.slice(-4).map((c) => c.name).join(", ")}]`;
626
+ loopDetectedCount++;
627
+ if (loopDetectedCount >= MAX_LOOP_DETECTIONS) {
613
628
  history.push({
614
629
  role: "tool",
615
630
  name: "loop_detection",
616
- content: `[LOOP DETECTED × ${loopDetectedCount}] 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. Nếu tiếp tục lặp, task sẽ bị dừng.`,
631
+ content: `[LOOP DETECTED × ${loopDetectedCount}] Bạn lặp lại ${label} nhiều lần — KHÔNG THỂ TIẾP TỤC. Dừng ngay.`,
617
632
  });
618
- } else {
619
- loopDetectedCount = 0; // model gọi tool khác → reset counter
633
+ return `[LOOP STOPPED] Đã dừng vì model bị kẹt trong vòng lặp.`;
620
634
  }
635
+ recentCalls.length = 0;
636
+ history.push({
637
+ role: "tool",
638
+ name: "loop_detection",
639
+ content: `[LOOP DETECTED × ${loopDetectedCount}] Bạn lặp lại ${label} — có vẻ đang kẹt. HÃY CHUYỂN BƯỚC: gọi tool khác hoặc trả lời Markdown nếu xong. KHÔNG lặp lại. Nếu tiếp tục, task sẽ dừng.`,
640
+ });
641
+ } else {
642
+ loopDetectedCount = 0; // tool khác → reset
621
643
  }
622
644
  }
623
645
  return t.maxSteps;