g3 console initial cut + error doesnt kill auto

This commit is contained in:
Dhanji Prasanna
2025-11-04 11:39:26 +11:00
parent 6913c5f72e
commit aaf918828f
53 changed files with 6796 additions and 23 deletions

View File

@@ -11,12 +11,6 @@ use serde::{Deserialize, Serialize};
use std::time::Duration;
use tracing::{error, info, warn};
/// Maximum number of retry attempts for recoverable errors (default mode)
const DEFAULT_MAX_RETRY_ATTEMPTS: u32 = 3;
/// Maximum number of retry attempts for autonomous mode
const AUTONOMOUS_MAX_RETRY_ATTEMPTS: u32 = 6;
/// Base delay for exponential backoff (in milliseconds)
const BASE_RETRY_DELAY_MS: u64 = 1000;
@@ -188,6 +182,8 @@ pub enum RecoverableError {
Timeout,
/// Token limit exceeded (might be recoverable with summarization)
TokenLimit,
/// Context length exceeded (prompt too long) - should end current turn in autonomous mode
ContextLengthExceeded,
}
/// Classify an error as recoverable or non-recoverable
@@ -224,6 +220,13 @@ pub fn classify_error(error: &anyhow::Error) -> ErrorType {
return ErrorType::Recoverable(RecoverableError::Timeout);
}
// Check for context length exceeded errors (HTTP 400 with specific messages)
if (error_str.contains("400") || error_str.contains("bad request")) &&
(error_str.contains("context length") || error_str.contains("prompt is too long") ||
error_str.contains("maximum context length") || error_str.contains("context_length_exceeded")) {
return ErrorType::Recoverable(RecoverableError::ContextLengthExceeded);
}
if error_str.contains("token") && (error_str.contains("limit") || error_str.contains("exceeded")) {
return ErrorType::Recoverable(RecoverableError::TokenLimit);
}
@@ -284,6 +287,7 @@ pub async fn retry_with_backoff<F, Fut, T>(
mut operation: F,
context: &ErrorContext,
is_autonomous: bool,
max_attempts: u32,
) -> Result<T>
where
F: FnMut() -> Fut,
@@ -307,8 +311,6 @@ where
}
Err(error) => {
let error_type = classify_error(&error);
let max_attempts = if is_autonomous { AUTONOMOUS_MAX_RETRY_ATTEMPTS } else { DEFAULT_MAX_RETRY_ATTEMPTS };
match error_type {
ErrorType::Recoverable(recoverable_type) => {
if attempt >= max_attempts {
@@ -421,6 +423,13 @@ mod tests {
let error = anyhow!("Token limit exceeded");
assert_eq!(classify_error(&error), ErrorType::Recoverable(RecoverableError::TokenLimit));
// Context length exceeded
let error = anyhow!("HTTP 400 Bad Request: context length exceeded");
assert_eq!(classify_error(&error), ErrorType::Recoverable(RecoverableError::ContextLengthExceeded));
let error = anyhow!("Error 400: prompt is too long");
assert_eq!(classify_error(&error), ErrorType::Recoverable(RecoverableError::ContextLengthExceeded));
// Non-recoverable
let error = anyhow!("Invalid API key");
assert_eq!(classify_error(&error), ErrorType::NonRecoverable);

View File

@@ -37,6 +37,7 @@ mod tests {
},
&context,
false, // not autonomous mode
3, // max_attempts
)
.await;
@@ -71,6 +72,7 @@ mod tests {
},
&context,
false, // not autonomous mode
3, // max_attempts
)
.await;
@@ -104,6 +106,7 @@ mod tests {
},
&context,
false, // not autonomous mode
3, // max_attempts
)
.await;

View File

@@ -1620,6 +1620,72 @@ If you can complete it with 1-2 tool calls, skip TODO.
&self.context_window
}
/// Log an error message to the session JSON file as the last message
/// This is used in autonomous mode to record context length exceeded errors
pub fn log_error_to_session(&self, error: &anyhow::Error, role: &str, forensic_context: Option<String>) {
// Skip if quiet mode is enabled
if self.quiet {
return;
}
// Only log if we have a session ID
let session_id = match &self.session_id {
Some(id) => id,
None => {
error!("Cannot log error to session: no session ID");
return;
}
};
let timestamp = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let filename = format!("logs/g3_session_{}.json", session_id);
// Read existing session log
let mut session_data: serde_json::Value = if std::path::Path::new(&filename).exists() {
match std::fs::read_to_string(&filename) {
Ok(content) => serde_json::from_str(&content).unwrap_or_else(|_| serde_json::json!({})),
Err(_) => serde_json::json!({}),
}
} else {
serde_json::json!({})
};
// Build error message with forensic context
let error_message = if let Some(context) = forensic_context {
format!(
"ERROR: {}\n\nForensic Context:\n{}",
error,
context
)
} else {
format!("ERROR: {}", error)
};
// Create error message entry
let error_entry = serde_json::json!({
"role": role,
"content": error_message,
"timestamp": timestamp,
"error_type": "context_length_exceeded"
});
// Append to conversation history
if let Some(history) = session_data.get_mut("context_window").and_then(|cw| cw.get_mut("conversation_history")) {
if let Some(history_array) = history.as_array_mut() {
history_array.push(error_entry);
}
}
// Write back to file
if let Ok(json_content) = serde_json::to_string_pretty(&session_data) {
let _ = std::fs::write(&filename, json_content);
}
}
/// Manually trigger context summarization regardless of context window size
/// Returns Ok(true) if summarization was successful, Ok(false) if it failed
pub async fn force_summarize(&mut self) -> Result<bool> {
@@ -2485,7 +2551,11 @@ If you can complete it with 1-2 tool calls, skip TODO.
use crate::error_handling::{calculate_retry_delay, classify_error, ErrorType};
let mut attempt = 0;
let max_attempts = if self.is_autonomous { 6 } else { 3 };
let max_attempts = if self.is_autonomous {
self.config.agent.autonomous_max_retry_attempts
} else {
self.config.agent.max_retry_attempts
};
loop {
attempt += 1;