claude-evolve 1.9.4 → 1.9.6

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/lib/ai-cli.sh CHANGED
@@ -123,10 +123,16 @@ $prompt"
123
123
  ;;
124
124
  gemini-3-pro-preview)
125
125
  local ai_output
126
- # Gemini v3 Pro Preview via OpenRouter (30 minute timeout)
126
+ # Gemini v3 Pro Preview via OpenRouter (30 minute timeout) - EXPENSIVE
127
127
  ai_output=$(timeout -k 30 1800 opencode -m openrouter/google/gemini-3-pro-preview run "$prompt" 2>&1)
128
128
  local ai_exit_code=$?
129
129
  ;;
130
+ gemini-3-flash)
131
+ local ai_output
132
+ # Gemini 3 Flash - fast, cheap, strong thinker
133
+ ai_output=$(timeout -k 30 600 opencode -m openrouter/google/gemini-3-flash-preview run "$prompt" 2>&1)
134
+ local ai_exit_code=$?
135
+ ;;
130
136
  cursor-sonnet)
131
137
  local ai_output
132
138
  ai_output=$(timeout -k 30 600 cursor-agent sonnet-4.5 -p "$prompt" 2>&1)
@@ -160,9 +166,16 @@ $prompt"
160
166
  ;;
161
167
  grok-4-openrouter)
162
168
  local ai_output
169
+ # EXPENSIVE - consider grok-4.1-fast instead
163
170
  ai_output=$(timeout -k 30 600 opencode -m openrouter/x-ai/grok-4 run "$prompt" 2>&1)
164
171
  local ai_exit_code=$?
165
172
  ;;
173
+ grok-4.1-fast)
174
+ local ai_output
175
+ # Grok 4.1 Fast - close to Grok 4 quality, much cheaper
176
+ ai_output=$(timeout -k 30 600 opencode -m openrouter/x-ai/grok-4.1-fast run "$prompt" 2>&1)
177
+ local ai_exit_code=$?
178
+ ;;
166
179
  opus-openrouter)
167
180
  local ai_output
168
181
  ai_output=$(timeout -k 30 600 opencode -m openrouter/anthropic/claude-opus-4.1 run "$prompt" 2>&1)
package/lib/ai_cli.py CHANGED
@@ -5,11 +5,13 @@ AIDEV-NOTE: This keeps ai-cli.sh as the source of truth for model configs and ti
5
5
  """
6
6
 
7
7
  import os
8
+ import random
8
9
  import subprocess
9
10
  import sys
10
11
  import tempfile
12
+ import time
11
13
  from pathlib import Path
12
- from typing import Optional, Tuple
14
+ from typing import Optional, Tuple, List
13
15
 
14
16
  # Path to ai-cli.sh relative to this file
15
17
  SCRIPT_DIR = Path(__file__).parent
@@ -155,6 +157,178 @@ def call_ai(
155
157
  raise AIError(f"Failed to call AI: {e}")
156
158
 
157
159
 
160
+ def get_models_for_command(command: str) -> List[str]:
161
+ """
162
+ Get the list of available models for a command.
163
+
164
+ Args:
165
+ command: Either "run" or "ideate"
166
+
167
+ Returns:
168
+ List of model names
169
+ """
170
+ bash_script = f'''
171
+ source "{SCRIPT_DIR}/config.sh"
172
+ load_config
173
+ case "$1" in
174
+ run) echo "$LLM_RUN" ;;
175
+ ideate) echo "$LLM_IDEATE" ;;
176
+ esac
177
+ '''
178
+
179
+ result = subprocess.run(
180
+ ["bash", "-c", bash_script, "bash", command],
181
+ capture_output=True,
182
+ text=True
183
+ )
184
+
185
+ if result.returncode != 0:
186
+ return []
187
+
188
+ model_list = result.stdout.strip()
189
+ if not model_list:
190
+ return []
191
+
192
+ return model_list.split()
193
+
194
+
195
+ def call_ai_model(
196
+ prompt: str,
197
+ model_name: str,
198
+ working_dir: Optional[str] = None,
199
+ env_vars: Optional[dict] = None
200
+ ) -> Tuple[str, str]:
201
+ """
202
+ Call a specific AI model.
203
+
204
+ Args:
205
+ prompt: The prompt to send to the AI
206
+ model_name: The specific model to use
207
+ working_dir: Directory to run the command in
208
+ env_vars: Additional environment variables
209
+
210
+ Returns:
211
+ Tuple of (output, model_name)
212
+
213
+ Raises:
214
+ TimeoutError, RateLimitError, APIExhaustedError, AIError
215
+ """
216
+ bash_script = f'''
217
+ source "{SCRIPT_DIR}/config.sh"
218
+ load_config
219
+ source "{AI_CLI_PATH}"
220
+ call_ai_model_configured "$1" "$2"
221
+ '''
222
+
223
+ env = os.environ.copy()
224
+ if working_dir:
225
+ env['CLAUDE_EVOLVE_WORKING_DIR'] = working_dir
226
+ if env_vars:
227
+ env.update(env_vars)
228
+
229
+ try:
230
+ result = subprocess.run(
231
+ ["bash", "-c", bash_script, "bash", model_name, prompt],
232
+ capture_output=True,
233
+ text=True,
234
+ cwd=working_dir,
235
+ env=env
236
+ )
237
+
238
+ output = result.stdout
239
+ stderr = result.stderr
240
+ exit_code = result.returncode
241
+
242
+ # Print stderr (contains debug info)
243
+ if stderr:
244
+ for line in stderr.strip().split('\n'):
245
+ if line:
246
+ print(f" {line}", file=sys.stderr)
247
+
248
+ # Handle exit codes
249
+ if exit_code == 124:
250
+ raise TimeoutError(f"AI call timed out (model: {model_name})")
251
+ elif exit_code == 2:
252
+ raise RateLimitError(f"Rate limit hit (model: {model_name})")
253
+ elif exit_code == 3:
254
+ raise APIExhaustedError(f"API quota exhausted (model: {model_name})")
255
+ elif exit_code != 0:
256
+ raise AIError(f"AI call failed with exit code {exit_code}: {stderr}")
257
+
258
+ return output, model_name
259
+
260
+ except subprocess.SubprocessError as e:
261
+ raise AIError(f"Failed to call AI: {e}")
262
+
263
+
264
+ def call_ai_with_backoff(
265
+ prompt: str,
266
+ command: str = "ideate",
267
+ working_dir: Optional[str] = None,
268
+ env_vars: Optional[dict] = None,
269
+ max_rounds: int = 10,
270
+ initial_wait: int = 60,
271
+ max_wait: int = 600
272
+ ) -> Tuple[str, str]:
273
+ """
274
+ Call AI with round-based retries and exponential backoff.
275
+
276
+ AIDEV-NOTE: This is the robust retry mechanism for handling rate limits.
277
+ - Tries each model in the pool (shuffled order)
278
+ - If all models fail in a round, waits with exponential backoff
279
+ - Keeps going until success or max_rounds exhausted
280
+
281
+ Args:
282
+ prompt: The prompt to send
283
+ command: "run" or "ideate" - determines model pool
284
+ working_dir: Directory for file operations
285
+ env_vars: Additional environment variables
286
+ max_rounds: Maximum number of full rounds to attempt
287
+ initial_wait: Initial wait time in seconds after first failed round
288
+ max_wait: Maximum wait time in seconds between rounds
289
+
290
+ Returns:
291
+ Tuple of (output, model_name)
292
+
293
+ Raises:
294
+ AIError: If all rounds exhausted without success
295
+ """
296
+ models = get_models_for_command(command)
297
+ if not models:
298
+ raise AIError(f"No models configured for command: {command}")
299
+
300
+ wait_time = initial_wait
301
+ last_errors = {}
302
+
303
+ for round_num in range(max_rounds):
304
+ # Shuffle models each round for fairness
305
+ shuffled_models = models.copy()
306
+ random.shuffle(shuffled_models)
307
+
308
+ print(f"[AI] Round {round_num + 1}/{max_rounds}: trying {len(shuffled_models)} models", file=sys.stderr)
309
+
310
+ for model in shuffled_models:
311
+ try:
312
+ output, model_name = call_ai_model(prompt, model, working_dir, env_vars)
313
+ if round_num > 0:
314
+ print(f"[AI] Succeeded on round {round_num + 1} with {model}", file=sys.stderr)
315
+ return output, model_name
316
+ except AIError as e:
317
+ last_errors[model] = str(e)
318
+ # Continue to next model
319
+
320
+ # All models failed in this round
321
+ if round_num < max_rounds - 1:
322
+ print(f"[AI] All models failed in round {round_num + 1}, waiting {wait_time}s before retry...", file=sys.stderr)
323
+ time.sleep(wait_time)
324
+ # Exponential backoff: 60 -> 120 -> 240 -> 480 (capped at max_wait)
325
+ wait_time = min(wait_time * 2, max_wait)
326
+
327
+ # All rounds exhausted
328
+ error_summary = "; ".join(f"{m}: {e[:50]}" for m, e in list(last_errors.items())[:3])
329
+ raise AIError(f"All {max_rounds} rounds exhausted. Last errors: {error_summary}")
330
+
331
+
158
332
  def call_ai_for_file_edit(
159
333
  prompt: str,
160
334
  file_path: str,
package/lib/config.sh CHANGED
@@ -58,9 +58,11 @@ DEFAULT_MEMORY_LIMIT_MB=12288
58
58
  DEFAULT_WORKER_MAX_CANDIDATES=3
59
59
 
60
60
  # Default LLM CLI configuration
61
- DEFAULT_LLM_RUN="glm-zai glm-zai glm-zai glm-zai glm-zai kimi-coder codex-oss-local haiku"
61
+ DEFAULT_LLM_RUN="glm-zai glm-zai glm-zai glm-zai glm-zai kimi-coder gemini-3-flash codex-oss-local haiku"
62
62
  # Ideate: Commercial models for idea generation + local fallback
63
- DEFAULT_LLM_IDEATE="opus-think kimi-k2-openrouter gemini-3-pro-preview gpt5high grok-4-openrouter deepseek-openrouter glm-zai"
63
+ # Removed: gemini-3-pro-preview (expensive), grok-4-openrouter (expensive)
64
+ # Added: gemini-3-flash (cheap thinker), grok-4.1-fast (cheaper than grok-4)
65
+ DEFAULT_LLM_IDEATE="opus-think kimi-k2-openrouter gemini-3-flash gpt5high grok-4.1-fast deepseek-openrouter glm-zai"
64
66
 
65
67
  # Load configuration from a YAML file and update variables
66
68
  _load_yaml_config() {
@@ -23,7 +23,7 @@ SCRIPT_DIR = Path(__file__).parent
23
23
  sys.path.insert(0, str(SCRIPT_DIR.parent))
24
24
 
25
25
  from lib.evolution_csv import EvolutionCSV
26
- from lib.ai_cli import call_ai, get_git_protection_warning, AIError
26
+ from lib.ai_cli import call_ai_with_backoff, get_git_protection_warning, AIError
27
27
  from lib.embedding import check_novelty as check_embedding_novelty, get_embedding, set_cache_file, save_cache
28
28
 
29
29
 
@@ -47,6 +47,13 @@ class IdeationConfig:
47
47
  novelty_enabled: bool = True
48
48
  novelty_threshold: float = 0.92
49
49
 
50
+ # Retry configuration with exponential backoff
51
+ # AIDEV-NOTE: This implements round-based retries like the shell version.
52
+ # Each round tries ALL models. If all fail, wait and retry.
53
+ max_rounds: int = 10 # Max full rounds of all models
54
+ initial_wait: int = 60 # Seconds to wait after first failed round
55
+ max_wait: int = 600 # Max wait between rounds (10 minutes)
56
+
50
57
 
51
58
  @dataclass
52
59
  class Idea:
@@ -85,8 +92,13 @@ class IdeationStrategy(ABC):
85
92
  """Build the AI prompt."""
86
93
  pass
87
94
 
88
- def generate(self, context: IdeationContext, count: int) -> List[Idea]:
89
- """Generate ideas using this strategy."""
95
+ def generate(self, context: IdeationContext, count: int,
96
+ max_rounds: int = 10, initial_wait: int = 60, max_wait: int = 600) -> List[Idea]:
97
+ """Generate ideas using this strategy with round-based retry and backoff.
98
+
99
+ AIDEV-NOTE: Uses call_ai_with_backoff for robust retry handling.
100
+ Each round tries ALL models. If all fail, waits with exponential backoff.
101
+ """
90
102
  if count <= 0:
91
103
  return []
92
104
 
@@ -110,8 +122,15 @@ class IdeationStrategy(ABC):
110
122
  # Build prompt
111
123
  prompt = self.build_prompt(context, ids, temp_csv.name)
112
124
 
113
- # Call AI
114
- output, model = call_ai(prompt, command="ideate", working_dir=self.config.evolution_dir)
125
+ # Call AI with round-based retry and backoff
126
+ output, model = call_ai_with_backoff(
127
+ prompt,
128
+ command="ideate",
129
+ working_dir=self.config.evolution_dir,
130
+ max_rounds=max_rounds,
131
+ initial_wait=initial_wait,
132
+ max_wait=max_wait
133
+ )
115
134
 
116
135
  # Parse results from modified CSV
117
136
  ideas = self._parse_results(temp_csv, ids)
@@ -120,12 +139,15 @@ class IdeationStrategy(ABC):
120
139
  # Record model used
121
140
  for idea in ideas:
122
141
  idea.strategy = f"{self.name} ({model})"
123
-
124
- return ideas
142
+ return ideas
143
+ else:
144
+ print(f"[IDEATE] AI completed but no ideas parsed from output", file=sys.stderr)
145
+ return []
125
146
 
126
147
  except AIError as e:
127
- print(f"[IDEATE] AI error in {self.name}: {e}", file=sys.stderr)
148
+ print(f"[IDEATE] All retries exhausted in {self.name}: {e}", file=sys.stderr)
128
149
  return []
150
+
129
151
  finally:
130
152
  temp_csv.unlink(missing_ok=True)
131
153
 
@@ -382,7 +404,12 @@ class Ideator:
382
404
  if count <= 0:
383
405
  continue
384
406
 
385
- ideas = strategy.generate(context, count)
407
+ ideas = strategy.generate(
408
+ context, count,
409
+ max_rounds=self.config.max_rounds,
410
+ initial_wait=self.config.initial_wait,
411
+ max_wait=self.config.max_wait
412
+ )
386
413
 
387
414
  if ideas:
388
415
  strategies_succeeded += 1
@@ -472,7 +499,10 @@ def load_config(config_path: Optional[str] = None) -> IdeationConfig:
472
499
  crossover_hybrid=ideation.get('crossover_hybrid', 4),
473
500
  num_elites=ideation.get('num_elites', 3),
474
501
  novelty_enabled=novelty.get('enabled', True),
475
- novelty_threshold=novelty.get('threshold', 0.92)
502
+ novelty_threshold=novelty.get('threshold', 0.92),
503
+ max_rounds=ideation.get('max_rounds', 10),
504
+ initial_wait=ideation.get('initial_wait', 60),
505
+ max_wait=ideation.get('max_wait', 600)
476
506
  )
477
507
 
478
508
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-evolve",
3
- "version": "1.9.4",
3
+ "version": "1.9.6",
4
4
  "bin": {
5
5
  "claude-evolve": "bin/claude-evolve",
6
6
  "claude-evolve-main": "bin/claude-evolve-main",