codeep 1.0.20 → 1.0.22

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.
@@ -28,7 +28,7 @@ const SETTINGS = [
28
28
  value: () => config.get('apiTimeout'),
29
29
  type: 'number',
30
30
  min: 5000,
31
- max: 120000,
31
+ max: 300000,
32
32
  step: 5000,
33
33
  },
34
34
  {
@@ -69,6 +69,15 @@ const SETTINGS = [
69
69
  { value: 'manual', label: 'Manual' },
70
70
  ],
71
71
  },
72
+ {
73
+ key: 'agentApiTimeout',
74
+ label: 'Agent API Timeout (ms)',
75
+ value: () => config.get('agentApiTimeout'),
76
+ type: 'number',
77
+ min: 30000,
78
+ max: 300000,
79
+ step: 10000,
80
+ },
72
81
  ];
73
82
  export const Settings = ({ onClose, notify }) => {
74
83
  const [selected, setSelected] = useState(0);
@@ -37,6 +37,7 @@ interface ConfigSchema {
37
37
  agentMaxFixAttempts: number;
38
38
  agentMaxIterations: number;
39
39
  agentMaxDuration: number;
40
+ agentApiTimeout: number;
40
41
  projectPermissions: ProjectPermission[];
41
42
  providerApiKeys: ProviderApiKey[];
42
43
  }
@@ -68,6 +68,7 @@ export const config = new Conf({
68
68
  agentMaxFixAttempts: 3,
69
69
  agentMaxIterations: 100,
70
70
  agentMaxDuration: 20, // minutes
71
+ agentApiTimeout: 90000, // 90 seconds base timeout for agent (dynamically adjusted)
71
72
  protocol: 'openai',
72
73
  plan: 'lite',
73
74
  language: 'en',
@@ -75,7 +76,7 @@ export const config = new Conf({
75
76
  currentSessionId: '',
76
77
  temperature: 0.7,
77
78
  maxTokens: 4096,
78
- apiTimeout: 30000,
79
+ apiTimeout: 60000,
79
80
  rateLimitApi: 30, // 30 requests per minute
80
81
  rateLimitCommands: 100, // 100 commands per minute
81
82
  projectPermissions: [],
@@ -1,6 +1,61 @@
1
1
  /**
2
2
  * Agent loop - autonomous task execution
3
3
  */
4
+ /**
5
+ * Custom error class for timeout - allows distinguishing from user abort
6
+ */
7
+ class TimeoutError extends Error {
8
+ constructor(message = 'Request timed out') {
9
+ super(message);
10
+ this.name = 'TimeoutError';
11
+ }
12
+ }
13
+ /**
14
+ * Calculate dynamic timeout based on task complexity
15
+ * Complex tasks (creating pages, multiple files) need more time
16
+ */
17
+ function calculateDynamicTimeout(prompt, iteration, baseTimeout) {
18
+ const promptLower = prompt.toLowerCase();
19
+ // Keywords that indicate complex tasks requiring more time
20
+ const complexKeywords = [
21
+ 'create', 'build', 'implement', 'generate', 'make', 'develop', 'setup',
22
+ 'website', 'app', 'application', 'page', 'component', 'feature',
23
+ 'kreiraj', 'napravi', 'izgradi', 'generiraj', 'razvij', 'stranica', 'aplikacija'
24
+ ];
25
+ // Keywords indicating very large tasks
26
+ const veryComplexKeywords = [
27
+ 'full', 'complete', 'entire', 'whole', 'multiple', 'all',
28
+ 'cijeli', 'kompletni', 'sve', 'višestruki'
29
+ ];
30
+ let multiplier = 1.0;
31
+ // Check for complex keywords
32
+ const hasComplexKeyword = complexKeywords.some(kw => promptLower.includes(kw));
33
+ if (hasComplexKeyword) {
34
+ multiplier = 2.0; // Double timeout for complex tasks
35
+ }
36
+ // Check for very complex keywords
37
+ const hasVeryComplexKeyword = veryComplexKeywords.some(kw => promptLower.includes(kw));
38
+ if (hasVeryComplexKeyword) {
39
+ multiplier = 3.0; // Triple timeout for very complex tasks
40
+ }
41
+ // Long prompts usually mean complex tasks
42
+ if (prompt.length > 200) {
43
+ multiplier = Math.max(multiplier, 2.0);
44
+ }
45
+ if (prompt.length > 500) {
46
+ multiplier = Math.max(multiplier, 3.0);
47
+ }
48
+ // Later iterations might need more time (context is larger)
49
+ if (iteration > 5) {
50
+ multiplier *= 1.2;
51
+ }
52
+ if (iteration > 10) {
53
+ multiplier *= 1.3;
54
+ }
55
+ // Minimum 60 seconds, maximum 5 minutes for a single API call
56
+ const calculatedTimeout = baseTimeout * multiplier;
57
+ return Math.min(Math.max(calculatedTimeout, 60000), 300000);
58
+ }
4
59
  import { parseToolCalls, executeTool, createActionLog, formatToolDefinitions, getOpenAITools, getAnthropicTools, parseOpenAIToolCalls, parseAnthropicToolCalls } from './tools.js';
5
60
  import { config, getApiKey } from '../config/index.js';
6
61
  import { getProviderBaseUrl, getProviderAuthHeader, supportsNativeTools } from '../config/providers.js';
@@ -123,7 +178,7 @@ You have FULL access. Execute tasks autonomously.`;
123
178
  /**
124
179
  * Make a chat API call for agent mode with native tool support
125
180
  */
126
- async function agentChat(messages, systemPrompt, onChunk, abortSignal) {
181
+ async function agentChat(messages, systemPrompt, onChunk, abortSignal, dynamicTimeout) {
127
182
  const protocol = config.get('protocol');
128
183
  const model = config.get('model');
129
184
  const apiKey = getApiKey();
@@ -139,9 +194,17 @@ async function agentChat(messages, systemPrompt, onChunk, abortSignal) {
139
194
  return await agentChatFallback(messages, systemPrompt, onChunk, abortSignal);
140
195
  }
141
196
  const controller = new AbortController();
142
- const timeout = setTimeout(() => controller.abort(), config.get('apiTimeout'));
197
+ const timeoutMs = dynamicTimeout || config.get('apiTimeout');
198
+ let isTimeout = false;
199
+ const timeout = setTimeout(() => {
200
+ isTimeout = true;
201
+ controller.abort();
202
+ }, timeoutMs);
143
203
  if (abortSignal) {
144
- abortSignal.addEventListener('abort', () => controller.abort());
204
+ abortSignal.addEventListener('abort', () => {
205
+ isTimeout = false; // User abort, not timeout
206
+ controller.abort();
207
+ });
145
208
  }
146
209
  const headers = {
147
210
  'Content-Type': 'application/json',
@@ -224,6 +287,14 @@ async function agentChat(messages, systemPrompt, onChunk, abortSignal) {
224
287
  }
225
288
  catch (error) {
226
289
  const err = error;
290
+ // Check if this was a timeout vs user abort
291
+ if (err.name === 'AbortError') {
292
+ if (isTimeout) {
293
+ throw new TimeoutError(`API request timed out after ${timeoutMs}ms`);
294
+ }
295
+ // User abort - rethrow as-is
296
+ throw error;
297
+ }
227
298
  // If native tools failed, try fallback
228
299
  if (err.message.includes('tools') || err.message.includes('function')) {
229
300
  return await agentChatFallback(messages, systemPrompt, onChunk, abortSignal);
@@ -237,7 +308,7 @@ async function agentChat(messages, systemPrompt, onChunk, abortSignal) {
237
308
  /**
238
309
  * Fallback chat without native tools (text-based parsing)
239
310
  */
240
- async function agentChatFallback(messages, systemPrompt, onChunk, abortSignal) {
311
+ async function agentChatFallback(messages, systemPrompt, onChunk, abortSignal, dynamicTimeout) {
241
312
  const protocol = config.get('protocol');
242
313
  const model = config.get('model');
243
314
  const apiKey = getApiKey();
@@ -248,9 +319,17 @@ async function agentChatFallback(messages, systemPrompt, onChunk, abortSignal) {
248
319
  throw new Error(`Provider ${providerId} does not support ${protocol} protocol`);
249
320
  }
250
321
  const controller = new AbortController();
251
- const timeout = setTimeout(() => controller.abort(), config.get('apiTimeout'));
322
+ const timeoutMs = dynamicTimeout || config.get('apiTimeout');
323
+ let isTimeout = false;
324
+ const timeout = setTimeout(() => {
325
+ isTimeout = true;
326
+ controller.abort();
327
+ }, timeoutMs);
252
328
  if (abortSignal) {
253
- abortSignal.addEventListener('abort', () => controller.abort());
329
+ abortSignal.addEventListener('abort', () => {
330
+ isTimeout = false; // User abort, not timeout
331
+ controller.abort();
332
+ });
254
333
  }
255
334
  const headers = {
256
335
  'Content-Type': 'application/json',
@@ -325,6 +404,18 @@ async function agentChatFallback(messages, systemPrompt, onChunk, abortSignal) {
325
404
  const toolCalls = parseToolCalls(content);
326
405
  return { content, toolCalls, usedNativeTools: false };
327
406
  }
407
+ catch (error) {
408
+ const err = error;
409
+ // Check if this was a timeout vs user abort
410
+ if (err.name === 'AbortError') {
411
+ if (isTimeout) {
412
+ throw new TimeoutError(`API request timed out after ${timeoutMs}ms`);
413
+ }
414
+ // User abort - rethrow as-is
415
+ throw error;
416
+ }
417
+ throw error;
418
+ }
328
419
  finally {
329
420
  clearTimeout(timeout);
330
421
  }
@@ -441,6 +532,9 @@ export async function runAgent(prompt, projectContext, options = {}) {
441
532
  let iteration = 0;
442
533
  let finalResponse = '';
443
534
  let result;
535
+ let consecutiveTimeouts = 0;
536
+ const maxTimeoutRetries = 3;
537
+ const baseTimeout = config.get('agentApiTimeout');
444
538
  try {
445
539
  while (iteration < opts.maxIterations) {
446
540
  // Check timeout
@@ -469,24 +563,69 @@ export async function runAgent(prompt, projectContext, options = {}) {
469
563
  iteration++;
470
564
  console.error(`[DEBUG] Starting iteration ${iteration}/${opts.maxIterations}, actions: ${actions.length}`);
471
565
  opts.onIteration?.(iteration, `Iteration ${iteration}/${opts.maxIterations}`);
472
- // Get AI response
566
+ // Calculate dynamic timeout based on task complexity
567
+ const dynamicTimeout = calculateDynamicTimeout(prompt, iteration, baseTimeout);
568
+ console.error(`[DEBUG] Using dynamic timeout: ${dynamicTimeout}ms (base: ${baseTimeout}ms, iteration: ${iteration})`);
569
+ // Get AI response with retry logic for timeouts
473
570
  let chatResponse;
474
- try {
475
- chatResponse = await agentChat(messages, systemPrompt, opts.onThinking, opts.abortSignal);
476
- }
477
- catch (error) {
478
- const err = error;
479
- if (err.name === 'AbortError') {
480
- result = {
481
- success: false,
482
- iterations: iteration,
483
- actions,
484
- finalResponse: 'Agent was stopped',
485
- aborted: true,
486
- };
487
- return result;
571
+ let retryCount = 0;
572
+ while (true) {
573
+ try {
574
+ chatResponse = await agentChat(messages, systemPrompt, opts.onThinking, opts.abortSignal, dynamicTimeout * (1 + retryCount * 0.5) // Increase timeout on retry
575
+ );
576
+ consecutiveTimeouts = 0; // Reset on success
577
+ break;
578
+ }
579
+ catch (error) {
580
+ const err = error;
581
+ // Handle user abort (not timeout)
582
+ if (err.name === 'AbortError') {
583
+ result = {
584
+ success: false,
585
+ iterations: iteration,
586
+ actions,
587
+ finalResponse: 'Agent was stopped by user',
588
+ aborted: true,
589
+ };
590
+ return result;
591
+ }
592
+ // Handle timeout with retry
593
+ if (err.name === 'TimeoutError') {
594
+ retryCount++;
595
+ consecutiveTimeouts++;
596
+ console.error(`[DEBUG] Timeout occurred (retry ${retryCount}/${maxTimeoutRetries}, consecutive: ${consecutiveTimeouts})`);
597
+ opts.onIteration?.(iteration, `API timeout, retrying (${retryCount}/${maxTimeoutRetries})...`);
598
+ if (retryCount >= maxTimeoutRetries) {
599
+ // Too many retries for this iteration
600
+ if (consecutiveTimeouts >= maxTimeoutRetries * 2) {
601
+ // Too many consecutive timeouts overall, give up
602
+ result = {
603
+ success: false,
604
+ iterations: iteration,
605
+ actions,
606
+ finalResponse: 'Agent stopped due to repeated API timeouts',
607
+ error: `API timed out ${consecutiveTimeouts} times consecutively. The task may be too complex or the API is overloaded.`,
608
+ };
609
+ return result;
610
+ }
611
+ // Skip this iteration and try next
612
+ console.error(`[DEBUG] Max retries reached, skipping to next iteration`);
613
+ messages.push({
614
+ role: 'user',
615
+ content: 'The previous request timed out. Please continue with the task, using simpler responses if needed.'
616
+ });
617
+ break;
618
+ }
619
+ // Wait before retry (exponential backoff)
620
+ await new Promise(resolve => setTimeout(resolve, 1000 * retryCount));
621
+ continue;
622
+ }
623
+ throw error;
488
624
  }
489
- throw error;
625
+ }
626
+ // If we broke out due to max retries without a response, continue to next iteration
627
+ if (!chatResponse) {
628
+ continue;
490
629
  }
491
630
  let { content, toolCalls, usedNativeTools } = chatResponse;
492
631
  // If native tools were used but no tool calls returned, try parsing text-based tool calls
@@ -196,9 +196,6 @@ export declare function getOpenAITools(): OpenAITool[];
196
196
  * Get tools in Anthropic Tool Use format
197
197
  */
198
198
  export declare function getAnthropicTools(): AnthropicTool[];
199
- /**
200
- * Parse tool calls from OpenAI response
201
- */
202
199
  export declare function parseOpenAIToolCalls(toolCalls: any[]): ToolCall[];
203
200
  /**
204
201
  * Parse tool calls from Anthropic response
@@ -174,6 +174,33 @@ export function getAnthropicTools() {
174
174
  /**
175
175
  * Parse tool calls from OpenAI response
176
176
  */
177
+ /**
178
+ * Normalize tool name to lowercase with underscores
179
+ */
180
+ function normalizeToolName(name) {
181
+ const toolNameMap = {
182
+ 'executecommand': 'execute_command',
183
+ 'execute_command': 'execute_command',
184
+ 'readfile': 'read_file',
185
+ 'read_file': 'read_file',
186
+ 'writefile': 'write_file',
187
+ 'write_file': 'write_file',
188
+ 'editfile': 'edit_file',
189
+ 'edit_file': 'edit_file',
190
+ 'deletefile': 'delete_file',
191
+ 'delete_file': 'delete_file',
192
+ 'listfiles': 'list_files',
193
+ 'list_files': 'list_files',
194
+ 'searchcode': 'search_code',
195
+ 'search_code': 'search_code',
196
+ 'createdirectory': 'create_directory',
197
+ 'create_directory': 'create_directory',
198
+ 'fetchurl': 'fetch_url',
199
+ 'fetch_url': 'fetch_url',
200
+ };
201
+ const lower = name.toLowerCase().replace(/-/g, '_');
202
+ return toolNameMap[lower] || lower;
203
+ }
177
204
  export function parseOpenAIToolCalls(toolCalls) {
178
205
  if (!toolCalls || !Array.isArray(toolCalls))
179
206
  return [];
@@ -186,7 +213,7 @@ export function parseOpenAIToolCalls(toolCalls) {
186
213
  parameters = {};
187
214
  }
188
215
  return {
189
- tool: tc.function?.name || '',
216
+ tool: normalizeToolName(tc.function?.name || ''),
190
217
  parameters,
191
218
  id: tc.id,
192
219
  };
@@ -201,7 +228,7 @@ export function parseAnthropicToolCalls(content) {
201
228
  return content
202
229
  .filter(block => block.type === 'tool_use')
203
230
  .map(block => ({
204
- tool: block.name || '',
231
+ tool: normalizeToolName(block.name || ''),
205
232
  parameters: block.input || {},
206
233
  id: block.id,
207
234
  }))
@@ -386,7 +413,7 @@ function tryParseToolCall(str) {
386
413
  const parsed = JSON.parse(cleaned);
387
414
  if (parsed.tool && typeof parsed.tool === 'string') {
388
415
  return {
389
- tool: parsed.tool,
416
+ tool: normalizeToolName(parsed.tool),
390
417
  parameters: parsed.parameters || {},
391
418
  id: parsed.id,
392
419
  };
@@ -394,9 +421,9 @@ function tryParseToolCall(str) {
394
421
  }
395
422
  catch {
396
423
  // Try to extract tool name and parameters manually for malformed JSON
397
- const toolMatch = str.match(/"tool"\s*:\s*"([^"]+)"/);
424
+ const toolMatch = str.match(/"tool"\s*:\s*"([^"]+)"/i);
398
425
  if (toolMatch) {
399
- const tool = toolMatch[1];
426
+ const tool = normalizeToolName(toolMatch[1]);
400
427
  const params = {};
401
428
  // Extract simple string parameters
402
429
  const paramMatches = str.matchAll(/"(\w+)"\s*:\s*"([^"]*)"/g);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "codeep",
3
- "version": "1.0.20",
3
+ "version": "1.0.22",
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",