Skip to content
Merged
2 changes: 1 addition & 1 deletion apps/native/src-tauri/configurable-derive/src/attrs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ mod tests {
let input: DeriveInput = parse_quote! {
#[config(scope = "repo", display_name = "Evolution")]
struct EvolutionLimits {
max_token_budget: usize,
max_iterations: usize,
}
};

Expand Down
38 changes: 31 additions & 7 deletions apps/native/src-tauri/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use tauri::AppHandle;
pub struct EvolveConfig {
pub prompt: String,
pub config: Option<PathBuf>,
pub max_iterations: Option<usize>,
pub max_output_tokens: Option<usize>,
pub max_token_budget: Option<u32>,
pub evolve_provider: Option<String>,
Expand Down Expand Up @@ -55,6 +56,10 @@ pub enum Commands {
#[arg(short, long)]
config: Option<PathBuf>,

/// Legacy fallback for providers that do not report token usage
#[arg(short, long, hide = true)]
max_iterations: Option<usize>,

/// Maximum output tokens requested per evolution model call
#[arg(long)]
max_output_tokens: Option<usize>,
Expand Down Expand Up @@ -106,6 +111,7 @@ pub async fn handle_evolve_command(app: &AppHandle, cfg: EvolveConfig) -> Result
let EvolveConfig {
prompt,
config,
max_iterations,
max_output_tokens,
max_token_budget,
evolve_provider,
Expand Down Expand Up @@ -192,6 +198,12 @@ pub async fn handle_evolve_command(app: &AppHandle, cfg: EvolveConfig) -> Result
None => crate::storage::store::get_summary_model(app).ok().flatten(),
};

// Effective legacy iteration fallback: prefer CLI value, otherwise read from store (has default)
let effective_max_iterations: usize = match max_iterations {
Some(v) => v,
None => crate::storage::store::get_max_iterations(app)
.unwrap_or(crate::storage::store::DEFAULT_MAX_ITERATIONS),
};
let effective_max_output_tokens: usize = match max_output_tokens {
Some(v) => v,
None => crate::storage::store::get_max_output_tokens(app)
Expand All @@ -205,6 +217,12 @@ pub async fn handle_evolve_command(app: &AppHandle, cfg: EvolveConfig) -> Result
.unwrap_or(crate::storage::store::DEFAULT_MAX_TOKEN_BUDGET),
};

// Legacy max iterations
if let Some(iterations) = max_iterations {
crate::storage::store::set_max_iterations(app, iterations)
.map_err(|e| format!("Failed to set max iterations: {}", e))?;
}

if let Some(output_tokens) = max_output_tokens {
crate::storage::store::set_max_output_tokens(app, output_tokens)
.map_err(|e| format!("Failed to set max output tokens: {}", e))?;
Expand Down Expand Up @@ -235,13 +253,18 @@ pub async fn handle_evolve_command(app: &AppHandle, cfg: EvolveConfig) -> Result

let (ok, output_value, failure_message) = match outcome {
Ok(output) => {
let is_conversational =
output.telemetry.state == crate::shared_types::EvolutionState::Conversational;

if is_conversational {
println!("(conversational response — no changes made)");
} else {
println!("Evolution completed successfully");
match output.telemetry.state {
crate::shared_types::EvolutionState::Conversational => {
println!("(conversational response — no changes made)");
}
crate::shared_types::EvolutionState::LimitReached => {
println!(
"Evolution stopped after reaching a safety limit (iterations, build attempts, token budget, or stale progress). Review any partial changes and re-run with adjusted limits to continue."
);
}
_ => {
println!("Evolution completed successfully");
}
}

let output_value = match serde_json::to_value(&output) {
Expand Down Expand Up @@ -272,6 +295,7 @@ pub async fn handle_evolve_command(app: &AppHandle, cfg: EvolveConfig) -> Result
"ok": ok,
"state": state_str,
"prompt": prompt,
"maxIterations": effective_max_iterations,
"maxOutputTokens": effective_max_output_tokens,
"maxTokenBudget": effective_max_token_budget,
"evolveProvider": effective_evolve_provider,
Expand Down
2 changes: 2 additions & 0 deletions apps/native/src-tauri/src/commands/settings_io.rs
Original file line number Diff line number Diff line change
Expand Up @@ -240,6 +240,7 @@ mod tests {
&mut output,
&mut skipped,
serde_json::to_value(EvolutionLimits {
max_iterations: 12,
max_token_budget: 80_000,
max_build_attempts: 4,
max_output_tokens: 16_384,
Expand All @@ -250,6 +251,7 @@ mod tests {

assert_eq!(output.get("hostAttr"), Some(&json!("macbook")));
assert_eq!(output.get("developerMode"), Some(&json!(true)));
assert_eq!(output.get("maxIterations"), Some(&json!(12)));
assert_eq!(output.get("maxBuildAttempts"), Some(&json!(4)));
assert!(!output.contains_key("openaiApiKey"));
assert!(!output.contains_key("promptHistory"));
Expand Down
7 changes: 7 additions & 0 deletions apps/native/src-tauri/src/commands/ui_prefs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ pub async fn ui_get_prefs(app: AppHandle) -> Result<shared_types::UiPrefs, Strin
let summary_model =
wrap_result_and_capture_err("ui_get_prefs", store::get_summary_model(&app))?;

let max_iterations =
Some(store::get_max_iterations(&app).unwrap_or(store::DEFAULT_MAX_ITERATIONS));
let max_token_budget =
Some(store::get_max_token_budget(&app).unwrap_or(store::DEFAULT_MAX_TOKEN_BUDGET));
let max_build_attempts = Some(store::get_max_build_attempts(&app).unwrap_or(5));
Expand Down Expand Up @@ -93,6 +95,7 @@ pub async fn ui_get_prefs(app: AppHandle) -> Result<shared_types::UiPrefs, Strin
summary_provider,
summary_model,

max_iterations,
max_token_budget,
max_build_attempts,
max_output_tokens,
Expand Down Expand Up @@ -144,6 +147,10 @@ pub async fn ui_set_prefs(
store::set_summary_model(&app, &summary_model)
.map_err(|e| capture_err("ui_set_prefs", e))?;
}
if let Some(max_iterations) = prefs.max_iterations {
store::set_max_iterations(&app, max_iterations)
.map_err(|e| capture_err("ui_set_prefs", e))?;
}
if let Some(max_token_budget) = prefs.max_token_budget {
store::set_max_token_budget(&app, max_token_budget)
.map_err(|e| capture_err("ui_set_prefs", e))?;
Expand Down
20 changes: 18 additions & 2 deletions apps/native/src-tauri/src/evolve/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,15 @@ pub const EVOLUTION_LIMITS_CHANGED_EVENT: &str = "evolution_limits_changed";
description = "How long the agent will try before giving up."
)]
pub struct EvolutionLimits {
#[config(
default = 25,
key = "maxIterations",
label = "Max iterations (legacy)",
range = 1..=200,
help = "Legacy iteration cap. Used only when the provider doesn't report token usage; the token budget is the primary stopping rule.",
)]
pub max_iterations: usize,

#[config(
default = 50_000,
key = "maxTokenBudget",
Expand Down Expand Up @@ -64,6 +73,7 @@ pub struct EvolutionLimits {
impl Default for EvolutionLimits {
fn default() -> Self {
Self {
max_iterations: 25,
max_token_budget: 50_000,
max_build_attempts: 5,
max_output_tokens: 32_768,
Expand All @@ -87,6 +97,7 @@ mod tests {
fn default_matches_configured_field_defaults() {
let limits = EvolutionLimits::default();

assert_eq!(limits.max_iterations, 25);
assert_eq!(limits.max_token_budget, 50_000);
assert_eq!(limits.max_build_attempts, 5);
assert_eq!(limits.max_output_tokens, 32_768);
Expand All @@ -95,6 +106,7 @@ mod tests {
#[test]
fn unknown_fields_do_not_change_limits() {
let limits: EvolutionLimits = serde_json::from_value(serde_json::json!({
"maxIterations": 11,
"maxTokenBudget": 80_000,
"maxBuildAttempts": 3,
"maxOutputTokens": 16_384,
Expand All @@ -105,6 +117,7 @@ mod tests {
assert_eq!(
limits,
EvolutionLimits {
max_iterations: 11,
max_token_budget: 80_000,
max_build_attempts: 3,
max_output_tokens: 16_384,
Expand All @@ -114,9 +127,12 @@ mod tests {

#[test]
fn missing_fields_use_defaults() {
let limits: EvolutionLimits =
serde_json::from_value(serde_json::json!({})).expect("limits deserialize");
let limits: EvolutionLimits = serde_json::from_value(serde_json::json!({
"maxIterations": 11,
}))
.expect("limits deserialize");

assert_eq!(limits.max_iterations, 11);
assert_eq!(limits.max_token_budget, 50_000);
assert_eq!(limits.max_build_attempts, 5);
assert_eq!(limits.max_output_tokens, 32_768);
Expand Down
Loading
Loading