codeep 1.2.62 → 1.2.63

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(),
@@ -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,6 +179,11 @@ 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
@@ -156,6 +211,13 @@ export async function runAgent(prompt, projectContext, options = {}) {
156
211
  }
157
212
  iteration++;
158
213
  opts.onIteration?.(iteration, `Iteration ${iteration}/${opts.maxIterations}`);
214
+ // Compress messages if context window is getting full
215
+ const compressed = compressMessages(messages, actions);
216
+ if (compressed !== messages) {
217
+ messages.length = 0;
218
+ messages.push(...compressed);
219
+ opts.onIteration?.(iteration, `Context compressed (${compressed.length} messages kept)`);
220
+ }
159
221
  debug(`Starting iteration ${iteration}/${opts.maxIterations}, actions: ${actions.length}`);
160
222
  // Calculate dynamic timeout based on task complexity
161
223
  const dynamicTimeout = calculateDynamicTimeout(prompt, iteration, baseTimeout);
@@ -294,13 +356,57 @@ export async function runAgent(prompt, projectContext, options = {}) {
294
356
  // Log action
295
357
  const actionLog = createActionLog(toolCall, toolResult);
296
358
  actions.push(actionLog);
297
- // Format result for AI
298
- if (toolResult.success) {
299
- toolResults.push(`Tool ${toolCall.tool} succeeded:\n${toolResult.output}`);
359
+ // ── Infinite loop detection for write/edit ──────────────────────────
360
+ if (toolCall.tool === 'write_file' || toolCall.tool === 'edit_file') {
361
+ const filePath = toolCall.parameters.path || '';
362
+ const contentKey = JSON.stringify(toolCall.parameters).slice(0, 500);
363
+ const prevHash = lastWriteHashByPath.get(filePath);
364
+ if (prevHash === contentKey) {
365
+ duplicateWriteCount++;
366
+ if (duplicateWriteCount >= 2) {
367
+ 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.`);
368
+ duplicateWriteCount = 0;
369
+ }
370
+ else {
371
+ toolResults.push(`Tool ${toolCall.tool} succeeded (note: same content as previous write to this file):\n${toolResult.output}`);
372
+ }
373
+ }
374
+ else {
375
+ duplicateWriteCount = 0;
376
+ lastWriteHashByPath.set(filePath, contentKey);
377
+ if (toolResult.success) {
378
+ toolResults.push(`Tool ${toolCall.tool} succeeded:\n${toolResult.output}`);
379
+ }
380
+ else {
381
+ toolResults.push(`Tool ${toolCall.tool} failed:\n${toolResult.error || 'Unknown error'}`);
382
+ }
383
+ }
384
+ // ── Duplicate read cache ────────────────────────────────────────────
385
+ }
386
+ else if (toolCall.tool === 'read_file' && toolResult.success) {
387
+ const filePath = toolCall.parameters.path || '';
388
+ if (readCache.has(filePath)) {
389
+ toolResults.push(`Tool read_file succeeded (cached — file unchanged since last read):\n${readCache.get(filePath)}`);
390
+ }
391
+ else {
392
+ const truncated = truncateToolResult(toolResult.output, toolCall.tool);
393
+ readCache.set(filePath, truncated);
394
+ toolResults.push(`Tool read_file succeeded:\n${truncated}`);
395
+ }
396
+ // ── General truncation for other tools ─────────────────────────────
397
+ }
398
+ else if (toolResult.success) {
399
+ const truncated = truncateToolResult(toolResult.output, toolCall.tool);
400
+ toolResults.push(`Tool ${toolCall.tool} succeeded:\n${truncated}`);
300
401
  }
301
402
  else {
302
403
  toolResults.push(`Tool ${toolCall.tool} failed:\n${toolResult.error || 'Unknown error'}`);
303
404
  }
405
+ // Invalidate read cache when a file is written/edited
406
+ if ((toolCall.tool === 'write_file' || toolCall.tool === 'edit_file') && toolResult.success) {
407
+ const filePath = toolCall.parameters.path || '';
408
+ readCache.delete(filePath);
409
+ }
304
410
  }
305
411
  // Add tool results to messages
306
412
  messages.push({
@@ -308,13 +414,20 @@ export async function runAgent(prompt, projectContext, options = {}) {
308
414
  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
415
  });
310
416
  }
311
- // Check if we hit max iterations
417
+ // Check if we hit max iterations — build partial summary from actions log
312
418
  if (iteration >= opts.maxIterations && !finalResponse) {
419
+ const filesDone = actions.filter(a => a.type === 'write' || a.type === 'edit').map(a => a.target);
420
+ const partialLines = [`Agent reached the iteration limit (${opts.maxIterations} steps).`];
421
+ if (filesDone.length > 0) {
422
+ partialLines.push(`\n**Partial progress — files written/edited:**`);
423
+ [...new Set(filesDone)].forEach(f => partialLines.push(` ✓ \`${f}\``));
424
+ partialLines.push(`\nThe task may be incomplete. You can continue by running the agent again.`);
425
+ }
313
426
  result = {
314
427
  success: false,
315
428
  iterations: iteration,
316
429
  actions,
317
- finalResponse: 'Agent reached maximum iterations',
430
+ finalResponse: partialLines.join('\n'),
318
431
  error: `Exceeded maximum of ${opts.maxIterations} iterations`,
319
432
  };
320
433
  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.63",
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",