Clean up error and retry messages for recoverable errors

Before:
   Error: Anthropic API error: AnthropicError { error_type: "overloaded_error", ... }
  ⚠️  Model busy detected (attempt 2/3). Retrying in 2.2s...
  [ERROR logs dumped to terminal]

After:
  g3: model overloaded [error: attempt 1/3]
  g3: retrying in 2.2s ... [done]

Changes:
- Use G3Status formatting for clean, consistent output
- Downgrade ERROR logs to debug for recoverable errors
- Apply same treatment to all recoverable error types:
  rate limited, server error, network error, timeout,
  model overloaded, token limit, context length exceeded
- Update both g3-cli (task_execution.rs) and g3-core (retry.rs)
This commit is contained in:
Dhanji R. Prasanna
2026-01-20 22:40:09 +05:30
parent 53e1ea9766
commit 60578e310c
2 changed files with 74 additions and 66 deletions

View File

@@ -4,9 +4,10 @@ use g3_core::error_handling::{calculate_retry_delay, classify_error, ErrorType,
use g3_core::ui_writer::UiWriter; use g3_core::ui_writer::UiWriter;
use g3_core::Agent; use g3_core::Agent;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use tracing::error; use tracing::{debug, error};
use crate::simple_output::SimpleOutput; use crate::simple_output::SimpleOutput;
use crate::g3_status::G3Status;
/// Maximum number of retry attempts for recoverable errors /// Maximum number of retry attempts for recoverable errors
const MAX_RETRIES: u32 = 3; const MAX_RETRIES: u32 = 3;
@@ -14,13 +15,13 @@ const MAX_RETRIES: u32 = 3;
/// Get a human-readable name for a recoverable error type. /// Get a human-readable name for a recoverable error type.
fn recoverable_error_name(err: &RecoverableError) -> &'static str { fn recoverable_error_name(err: &RecoverableError) -> &'static str {
match err { match err {
RecoverableError::RateLimit => "Rate limit", RecoverableError::RateLimit => "rate limited",
RecoverableError::ServerError => "Server error", RecoverableError::ServerError => "server error",
RecoverableError::NetworkError => "Network error", RecoverableError::NetworkError => "network error",
RecoverableError::Timeout => "Timeout", RecoverableError::Timeout => "timeout",
RecoverableError::ModelBusy => "Model busy", RecoverableError::ModelBusy => "model overloaded",
RecoverableError::TokenLimit => "Token limit", RecoverableError::TokenLimit => "token limit",
RecoverableError::ContextLengthExceeded => "Context length", RecoverableError::ContextLengthExceeded => "context length exceeded",
} }
} }
@@ -78,18 +79,20 @@ pub async fn execute_task_with_retry<W: UiWriter>(
if attempt < MAX_RETRIES { if attempt < MAX_RETRIES {
// Use shared retry delay calculation (non-autonomous mode) // Use shared retry delay calculation (non-autonomous mode)
let delay = calculate_retry_delay(attempt, false); let delay = calculate_retry_delay(attempt, false);
let delay_ms = delay.as_millis(); let delay_secs = delay.as_secs_f64();
output.print(&format!( // Print error status
"⚠️ {} detected (attempt {}/{}). Retrying in {:.1}s...", G3Status::complete(
recoverable_error_name(&recoverable_error), recoverable_error_name(&recoverable_error),
attempt, crate::g3_status::Status::Error(format!("attempt {}/{}", attempt, MAX_RETRIES)),
MAX_RETRIES, );
delay_ms as f64 / 1000.0
)); // Print retry message (no newline, will show [done] after sleep)
G3Status::progress(&format!("retrying in {:.1}s", delay_secs));
// Wait before retrying // Wait before retrying
tokio::time::sleep(delay).await; tokio::time::sleep(delay).await;
G3Status::done();
continue; continue;
} }
} }
@@ -104,32 +107,41 @@ pub async fn execute_task_with_retry<W: UiWriter>(
/// Handle execution errors with detailed logging and user-friendly output. /// Handle execution errors with detailed logging and user-friendly output.
pub fn handle_execution_error(e: &anyhow::Error, input: &str, output: &SimpleOutput, attempt: u32) { pub fn handle_execution_error(e: &anyhow::Error, input: &str, output: &SimpleOutput, attempt: u32) {
// Enhanced error logging with detailed information // Check if this is a recoverable error type (for logging level decision)
error!("=== TASK EXECUTION ERROR ==="); let error_type = classify_error(e);
error!("Error: {}", e); let is_recoverable = matches!(error_type, ErrorType::Recoverable(_));
if attempt > 1 {
error!("Failed after {} attempts", attempt); // Use debug level for recoverable errors (they're expected), error level for others
if is_recoverable {
debug!("Task execution failed (recoverable): {}", e);
if attempt > 1 {
debug!("Failed after {} attempts", attempt);
}
} else {
error!("=== TASK EXECUTION ERROR ===");
error!("Error: {}", e);
if attempt > 1 {
error!("Failed after {} attempts", attempt);
}
// Log error chain only for non-recoverable errors
let mut source = e.source();
let mut depth = 1;
while let Some(err) = source {
error!(" Caused by [{}]: {}", depth, err);
source = err.source();
depth += 1;
}
error!("Task input: {}", input);
error!("Error type: {}", std::any::type_name_of_val(&e));
} }
// Log error chain // Display user-friendly error message using G3Status
let mut source = e.source(); if let ErrorType::Recoverable(ref recoverable_error) = error_type {
let mut depth = 1; let error_name = recoverable_error_name(recoverable_error);
while let Some(err) = source { G3Status::complete(error_name, crate::g3_status::Status::Failed);
error!(" Caused by [{}]: {}", depth, err); } else {
source = err.source(); G3Status::complete(&format!("error: {}", e), crate::g3_status::Status::Failed);
depth += 1;
}
// Log additional context
error!("Task input: {}", input);
error!("Error type: {}", std::any::type_name_of_val(&e));
// Display user-friendly error message
output.print(&format!("❌ Error: {}", e));
// If it's a stream error, provide helpful guidance
if e.to_string().contains("No response received") || e.to_string().contains("timed out") {
output.print("💡 This may be a temporary issue. Please try again or check the logs for more details.");
output.print(" Log files are saved in the '.g3/sessions/' directory.");
} }
} }

View File

@@ -194,8 +194,8 @@ where
if retry_count >= config.max_retries { if retry_count >= config.max_retries {
let msg = format!( let msg = format!(
"🔄 Max retries ({}) reached for {}", "g3: {} max retries reached [failed]",
config.max_retries, config.role_name config.role_name
); );
print_fn(&msg); print_fn(&msg);
return RetryResult::MaxRetriesReached(e.to_string()); return RetryResult::MaxRetriesReached(e.to_string());
@@ -203,20 +203,17 @@ where
// Calculate backoff delay // Calculate backoff delay
let delay = calculate_retry_delay(retry_count, config.is_autonomous); let delay = calculate_retry_delay(retry_count, config.is_autonomous);
let delay_secs = delay.as_secs_f64();
let msg = format!( // Clean error message
"⚠️ {} error (attempt {}/{}): {:?} - {}", let msg = format!("g3: {:?} [error: attempt {}/{}]", recoverable_type, retry_count, config.max_retries);
config.role_name, retry_count, config.max_retries, recoverable_type, e
);
print_fn(&msg); print_fn(&msg);
let retry_msg = format!( // Retry message - note: can't show [done] here since we don't control when sleep finishes
"🔄 Retrying {} in {:?}...", let retry_msg = format!("g3: retrying in {:.1}s ...", delay_secs);
config.role_name, delay
);
print_fn(&retry_msg); print_fn(&retry_msg);
warn!( debug!(
"Recoverable error ({:?}) in {} (attempt {}/{}). Retrying in {:?}...", "Recoverable error ({:?}) in {} (attempt {}/{}). Retrying in {:?}...",
recoverable_type, config.role_name, retry_count, config.max_retries, delay recoverable_type, config.role_name, retry_count, config.max_retries, delay
); );
@@ -225,8 +222,8 @@ where
} }
ErrorType::NonRecoverable => { ErrorType::NonRecoverable => {
let msg = format!( let msg = format!(
"❌ Non-recoverable error in {}: {}", "g3: {} error [failed]",
config.role_name, e config.role_name
); );
print_fn(&msg); print_fn(&msg);
return RetryResult::MaxRetriesReached(e.to_string()); return RetryResult::MaxRetriesReached(e.to_string());
@@ -275,30 +272,29 @@ where
if retry_count >= max_retries { if retry_count >= max_retries {
let msg = format!( let msg = format!(
"❌ Operation '{}' failed after {} retries: {}", "g3: {} max retries reached [failed]",
operation_name, retry_count, e operation_name
); );
print_fn(&msg); print_fn(&msg);
return Err(e); return Err(e);
} }
let delay = calculate_retry_delay(retry_count, is_autonomous); let delay = calculate_retry_delay(retry_count, is_autonomous);
let msg = format!( let delay_secs = delay.as_secs_f64();
"⚠️ {} error in '{}' (attempt {}/{}), retrying in {:?}...",
format!("{:?}", recoverable_type), // Clean error message
operation_name, let msg = format!("g3: {:?} [error: attempt {}/{}]", recoverable_type, retry_count, max_retries);
retry_count,
max_retries,
delay
);
print_fn(&msg); print_fn(&msg);
let retry_msg = format!("g3: retrying in {:.1}s ...", delay_secs);
print_fn(&retry_msg);
tokio::time::sleep(delay).await; tokio::time::sleep(delay).await;
} }
ErrorType::NonRecoverable => { ErrorType::NonRecoverable => {
let msg = format!( let msg = format!(
"❌ Non-recoverable error in '{}': {}", "g3: {} error [failed]",
operation_name, e operation_name
); );
print_fn(&msg); print_fn(&msg);
return Err(e); return Err(e);