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.
@@ -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
  */
@@ -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 agentText = `${spinner} Agent working... step ${this.agentIteration} | ${this.agentActions.length} actions (Esc to stop)`;
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 = `step ${this.agentIteration}`;
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
- app.addMessage({ role: 'assistant', content: `Agent failed: ${result.error || 'Unknown error'}` });
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
  }
@@ -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: 'Agent timed out',
141
- error: `Exceeded maximum duration of ${opts.maxDuration / 1000} seconds`,
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
- // Format result for AI
298
- if (toolResult.success) {
299
- toolResults.push(`Tool ${toolCall.tool} succeeded:\n${toolResult.output}`);
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: 'Agent reached maximum iterations',
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.62",
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",