coder-agent 2.7.1 → 2.7.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/dist/agent.js +72 -0
- package/dist/index.js +44 -19
- package/package.json +1 -1
package/dist/agent.js
CHANGED
|
@@ -180,6 +180,62 @@ function formatResponseText(text) {
|
|
|
180
180
|
return chalk.white(part);
|
|
181
181
|
}).join('');
|
|
182
182
|
}
|
|
183
|
+
function normalizeContentForLoopCheck(content) {
|
|
184
|
+
if (!content)
|
|
185
|
+
return "";
|
|
186
|
+
return content
|
|
187
|
+
.toLowerCase()
|
|
188
|
+
.replace(/\s+/g, " ")
|
|
189
|
+
.trim();
|
|
190
|
+
}
|
|
191
|
+
function normalizeToolCallsForLoopCheck(toolCalls) {
|
|
192
|
+
if (!toolCalls || toolCalls.length === 0)
|
|
193
|
+
return "";
|
|
194
|
+
return toolCalls.map(tc => {
|
|
195
|
+
const name = tc.function?.name || "";
|
|
196
|
+
let argsStr = "";
|
|
197
|
+
try {
|
|
198
|
+
const rawArgs = tc.function?.arguments;
|
|
199
|
+
const args = typeof rawArgs === 'string'
|
|
200
|
+
? JSON.parse(rawArgs)
|
|
201
|
+
: rawArgs;
|
|
202
|
+
if (args && typeof args === 'object') {
|
|
203
|
+
const sortedArgs = {};
|
|
204
|
+
for (const key of Object.keys(args).sort()) {
|
|
205
|
+
sortedArgs[key] = args[key];
|
|
206
|
+
}
|
|
207
|
+
argsStr = JSON.stringify(sortedArgs);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
catch {
|
|
211
|
+
argsStr = tc.function?.arguments || "";
|
|
212
|
+
}
|
|
213
|
+
return `${name}(${argsStr})`;
|
|
214
|
+
}).join(";");
|
|
215
|
+
}
|
|
216
|
+
function hasRepeatingCycle(history) {
|
|
217
|
+
const n = history.length;
|
|
218
|
+
for (let len = 1; len <= 4; len++) {
|
|
219
|
+
if (n >= len * 2) {
|
|
220
|
+
const minRepeats = len === 1 ? 3 : 2;
|
|
221
|
+
if (n >= len * minRepeats) {
|
|
222
|
+
let isLoop = true;
|
|
223
|
+
const lastBlock = history.slice(n - len);
|
|
224
|
+
for (let r = 1; r < minRepeats; r++) {
|
|
225
|
+
const prevBlock = history.slice(n - len * (r + 1), n - len * r);
|
|
226
|
+
if (JSON.stringify(lastBlock) !== JSON.stringify(prevBlock)) {
|
|
227
|
+
isLoop = false;
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
if (isLoop) {
|
|
232
|
+
return true;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return false;
|
|
238
|
+
}
|
|
183
239
|
// ─── Extract Text Tool Calls ──────────────────────────────────────────────────
|
|
184
240
|
function extractTextToolCalls(content) {
|
|
185
241
|
const calls = [];
|
|
@@ -568,6 +624,7 @@ export class Agent {
|
|
|
568
624
|
const MAX_WAITS = 3;
|
|
569
625
|
const modifiedFiles = new Set();
|
|
570
626
|
let cleanContent = "";
|
|
627
|
+
const stateHistory = [];
|
|
571
628
|
while (true) {
|
|
572
629
|
if (signal?.aborted) {
|
|
573
630
|
const abortErr = new Error("The user aborted a request.");
|
|
@@ -707,6 +764,21 @@ export class Agent {
|
|
|
707
764
|
for (const line of statusLines) {
|
|
708
765
|
console.log(line);
|
|
709
766
|
}
|
|
767
|
+
// Loop detection & intervention
|
|
768
|
+
const currentKey = `${normalizeContentForLoopCheck(msg.content || "")}|${normalizeToolCallsForLoopCheck(toolCalls)}`;
|
|
769
|
+
const tempHistory = [...stateHistory, currentKey];
|
|
770
|
+
if (hasRepeatingCycle(tempHistory)) {
|
|
771
|
+
const warningMessage = `⚠️ [LOOP DETECTED] You are repeating the exact same thoughts or tool calls. Please break out of this loop. Do not repeat the same actions. Re-evaluate your strategy, look at a different file, run a different command, or ask the user for clarification if you cannot proceed.`;
|
|
772
|
+
console.log(chalk.hex('#ff9f0a')('\n⚠ Loop detected! Intervening to break the loop...'));
|
|
773
|
+
this.memory.add({
|
|
774
|
+
role: "user",
|
|
775
|
+
content: warningMessage,
|
|
776
|
+
});
|
|
777
|
+
stateHistory.length = 0; // Reset history to allow a fresh start
|
|
778
|
+
}
|
|
779
|
+
else {
|
|
780
|
+
stateHistory.push(currentKey);
|
|
781
|
+
}
|
|
710
782
|
if (iterations < MAX_ITERATIONS) {
|
|
711
783
|
console.log(chalk.dim('\n' + '─'.repeat(48) + '\n'));
|
|
712
784
|
startSpinner("thinking...");
|
package/dist/index.js
CHANGED
|
@@ -183,10 +183,52 @@ async function main() {
|
|
|
183
183
|
if (!apiKey) {
|
|
184
184
|
apiKey = await promptApiKey();
|
|
185
185
|
}
|
|
186
|
+
let currentAbortController = null;
|
|
187
|
+
let originalListeners = [];
|
|
188
|
+
let isHijacked = false;
|
|
189
|
+
let tempSigintHandler = null;
|
|
190
|
+
const startHijack = () => {
|
|
191
|
+
if (isHijacked)
|
|
192
|
+
return;
|
|
193
|
+
originalListeners = process.stdin.listeners("data");
|
|
194
|
+
for (const listener of originalListeners) {
|
|
195
|
+
process.stdin.removeListener("data", listener);
|
|
196
|
+
}
|
|
197
|
+
tempSigintHandler = (data) => {
|
|
198
|
+
if (data.includes(3)) { // Ctrl+C byte
|
|
199
|
+
if (currentAbortController) {
|
|
200
|
+
currentAbortController.abort();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
process.stdin.on("data", tempSigintHandler);
|
|
205
|
+
process.stdin.resume();
|
|
206
|
+
isHijacked = true;
|
|
207
|
+
};
|
|
208
|
+
const stopHijack = () => {
|
|
209
|
+
if (!isHijacked)
|
|
210
|
+
return;
|
|
211
|
+
process.stdin.removeListener("data", tempSigintHandler);
|
|
212
|
+
for (const listener of originalListeners) {
|
|
213
|
+
process.stdin.on("data", listener);
|
|
214
|
+
}
|
|
215
|
+
originalListeners = [];
|
|
216
|
+
tempSigintHandler = null;
|
|
217
|
+
isHijacked = false;
|
|
218
|
+
};
|
|
186
219
|
const confirmHandler = async (question) => {
|
|
187
220
|
if (rl) {
|
|
221
|
+
const wasHijacked = isHijacked;
|
|
222
|
+
if (wasHijacked) {
|
|
223
|
+
stopHijack();
|
|
224
|
+
rl.resume();
|
|
225
|
+
}
|
|
188
226
|
return new Promise((resolve) => {
|
|
189
227
|
rl.question(question, (answer) => {
|
|
228
|
+
if (wasHijacked) {
|
|
229
|
+
rl.pause();
|
|
230
|
+
startHijack();
|
|
231
|
+
}
|
|
190
232
|
resolve(answer.trim().toLowerCase().startsWith("y"));
|
|
191
233
|
});
|
|
192
234
|
});
|
|
@@ -236,7 +278,6 @@ async function main() {
|
|
|
236
278
|
let inputBuffer = "";
|
|
237
279
|
let pasteTimeout = null;
|
|
238
280
|
let lineCountInBurst = 0;
|
|
239
|
-
let currentAbortController = null;
|
|
240
281
|
async function executeAgentChat(trimmed) {
|
|
241
282
|
// Pause standard input processing during agent thinking & updates
|
|
242
283
|
rl.pause();
|
|
@@ -294,20 +335,7 @@ async function main() {
|
|
|
294
335
|
return;
|
|
295
336
|
}
|
|
296
337
|
currentAbortController = new AbortController();
|
|
297
|
-
|
|
298
|
-
const originalListeners = process.stdin.listeners("data");
|
|
299
|
-
for (const listener of originalListeners) {
|
|
300
|
-
process.stdin.removeListener("data", listener);
|
|
301
|
-
}
|
|
302
|
-
const tempSigintHandler = (data) => {
|
|
303
|
-
if (data.includes(3)) { // Ctrl+C byte
|
|
304
|
-
if (currentAbortController) {
|
|
305
|
-
currentAbortController.abort();
|
|
306
|
-
}
|
|
307
|
-
}
|
|
308
|
-
};
|
|
309
|
-
process.stdin.on("data", tempSigintHandler);
|
|
310
|
-
process.stdin.resume();
|
|
338
|
+
startHijack();
|
|
311
339
|
try {
|
|
312
340
|
await agent.chat(trimmed, currentAbortController.signal);
|
|
313
341
|
}
|
|
@@ -329,10 +357,7 @@ async function main() {
|
|
|
329
357
|
}
|
|
330
358
|
}
|
|
331
359
|
finally {
|
|
332
|
-
|
|
333
|
-
for (const listener of originalListeners) {
|
|
334
|
-
process.stdin.on("data", listener);
|
|
335
|
-
}
|
|
360
|
+
stopHijack();
|
|
336
361
|
currentAbortController = null;
|
|
337
362
|
}
|
|
338
363
|
rl.resume();
|