@@ -327,9 +327,9 @@ fn log_api_error(
327327const DEFAULT_MODEL : & str = "anthropic/claude-sonnet-4" ;
328328const DEFAULT_OLLAMA_API_BASE : & str = "http://localhost:11434" ;
329329
330- // Percentage of max_iterations after which we require at least one edit/build_check.
331- // Example: with max_iterations =50 and this set to 75, threshold is 37 iterations .
332- const MAX_ITERATIONS_BEFORE_EDIT_PERCENT : usize = 75 ;
330+ // Percentage of the token budget after which we require at least one edit/build_check.
331+ // Example: with maxTokenBudget =50,000 and this set to 75, threshold is 37,500 tokens .
332+ const MAX_TOKEN_BUDGET_BEFORE_EDIT_PERCENT : u32 = 75 ;
333333
334334// Applied separately to stdout and stderr. So when thinking about tokens,
335335// the effective output limit could be up to double this if both are long.
@@ -690,22 +690,26 @@ pub async fn generate_evolution<R: Runtime>(
690690
691691 // Read configurable limits from store (hot-reloaded on every run).
692692 let config:: EvolutionLimits {
693- max_iterations,
694693 max_build_attempts,
694+ ..
695695 } = config:: EvolutionLimits :: load ( app)
696696 . inspect_err ( |e| warn ! ( "EvolutionLimits::load failed ({e}); using defaults" ) )
697697 . unwrap_or_default ( ) ;
698- let max_iterations_before_edit = std:: cmp:: max (
698+ let legacy_max_iterations =
699+ store:: get_max_iterations ( app) . unwrap_or ( store:: DEFAULT_MAX_ITERATIONS ) ;
700+ let max_token_budget =
701+ store:: get_max_token_budget ( app) . unwrap_or ( store:: DEFAULT_MAX_TOKEN_BUDGET ) ;
702+ let max_tokens_before_edit = std:: cmp:: max (
699703 1 ,
700- ( max_iterations * MAX_ITERATIONS_BEFORE_EDIT_PERCENT ) / 100 ,
704+ ( max_token_budget * MAX_TOKEN_BUDGET_BEFORE_EDIT_PERCENT ) / 100 ,
701705 ) ;
702706 info ! (
703- "Limits: max_iterations ={}, max_iterations_before_edit ={} ({}%), max_build_attempts={}, max_output_tokens ={}" ,
704- max_iterations ,
705- max_iterations_before_edit ,
706- MAX_ITERATIONS_BEFORE_EDIT_PERCENT ,
707+ "Limits: max_token_budget ={}, max_tokens_before_edit ={} ({}%), max_build_attempts={}, legacy_max_iterations ={}" ,
708+ max_token_budget ,
709+ max_tokens_before_edit ,
710+ MAX_TOKEN_BUDGET_BEFORE_EDIT_PERCENT ,
707711 max_build_attempts,
708- max_output_tokens
712+ legacy_max_iterations ,
709713 ) ;
710714
711715 let tools = create_tools ( banned_tools) ;
@@ -719,6 +723,7 @@ pub async fn generate_evolution<R: Runtime>(
719723 let mut build_attempts: usize = 0 ;
720724 let mut build_verified = false ;
721725 let mut total_tokens: u32 = 0 ;
726+ let mut token_usage_observed = false ;
722727 let chat_memory_store = session_chat_memory_store ( ) ;
723728
724729 // Restore only persisted conversational history (user/assistant, NOT tool)
@@ -923,14 +928,21 @@ pub async fn generate_evolution<R: Runtime>(
923928
924929 // Track token usage
925930 if let Some ( usage) = & response. usage {
926- total_tokens += usage. total ;
931+ token_usage_observed = true ;
932+ total_tokens = total_tokens. saturating_add ( usage. total ) ;
927933 info ! (
928- "📊 Tokens | this_call: {} (in={}, out={}) | total_session: {}" ,
929- usage. total, usage. input, usage. output, total_tokens
934+ "📊 Tokens | this_call: {} (in={}, out={}) | total_session: {}/{} " ,
935+ usage. total, usage. input, usage. output, total_tokens, max_token_budget
930936 ) ;
931937 emit_evolve_event (
932938 app,
933- EvolveEvent :: api_response ( start_time, iteration, usage. total ) ,
939+ EvolveEvent :: api_response (
940+ start_time,
941+ iteration,
942+ usage. total ,
943+ total_tokens,
944+ max_token_budget,
945+ ) ,
934946 ) ;
935947 }
936948
@@ -1298,27 +1310,58 @@ Do not invent tool names and do not place tool invocations in assistant content.
12981310 break ;
12991311 }
13001312
1301- // Safety limits -- Max Iterations Before Edit Check
1302- if iteration == max_iterations_before_edit && !( made_edit || made_build_check) {
1313+ // Safety limits -- Max Token Budget
1314+ if total_tokens >= max_token_budget {
1315+ warn ! (
1316+ "⚠️ Evolution reached token budget ({}/{}) - aborting" ,
1317+ total_tokens, max_token_budget
1318+ ) ;
1319+ evolution. state = EvolutionState :: Failed ;
1320+ let stop_reason = format ! (
1321+ "Token budget exhausted ({} of {} tokens)" ,
1322+ total_tokens, max_token_budget
1323+ ) ;
1324+ emit_evolve_event (
1325+ app,
1326+ EvolveEvent :: error ( start_time, Some ( iteration) , & stop_reason, & stop_reason) ,
1327+ ) ;
1328+ // Track failure
1329+ if let Err ( e) = statistics:: record_evolution_failure ( app, iteration) {
1330+ warn ! ( "Failed to record evolution failure stats: {}" , e) ;
1331+ }
1332+ return Err ( EvolutionRunError :: from_state (
1333+ format ! (
1334+ "Evolution stopped because the token budget was exhausted ({} of {} tokens)" ,
1335+ total_tokens, max_token_budget
1336+ ) ,
1337+ & evolution,
1338+ iteration,
1339+ build_attempts,
1340+ total_tokens,
1341+ )
1342+ . into ( ) ) ;
1343+ }
1344+
1345+ // Safety limits -- Token Budget Before Edit Check
1346+ if total_tokens >= max_tokens_before_edit && !made_edit_or_build_check {
13031347 warn ! (
1304- "⚠️ No edit or build_check by iteration {} - agent not making progress" ,
1305- max_iterations_before_edit
1348+ "⚠️ No edit or build_check after {} tokens - agent not making progress" ,
1349+ total_tokens
13061350 ) ;
13071351 evolution. state = EvolutionState :: Failed ;
13081352 let message = format ! (
1309- "I've analyzed your configuration for {} iterations but haven't started making concrete changes yet. \
1353+ "I've analyzed your configuration for {} tokens but haven't started making concrete changes yet. \
13101354 This suggests I'm having difficulty understanding what modifications you'd like. \
13111355 Could you provide more specific guidance on what aspects of your configuration need adjustment?",
1312- max_iterations_before_edit
1356+ total_tokens
1357+ ) ;
1358+ let stop_reason = format ! (
1359+ "No concrete progress after {} of {} token budget" ,
1360+ total_tokens, max_token_budget
13131361 ) ;
13141362 emit_evolve_event (
13151363 app,
1316- EvolveEvent :: error (
1317- start_time,
1318- Some ( iteration) ,
1319- & format ! ( "Maximum iterations exceeded ({})" , max_iterations) ,
1320- & format ! ( "Maximum iterations exceeded ({})" , max_iterations) ,
1321- ) ,
1364+ EvolveEvent :: error ( start_time, Some ( iteration) , & stop_reason, & stop_reason) ,
13221365 ) ;
13231366 // Track failure
13241367 if let Err ( e) = statistics:: record_evolution_failure ( app, iteration) {
@@ -1334,28 +1377,27 @@ Could you provide more specific guidance on what aspects of your configuration n
13341377 . into ( ) ) ;
13351378 }
13361379
1337- // Safety limits -- Max Iterations
1338- if iteration >= max_iterations {
1380+ // Safety limits -- Unmetered Provider Fallback
1381+ if !token_usage_observed && iteration >= legacy_max_iterations {
13391382 warn ! (
1340- "⚠️ Evolution exceeded maximum iterations ({}) - aborting" ,
1341- max_iterations
1383+ "⚠️ Provider has not reported token usage after {} calls - aborting" ,
1384+ legacy_max_iterations
13421385 ) ;
13431386 evolution. state = EvolutionState :: Failed ;
1387+ let stop_reason = format ! (
1388+ "Provider did not report token usage; stopped after {} unmetered AI calls" ,
1389+ legacy_max_iterations
1390+ ) ;
13441391 emit_evolve_event (
13451392 app,
1346- EvolveEvent :: error (
1347- start_time,
1348- Some ( iteration) ,
1349- & format ! ( "Maximum iterations exceeded ({})" , max_iterations) ,
1350- & format ! ( "Maximum iterations exceeded ({})" , max_iterations) ,
1351- ) ,
1393+ EvolveEvent :: error ( start_time, Some ( iteration) , & stop_reason, & stop_reason) ,
13521394 ) ;
13531395 // Track failure
13541396 if let Err ( e) = statistics:: record_evolution_failure ( app, iteration) {
13551397 warn ! ( "Failed to record evolution failure stats: {}" , e) ;
13561398 }
13571399 return Err ( EvolutionRunError :: from_state (
1358- format ! ( "Evolution exceeded maximum iterations ({})" , max_iterations ) ,
1400+ stop_reason ,
13591401 & evolution,
13601402 iteration,
13611403 build_attempts,
0 commit comments