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::Agent;
use tokio_util::sync::CancellationToken;
use tracing::error;
use tracing::{debug, error};
use crate::simple_output::SimpleOutput;
use crate::g3_status::G3Status;
/// Maximum number of retry attempts for recoverable errors
const MAX_RETRIES: u32 = 3;
@@ -14,13 +15,13 @@ const MAX_RETRIES: u32 = 3;
/// Get a human-readable name for a recoverable error type.
fn recoverable_error_name(err: &RecoverableError) -> &'static str {
match err {
RecoverableError::RateLimit => "Rate limit",
RecoverableError::ServerError => "Server error",
RecoverableError::NetworkError => "Network error",
RecoverableError::Timeout => "Timeout",
RecoverableError::ModelBusy => "Model busy",
RecoverableError::TokenLimit => "Token limit",
RecoverableError::ContextLengthExceeded => "Context length",
RecoverableError::RateLimit => "rate limited",
RecoverableError::ServerError => "server error",
RecoverableError::NetworkError => "network error",
RecoverableError::Timeout => "timeout",
RecoverableError::ModelBusy => "model overloaded",
RecoverableError::TokenLimit => "token limit",
RecoverableError::ContextLengthExceeded => "context length exceeded",
}
}
@@ -78,18 +79,20 @@ pub async fn execute_task_with_retry<W: UiWriter>(
if attempt < MAX_RETRIES {
// Use shared retry delay calculation (non-autonomous mode)
let delay = calculate_retry_delay(attempt, false);
let delay_ms = delay.as_millis();
let delay_secs = delay.as_secs_f64();
output.print(&format!(
"⚠️ {} detected (attempt {}/{}). Retrying in {:.1}s...",
// Print error status
G3Status::complete(
recoverable_error_name(&recoverable_error),
attempt,
MAX_RETRIES,
delay_ms as f64 / 1000.0
));
crate::g3_status::Status::Error(format!("attempt {}/{}", attempt, MAX_RETRIES)),
);
// Print retry message (no newline, will show [done] after sleep)
G3Status::progress(&format!("retrying in {:.1}s", delay_secs));
// Wait before retrying
tokio::time::sleep(delay).await;
G3Status::done();
continue;
}
}
@@ -104,32 +107,41 @@ pub async fn execute_task_with_retry<W: UiWriter>(
/// Handle execution errors with detailed logging and user-friendly output.
pub fn handle_execution_error(e: &anyhow::Error, input: &str, output: &SimpleOutput, attempt: u32) {
// Enhanced error logging with detailed information
error!("=== TASK EXECUTION ERROR ===");
error!("Error: {}", e);
if attempt > 1 {
error!("Failed after {} attempts", attempt);
// Check if this is a recoverable error type (for logging level decision)
let error_type = classify_error(e);
let is_recoverable = matches!(error_type, ErrorType::Recoverable(_));
// 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
let mut source = e.source();
let mut depth = 1;
while let Some(err) = source {
error!(" Caused by [{}]: {}", depth, err);
source = err.source();
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.");
// Display user-friendly error message using G3Status
if let ErrorType::Recoverable(ref recoverable_error) = error_type {
let error_name = recoverable_error_name(recoverable_error);
G3Status::complete(error_name, crate::g3_status::Status::Failed);
} else {
G3Status::complete(&format!("error: {}", e), crate::g3_status::Status::Failed);
}
}

View File

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