coder-agent 2.9.9 → 2.9.11
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/dist/agent.js +77 -11
- package/dist/memory.js +19 -0
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -248,11 +248,10 @@ function getLoopCheckKey(toolCalls) {
|
|
|
248
248
|
const end = args.end_line ?? "";
|
|
249
249
|
return `read_file_lines:${normalizedPath}:${start}-${end}`;
|
|
250
250
|
case "write_file":
|
|
251
|
-
return `write_file:${normalizedPath}
|
|
251
|
+
return `write_file:${normalizedPath}`;
|
|
252
252
|
case "patch_file":
|
|
253
253
|
const targetCode = normalizeCode(args.target_code || "");
|
|
254
|
-
|
|
255
|
-
return `patch_file:${normalizedPath}:${targetCode}:${replacementCode}`;
|
|
254
|
+
return `patch_file:${normalizedPath}:${targetCode}`;
|
|
256
255
|
case "list_directory":
|
|
257
256
|
return `list_directory:${normalizedPath}`;
|
|
258
257
|
case "run_shell":
|
|
@@ -277,7 +276,7 @@ function hasRepeatingCycle(history) {
|
|
|
277
276
|
const n = history.length;
|
|
278
277
|
for (let len = 1; len <= 4; len++) {
|
|
279
278
|
if (n >= len * 2) {
|
|
280
|
-
const minRepeats = len === 1 ? 3 : 2;
|
|
279
|
+
const minRepeats = len === 1 ? 2 : (len === 2 ? 3 : 2);
|
|
281
280
|
if (n >= len * minRepeats) {
|
|
282
281
|
let isLoop = true;
|
|
283
282
|
const lastBlock = history.slice(n - len);
|
|
@@ -296,6 +295,59 @@ function hasRepeatingCycle(history) {
|
|
|
296
295
|
}
|
|
297
296
|
return false;
|
|
298
297
|
}
|
|
298
|
+
function getJaccardSimilarity(str1, str2) {
|
|
299
|
+
const getWords = (str) => {
|
|
300
|
+
return new Set(str
|
|
301
|
+
.toLowerCase()
|
|
302
|
+
.replace(/[^a-z0-9\s]/g, "")
|
|
303
|
+
.split(/\s+/)
|
|
304
|
+
.filter(w => w.length > 2));
|
|
305
|
+
};
|
|
306
|
+
const words1 = getWords(str1);
|
|
307
|
+
const words2 = getWords(str2);
|
|
308
|
+
if (words1.size === 0 && words2.size === 0)
|
|
309
|
+
return 1.0;
|
|
310
|
+
if (words1.size === 0 || words2.size === 0)
|
|
311
|
+
return 0.0;
|
|
312
|
+
let intersectionCount = 0;
|
|
313
|
+
for (const w of words1) {
|
|
314
|
+
if (words2.has(w)) {
|
|
315
|
+
intersectionCount++;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
const unionSize = words1.size + words2.size - intersectionCount;
|
|
319
|
+
return intersectionCount / unionSize;
|
|
320
|
+
}
|
|
321
|
+
function hasRepeatingThoughtCycle(thoughts) {
|
|
322
|
+
const n = thoughts.length;
|
|
323
|
+
const nonEmptyThoughts = thoughts.filter(t => t.trim().length > 10);
|
|
324
|
+
const m = nonEmptyThoughts.length;
|
|
325
|
+
for (let len = 1; len <= 4; len++) {
|
|
326
|
+
if (m >= len * 2) {
|
|
327
|
+
const minRepeats = len === 1 ? 2 : (len === 2 ? 3 : 2);
|
|
328
|
+
if (m >= len * minRepeats) {
|
|
329
|
+
let isLoop = true;
|
|
330
|
+
const lastBlock = nonEmptyThoughts.slice(m - len);
|
|
331
|
+
for (let r = 1; r < minRepeats; r++) {
|
|
332
|
+
const prevBlock = nonEmptyThoughts.slice(m - len * (r + 1), m - len * r);
|
|
333
|
+
for (let i = 0; i < len; i++) {
|
|
334
|
+
const sim = getJaccardSimilarity(lastBlock[i], prevBlock[i]);
|
|
335
|
+
if (sim < 0.60) {
|
|
336
|
+
isLoop = false;
|
|
337
|
+
break;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if (!isLoop)
|
|
341
|
+
break;
|
|
342
|
+
}
|
|
343
|
+
if (isLoop) {
|
|
344
|
+
return true;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return false;
|
|
350
|
+
}
|
|
299
351
|
// ─── Extract Text Tool Calls ──────────────────────────────────────────────────
|
|
300
352
|
function extractTextToolCalls(content) {
|
|
301
353
|
const calls = [];
|
|
@@ -757,6 +809,7 @@ export class Agent {
|
|
|
757
809
|
const modifiedFiles = new Set();
|
|
758
810
|
let cleanContent = "";
|
|
759
811
|
const stateHistory = [];
|
|
812
|
+
const thoughtHistory = [];
|
|
760
813
|
while (true) {
|
|
761
814
|
if (signal?.aborted) {
|
|
762
815
|
const abortErr = new Error("The user aborted a request.");
|
|
@@ -921,22 +974,35 @@ export class Agent {
|
|
|
921
974
|
// Loop detection & intervention
|
|
922
975
|
const currentKey = getLoopCheckKey(toolCalls);
|
|
923
976
|
const tempHistory = [...stateHistory, currentKey];
|
|
924
|
-
|
|
977
|
+
const currentThought = msg.content || "";
|
|
978
|
+
const tempThoughtHistory = [...thoughtHistory, currentThought];
|
|
979
|
+
let loopTriggered = false;
|
|
980
|
+
if (hasRepeatingCycle(tempHistory) || hasRepeatingThoughtCycle(tempThoughtHistory)) {
|
|
981
|
+
loopTriggered = true;
|
|
982
|
+
}
|
|
983
|
+
if (loopTriggered) {
|
|
925
984
|
loopInterventions++;
|
|
926
985
|
if (loopInterventions >= 2) {
|
|
927
986
|
console.log(chalk.hex('#ff453a')('\n✕ Loop intervention failed: Coder is stuck in an execution loop. Exiting to prompt.'));
|
|
928
987
|
break;
|
|
929
988
|
}
|
|
930
|
-
const
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
989
|
+
const originalGoal = this.memory.getAll().find(m => m.role === "user")?.content || userMessage;
|
|
990
|
+
const warningMessage = `⚠️ [LOOP DETECTED & CONTEXT RESET] You got stuck in a repeating thinking/execution loop. To break the loop, all intermediate repetitive chat context has been discarded.
|
|
991
|
+
|
|
992
|
+
Here is your original goal:
|
|
993
|
+
"${originalGoal}"
|
|
994
|
+
|
|
995
|
+
Please start fresh. Re-evaluate your strategy, check other files, run different commands, or ask the user for clarification directly. Do NOT repeat the same failed tool calls.`;
|
|
996
|
+
console.log(chalk.hex('#ff9f0a')('\n⚠ Loop detected! Compressing memory and resetting context window...'));
|
|
997
|
+
this.memory.resetToInitialPrompt(warningMessage);
|
|
936
998
|
stateHistory.length = 0; // Reset history to allow a fresh start
|
|
999
|
+
thoughtHistory.length = 0;
|
|
937
1000
|
}
|
|
938
1001
|
else {
|
|
939
1002
|
stateHistory.push(currentKey);
|
|
1003
|
+
if (currentThought.trim().length > 10) {
|
|
1004
|
+
thoughtHistory.push(currentThought);
|
|
1005
|
+
}
|
|
940
1006
|
if (loopInterventions > 0)
|
|
941
1007
|
loopInterventions = 0;
|
|
942
1008
|
}
|
package/dist/memory.js
CHANGED
|
@@ -524,6 +524,25 @@ export class Memory {
|
|
|
524
524
|
this.messages = [this.messages[0]]; // keep system prompt
|
|
525
525
|
console.log(" Memory cleared.");
|
|
526
526
|
}
|
|
527
|
+
resetToInitialPrompt(warningMessage) {
|
|
528
|
+
if (this.messages.length <= 1)
|
|
529
|
+
return;
|
|
530
|
+
const systemMsg = this.messages[0];
|
|
531
|
+
const firstUserMsg = this.messages.find(m => m.role === "user");
|
|
532
|
+
if (firstUserMsg) {
|
|
533
|
+
this.messages = [
|
|
534
|
+
systemMsg,
|
|
535
|
+
firstUserMsg,
|
|
536
|
+
{ role: "user", content: warningMessage }
|
|
537
|
+
];
|
|
538
|
+
}
|
|
539
|
+
else {
|
|
540
|
+
this.messages = [
|
|
541
|
+
systemMsg,
|
|
542
|
+
{ role: "user", content: warningMessage }
|
|
543
|
+
];
|
|
544
|
+
}
|
|
545
|
+
}
|
|
527
546
|
summary() {
|
|
528
547
|
const turns = this.messages.filter(m => m.role === "user").length;
|
|
529
548
|
return `${turns} turn(s) in memory (${getMemoryScopeDisplay(this.scope)} scope)`;
|