codeep 1.2.62 → 1.2.64
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/renderer/App.d.ts +2 -0
- package/dist/renderer/App.js +12 -2
- package/dist/renderer/agentExecution.js +7 -1
- package/dist/utils/agent.js +128 -7
- package/package.json +1 -1
package/dist/renderer/App.d.ts
CHANGED
|
@@ -41,6 +41,7 @@ export declare class App {
|
|
|
41
41
|
private spinnerInterval;
|
|
42
42
|
private isAgentRunning;
|
|
43
43
|
private agentIteration;
|
|
44
|
+
private agentMaxIterations;
|
|
44
45
|
private agentActions;
|
|
45
46
|
private agentThinking;
|
|
46
47
|
private pasteInfo;
|
|
@@ -173,6 +174,7 @@ export declare class App {
|
|
|
173
174
|
target: string;
|
|
174
175
|
result: string;
|
|
175
176
|
}): void;
|
|
177
|
+
setAgentMaxIterations(max: number): void;
|
|
176
178
|
/**
|
|
177
179
|
* Set agent thinking text
|
|
178
180
|
*/
|
package/dist/renderer/App.js
CHANGED
|
@@ -99,6 +99,7 @@ export class App {
|
|
|
99
99
|
// Agent progress state
|
|
100
100
|
isAgentRunning = false;
|
|
101
101
|
agentIteration = 0;
|
|
102
|
+
agentMaxIterations = 0;
|
|
102
103
|
agentActions = [];
|
|
103
104
|
agentThinking = '';
|
|
104
105
|
// Paste detection state
|
|
@@ -361,6 +362,7 @@ export class App {
|
|
|
361
362
|
this.isAgentRunning = running;
|
|
362
363
|
if (running) {
|
|
363
364
|
this.agentIteration = 0;
|
|
365
|
+
this.agentMaxIterations = 0;
|
|
364
366
|
this.agentActions = [];
|
|
365
367
|
this.agentThinking = '';
|
|
366
368
|
this.isLoading = false; // Clear loading state when agent takes over
|
|
@@ -382,6 +384,9 @@ export class App {
|
|
|
382
384
|
}
|
|
383
385
|
this.render();
|
|
384
386
|
}
|
|
387
|
+
setAgentMaxIterations(max) {
|
|
388
|
+
this.agentMaxIterations = max;
|
|
389
|
+
}
|
|
385
390
|
/**
|
|
386
391
|
* Set agent thinking text
|
|
387
392
|
*/
|
|
@@ -1445,7 +1450,10 @@ export class App {
|
|
|
1445
1450
|
// Agent running state - show special prompt
|
|
1446
1451
|
if (this.isAgentRunning) {
|
|
1447
1452
|
const spinner = SPINNER_FRAMES[this.spinnerFrame];
|
|
1448
|
-
const
|
|
1453
|
+
const stepLabel = this.agentMaxIterations > 0
|
|
1454
|
+
? `step ${this.agentIteration}/${this.agentMaxIterations}`
|
|
1455
|
+
: `step ${this.agentIteration}`;
|
|
1456
|
+
const agentText = `${spinner} Agent working... ${stepLabel} | ${this.agentActions.length} actions (Esc to stop)`;
|
|
1449
1457
|
this.screen.writeLine(y, agentText, PRIMARY_COLOR);
|
|
1450
1458
|
this.screen.showCursor(false);
|
|
1451
1459
|
return;
|
|
@@ -1932,7 +1940,9 @@ export class App {
|
|
|
1932
1940
|
x += txt.length + 1;
|
|
1933
1941
|
}
|
|
1934
1942
|
// Step info on the right
|
|
1935
|
-
const stepText =
|
|
1943
|
+
const stepText = this.agentMaxIterations > 0
|
|
1944
|
+
? `step ${this.agentIteration}/${this.agentMaxIterations}`
|
|
1945
|
+
: `step ${this.agentIteration}`;
|
|
1936
1946
|
this.screen.write(width - stepText.length - 1, y, stepText, fg.gray);
|
|
1937
1947
|
y++;
|
|
1938
1948
|
// Bottom border with help
|
|
@@ -135,6 +135,8 @@ export async function executeAgentTask(task, dryRun, ctx) {
|
|
|
135
135
|
try {
|
|
136
136
|
const fileContext = ctx.formatAddedFilesContext();
|
|
137
137
|
const enrichedTask = fileContext ? fileContext + task : task;
|
|
138
|
+
// Show N/M progress in status bar
|
|
139
|
+
app.setAgentMaxIterations(config.get('agentMaxIterations'));
|
|
138
140
|
const result = await runAgent(enrichedTask, context, {
|
|
139
141
|
dryRun,
|
|
140
142
|
chatHistory: app.getChatHistory(),
|
|
@@ -320,7 +322,11 @@ export async function executeAgentTask(task, dryRun, ctx) {
|
|
|
320
322
|
app.addMessage({ role: 'assistant', content: 'Agent stopped by user.' });
|
|
321
323
|
}
|
|
322
324
|
else {
|
|
323
|
-
|
|
325
|
+
const failLines = [];
|
|
326
|
+
if (result.finalResponse)
|
|
327
|
+
failLines.push(result.finalResponse);
|
|
328
|
+
failLines.push(`**Agent stopped**: ${result.error || 'Unknown error'}`);
|
|
329
|
+
app.addMessage({ role: 'assistant', content: failLines.join('\n\n') });
|
|
324
330
|
}
|
|
325
331
|
autoSaveSession(app.getMessages(), ctx.projectPath);
|
|
326
332
|
}
|
package/dist/utils/agent.js
CHANGED
|
@@ -38,6 +38,56 @@ import { startSession, endSession, undoLastAction, undoAllActions, getCurrentSes
|
|
|
38
38
|
import { runAllVerifications, formatErrorsForAgent, hasVerificationErrors, getVerificationSummary } from './verify.js';
|
|
39
39
|
import { gatherSmartContext, formatSmartContext, extractTargetFile } from './smartContext.js';
|
|
40
40
|
import { planTasks, formatTaskPlan } from './taskPlanner.js';
|
|
41
|
+
// ─── Tool result truncation ───────────────────────────────────────────────────
|
|
42
|
+
const TOOL_RESULT_MAX_CHARS = 8_000; // ~2K tokens per tool result
|
|
43
|
+
function truncateToolResult(output, toolName) {
|
|
44
|
+
if (output.length <= TOOL_RESULT_MAX_CHARS)
|
|
45
|
+
return output;
|
|
46
|
+
const kept = output.slice(0, TOOL_RESULT_MAX_CHARS);
|
|
47
|
+
const truncated = output.length - TOOL_RESULT_MAX_CHARS;
|
|
48
|
+
return `${kept}\n[... ${truncated} chars truncated — use search_code or read specific sections if you need more]`;
|
|
49
|
+
}
|
|
50
|
+
// ─── Context window compression ───────────────────────────────────────────────
|
|
51
|
+
const CONTEXT_COMPRESS_THRESHOLD = 80_000; // ~20K tokens, safe for all providers
|
|
52
|
+
const RECENT_MESSAGES_TO_KEEP = 6; // Always preserve the last N messages verbatim
|
|
53
|
+
/**
|
|
54
|
+
* Compress old messages when the conversation grows too large.
|
|
55
|
+
* Keeps the first message (original task) and the last RECENT_MESSAGES_TO_KEEP
|
|
56
|
+
* messages intact. Everything in between is replaced with a compact summary
|
|
57
|
+
* built from the actions log — no extra API call needed.
|
|
58
|
+
*/
|
|
59
|
+
function compressMessages(messages, actions) {
|
|
60
|
+
const totalChars = messages.reduce((sum, m) => sum + m.content.length, 0);
|
|
61
|
+
if (totalChars < CONTEXT_COMPRESS_THRESHOLD)
|
|
62
|
+
return messages;
|
|
63
|
+
// Need at least first + recent block to be worth compressing
|
|
64
|
+
if (messages.length <= RECENT_MESSAGES_TO_KEEP + 1)
|
|
65
|
+
return messages;
|
|
66
|
+
const firstMessage = messages[0];
|
|
67
|
+
const recentMessages = messages.slice(-RECENT_MESSAGES_TO_KEEP);
|
|
68
|
+
// Build summary from action log
|
|
69
|
+
const fileWrites = actions.filter(a => a.type === 'write' || a.type === 'edit');
|
|
70
|
+
const fileDeletes = actions.filter(a => a.type === 'delete');
|
|
71
|
+
const commands = actions.filter(a => a.type === 'command');
|
|
72
|
+
const reads = actions.filter(a => a.type === 'read');
|
|
73
|
+
const summaryLines = ['[Context compressed — summary of work so far]'];
|
|
74
|
+
if (fileWrites.length > 0) {
|
|
75
|
+
summaryLines.push(`Files written/edited (${fileWrites.length}): ${fileWrites.map(a => a.target).join(', ')}`);
|
|
76
|
+
}
|
|
77
|
+
if (fileDeletes.length > 0) {
|
|
78
|
+
summaryLines.push(`Files deleted: ${fileDeletes.map(a => a.target).join(', ')}`);
|
|
79
|
+
}
|
|
80
|
+
if (commands.length > 0) {
|
|
81
|
+
summaryLines.push(`Commands run: ${commands.map(a => a.target).join(', ')}`);
|
|
82
|
+
}
|
|
83
|
+
if (reads.length > 0) {
|
|
84
|
+
summaryLines.push(`Files read (${reads.length}): ${reads.slice(-10).map(a => a.target).join(', ')}`);
|
|
85
|
+
}
|
|
86
|
+
summaryLines.push('[End of summary — continuing from current state]');
|
|
87
|
+
const summaryMessage = { role: 'user', content: summaryLines.join('\n') };
|
|
88
|
+
debug(`Context compressed: ${totalChars} chars → keeping first + summary + last ${RECENT_MESSAGES_TO_KEEP} messages`);
|
|
89
|
+
return [firstMessage, summaryMessage, ...recentMessages];
|
|
90
|
+
}
|
|
41
91
|
const DEFAULT_OPTIONS = {
|
|
42
92
|
maxIterations: 100, // Increased for large tasks
|
|
43
93
|
maxDuration: 20 * 60 * 1000, // 20 minutes
|
|
@@ -129,16 +179,29 @@ export async function runAgent(prompt, projectContext, options = {}) {
|
|
|
129
179
|
const maxTimeoutRetries = 3;
|
|
130
180
|
const maxConsecutiveTimeouts = 9; // Allow more consecutive timeouts before giving up
|
|
131
181
|
const baseTimeout = config.get('agentApiTimeout');
|
|
182
|
+
// Infinite loop detection: track last write hash per file path
|
|
183
|
+
const lastWriteHashByPath = new Map();
|
|
184
|
+
let duplicateWriteCount = 0;
|
|
185
|
+
// Duplicate read cache: path → truncated output (avoid re-sending large file content)
|
|
186
|
+
const readCache = new Map();
|
|
132
187
|
try {
|
|
133
188
|
while (iteration < opts.maxIterations) {
|
|
134
189
|
// Check timeout
|
|
135
190
|
if (Date.now() - startTime > opts.maxDuration) {
|
|
191
|
+
const filesDone = actions.filter(a => a.type === 'write' || a.type === 'edit').map(a => a.target);
|
|
192
|
+
const durationMin = Math.round(opts.maxDuration / 60000);
|
|
193
|
+
const partialLines = [`Agent reached the time limit (${durationMin} min).`];
|
|
194
|
+
if (filesDone.length > 0) {
|
|
195
|
+
partialLines.push(`\n**Partial progress — files written/edited:**`);
|
|
196
|
+
[...new Set(filesDone)].forEach(f => partialLines.push(` ✓ \`${f}\``));
|
|
197
|
+
partialLines.push(`\nYou can continue by running the agent again.`);
|
|
198
|
+
}
|
|
136
199
|
result = {
|
|
137
200
|
success: false,
|
|
138
201
|
iterations: iteration,
|
|
139
202
|
actions,
|
|
140
|
-
finalResponse: '
|
|
141
|
-
error: `Exceeded maximum duration of ${
|
|
203
|
+
finalResponse: partialLines.join('\n'),
|
|
204
|
+
error: `Exceeded maximum duration of ${durationMin} min`,
|
|
142
205
|
};
|
|
143
206
|
return result;
|
|
144
207
|
}
|
|
@@ -156,6 +219,13 @@ export async function runAgent(prompt, projectContext, options = {}) {
|
|
|
156
219
|
}
|
|
157
220
|
iteration++;
|
|
158
221
|
opts.onIteration?.(iteration, `Iteration ${iteration}/${opts.maxIterations}`);
|
|
222
|
+
// Compress messages if context window is getting full
|
|
223
|
+
const compressed = compressMessages(messages, actions);
|
|
224
|
+
if (compressed !== messages) {
|
|
225
|
+
messages.length = 0;
|
|
226
|
+
messages.push(...compressed);
|
|
227
|
+
opts.onIteration?.(iteration, `Context compressed (${compressed.length} messages kept)`);
|
|
228
|
+
}
|
|
159
229
|
debug(`Starting iteration ${iteration}/${opts.maxIterations}, actions: ${actions.length}`);
|
|
160
230
|
// Calculate dynamic timeout based on task complexity
|
|
161
231
|
const dynamicTimeout = calculateDynamicTimeout(prompt, iteration, baseTimeout);
|
|
@@ -294,13 +364,57 @@ export async function runAgent(prompt, projectContext, options = {}) {
|
|
|
294
364
|
// Log action
|
|
295
365
|
const actionLog = createActionLog(toolCall, toolResult);
|
|
296
366
|
actions.push(actionLog);
|
|
297
|
-
//
|
|
298
|
-
if (
|
|
299
|
-
|
|
367
|
+
// ── Infinite loop detection for write/edit ──────────────────────────
|
|
368
|
+
if (toolCall.tool === 'write_file' || toolCall.tool === 'edit_file') {
|
|
369
|
+
const filePath = toolCall.parameters.path || '';
|
|
370
|
+
const contentKey = JSON.stringify(toolCall.parameters).slice(0, 500);
|
|
371
|
+
const prevHash = lastWriteHashByPath.get(filePath);
|
|
372
|
+
if (prevHash === contentKey) {
|
|
373
|
+
duplicateWriteCount++;
|
|
374
|
+
if (duplicateWriteCount >= 2) {
|
|
375
|
+
toolResults.push(`[WARNING] You have written the same content to \`${filePath}\` ${duplicateWriteCount + 1} times in a row. You are stuck in a loop. Stop and think differently — read the file to check its current state, then try a completely different approach.`);
|
|
376
|
+
duplicateWriteCount = 0;
|
|
377
|
+
}
|
|
378
|
+
else {
|
|
379
|
+
toolResults.push(`Tool ${toolCall.tool} succeeded (note: same content as previous write to this file):\n${toolResult.output}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
duplicateWriteCount = 0;
|
|
384
|
+
lastWriteHashByPath.set(filePath, contentKey);
|
|
385
|
+
if (toolResult.success) {
|
|
386
|
+
toolResults.push(`Tool ${toolCall.tool} succeeded:\n${toolResult.output}`);
|
|
387
|
+
}
|
|
388
|
+
else {
|
|
389
|
+
toolResults.push(`Tool ${toolCall.tool} failed:\n${toolResult.error || 'Unknown error'}`);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
// ── Duplicate read cache ────────────────────────────────────────────
|
|
393
|
+
}
|
|
394
|
+
else if (toolCall.tool === 'read_file' && toolResult.success) {
|
|
395
|
+
const filePath = toolCall.parameters.path || '';
|
|
396
|
+
if (readCache.has(filePath)) {
|
|
397
|
+
toolResults.push(`Tool read_file succeeded (cached — file unchanged since last read):\n${readCache.get(filePath)}`);
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
const truncated = truncateToolResult(toolResult.output, toolCall.tool);
|
|
401
|
+
readCache.set(filePath, truncated);
|
|
402
|
+
toolResults.push(`Tool read_file succeeded:\n${truncated}`);
|
|
403
|
+
}
|
|
404
|
+
// ── General truncation for other tools ─────────────────────────────
|
|
405
|
+
}
|
|
406
|
+
else if (toolResult.success) {
|
|
407
|
+
const truncated = truncateToolResult(toolResult.output, toolCall.tool);
|
|
408
|
+
toolResults.push(`Tool ${toolCall.tool} succeeded:\n${truncated}`);
|
|
300
409
|
}
|
|
301
410
|
else {
|
|
302
411
|
toolResults.push(`Tool ${toolCall.tool} failed:\n${toolResult.error || 'Unknown error'}`);
|
|
303
412
|
}
|
|
413
|
+
// Invalidate read cache when a file is written/edited
|
|
414
|
+
if ((toolCall.tool === 'write_file' || toolCall.tool === 'edit_file') && toolResult.success) {
|
|
415
|
+
const filePath = toolCall.parameters.path || '';
|
|
416
|
+
readCache.delete(filePath);
|
|
417
|
+
}
|
|
304
418
|
}
|
|
305
419
|
// Add tool results to messages
|
|
306
420
|
messages.push({
|
|
@@ -308,13 +422,20 @@ export async function runAgent(prompt, projectContext, options = {}) {
|
|
|
308
422
|
content: `Tool results:\n\n${toolResults.join('\n\n')}\n\nContinue with the task. If this subtask is complete, provide a summary without tool calls.`,
|
|
309
423
|
});
|
|
310
424
|
}
|
|
311
|
-
// Check if we hit max iterations
|
|
425
|
+
// Check if we hit max iterations — build partial summary from actions log
|
|
312
426
|
if (iteration >= opts.maxIterations && !finalResponse) {
|
|
427
|
+
const filesDone = actions.filter(a => a.type === 'write' || a.type === 'edit').map(a => a.target);
|
|
428
|
+
const partialLines = [`Agent reached the iteration limit (${opts.maxIterations} steps).`];
|
|
429
|
+
if (filesDone.length > 0) {
|
|
430
|
+
partialLines.push(`\n**Partial progress — files written/edited:**`);
|
|
431
|
+
[...new Set(filesDone)].forEach(f => partialLines.push(` ✓ \`${f}\``));
|
|
432
|
+
partialLines.push(`\nThe task may be incomplete. You can continue by running the agent again.`);
|
|
433
|
+
}
|
|
313
434
|
result = {
|
|
314
435
|
success: false,
|
|
315
436
|
iterations: iteration,
|
|
316
437
|
actions,
|
|
317
|
-
finalResponse: '
|
|
438
|
+
finalResponse: partialLines.join('\n'),
|
|
318
439
|
error: `Exceeded maximum of ${opts.maxIterations} iterations`,
|
|
319
440
|
};
|
|
320
441
|
return result;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "codeep",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.64",
|
|
4
4
|
"description": "AI-powered coding assistant built for the terminal. Multiple LLM providers, project-aware context, and a seamless development workflow.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|