This commit is contained in:
Dhanji Prasanna
2025-10-10 13:52:04 +11:00
parent 16216532d0
commit 2d959b3d63

View File

@@ -4,20 +4,20 @@ use g3_config::Config;
use g3_core::{project::Project, ui_writer::UiWriter, Agent}; use g3_core::{project::Project, ui_writer::UiWriter, Agent};
use rustyline::error::ReadlineError; use rustyline::error::ReadlineError;
use rustyline::DefaultEditor; use rustyline::DefaultEditor;
use std::path::PathBuf;
use std::path::Path; use std::path::Path;
use std::path::PathBuf;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use tracing::{error, info}; use tracing::{error, info};
use g3_core::error_handling::{classify_error, ErrorType, RecoverableError}; use g3_core::error_handling::{classify_error, ErrorType, RecoverableError};
mod retro_tui; mod retro_tui;
mod theme;
mod tui; mod tui;
mod ui_writer_impl; mod ui_writer_impl;
mod theme;
use retro_tui::RetroTui; use retro_tui::RetroTui;
use theme::ColorTheme;
use tui::SimpleOutput; use tui::SimpleOutput;
use ui_writer_impl::{ConsoleUiWriter, RetroTuiWriter}; use ui_writer_impl::{ConsoleUiWriter, RetroTuiWriter};
use theme::ColorTheme;
#[derive(Parser)] #[derive(Parser)]
#[command(name = "g3")] #[command(name = "g3")]
@@ -183,7 +183,14 @@ pub async fn run() -> Result<()> {
if cli.retro { if cli.retro {
// Use retro terminal UI // Use retro terminal UI
run_interactive_retro(config, cli.show_prompt, cli.show_code, cli.theme, readme_content).await?; run_interactive_retro(
config,
cli.show_prompt,
cli.show_code,
cli.theme,
readme_content,
)
.await?;
} else { } else {
// Use standard terminal UI // Use standard terminal UI
let output = SimpleOutput::new(); let output = SimpleOutput::new();
@@ -199,11 +206,11 @@ pub async fn run() -> Result<()> {
fn read_project_readme(workspace_dir: &Path) -> Option<String> { fn read_project_readme(workspace_dir: &Path) -> Option<String> {
// Check if we're in a project directory (contains .g3 or .git) // Check if we're in a project directory (contains .g3 or .git)
let is_project_dir = workspace_dir.join(".g3").exists() || workspace_dir.join(".git").exists(); let is_project_dir = workspace_dir.join(".g3").exists() || workspace_dir.join(".git").exists();
if !is_project_dir { if !is_project_dir {
return None; return None;
} }
// Look for README files in common formats // Look for README files in common formats
let readme_names = [ let readme_names = [
"README.md", "README.md",
@@ -214,14 +221,17 @@ fn read_project_readme(workspace_dir: &Path) -> Option<String> {
"README.txt", "README.txt",
"README.rst", "README.rst",
]; ];
for readme_name in &readme_names { for readme_name in &readme_names {
let readme_path = workspace_dir.join(readme_name); let readme_path = workspace_dir.join(readme_name);
if readme_path.exists() { if readme_path.exists() {
match std::fs::read_to_string(&readme_path) { match std::fs::read_to_string(&readme_path) {
Ok(content) => { Ok(content) => {
// Return the content with a note about which file was read // Return the content with a note about which file was read
return Some(format!("📚 Project README (from {}):\n\n{}", readme_name, content)); return Some(format!(
"📚 Project README (from {}):\n\n{}",
readme_name, content
));
} }
Err(e) => { Err(e) => {
// Log the error but continue looking for other README files // Log the error but continue looking for other README files
@@ -230,11 +240,17 @@ fn read_project_readme(workspace_dir: &Path) -> Option<String> {
} }
} }
} }
None None
} }
async fn run_interactive_retro(config: Config, show_prompt: bool, show_code: bool, theme_name: Option<String>, readme_content: Option<String>) -> Result<()> { async fn run_interactive_retro(
config: Config,
show_prompt: bool,
show_code: bool,
theme_name: Option<String>,
readme_content: Option<String>,
) -> Result<()> {
use crossterm::event::{self, Event, KeyCode, KeyModifiers}; use crossterm::event::{self, Event, KeyCode, KeyModifiers};
use std::time::Duration; use std::time::Duration;
@@ -259,7 +275,7 @@ async fn run_interactive_retro(config: Config, show_prompt: bool, show_code: boo
// Display initial system messages // Display initial system messages
tui.output("SYSTEM: AGENT ONLINE\n\n"); tui.output("SYSTEM: AGENT ONLINE\n\n");
// Display message if README was loaded // Display message if README was loaded
if readme_content.is_some() { if readme_content.is_some() {
tui.output("SYSTEM: PROJECT README LOADED INTO CONTEXT\n\n"); tui.output("SYSTEM: PROJECT README LOADED INTO CONTEXT\n\n");
@@ -383,13 +399,13 @@ async fn run_interactive_retro(config: Config, show_prompt: bool, show_code: boo
// Execute the task // Execute the task
tui.output(&format!("> {}", input)); tui.output(&format!("> {}", input));
tui.status("PROCESSING"); tui.status("PROCESSING");
const MAX_TIMEOUT_RETRIES: u32 = 3; const MAX_TIMEOUT_RETRIES: u32 = 3;
let mut attempt = 0; let mut attempt = 0;
loop { loop {
attempt += 1; attempt += 1;
match agent match agent
.execute_task_with_timing( .execute_task_with_timing(
&input, &input,
@@ -403,7 +419,10 @@ async fn run_interactive_retro(config: Config, show_prompt: bool, show_code: boo
{ {
Ok(result) => { Ok(result) => {
if attempt > 1 { if attempt > 1 {
tui.output(&format!("SYSTEM: REQUEST SUCCEEDED AFTER {} ATTEMPTS", attempt)); tui.output(&format!(
"SYSTEM: REQUEST SUCCEEDED AFTER {} ATTEMPTS",
attempt
));
} }
tui.output(&result.response); tui.output(&result.response);
tui.status("READY"); tui.status("READY");
@@ -412,21 +431,25 @@ async fn run_interactive_retro(config: Config, show_prompt: bool, show_code: boo
Err(e) => { Err(e) => {
// Check if this is a timeout error that we should retry // Check if this is a timeout error that we should retry
let error_type = classify_error(&e); let error_type = classify_error(&e);
if matches!(error_type, ErrorType::Recoverable(RecoverableError::Timeout)) && attempt < MAX_TIMEOUT_RETRIES { if matches!(
error_type,
ErrorType::Recoverable(RecoverableError::Timeout)
) && attempt < MAX_TIMEOUT_RETRIES
{
// Calculate retry delay with exponential backoff // Calculate retry delay with exponential backoff
let delay_ms = 1000 * (2_u64.pow(attempt - 1)); let delay_ms = 1000 * (2_u64.pow(attempt - 1));
let delay = std::time::Duration::from_millis(delay_ms); let delay = std::time::Duration::from_millis(delay_ms);
tui.output(&format!("SYSTEM: TIMEOUT ERROR (ATTEMPT {}/{}). RETRYING IN {:?}...", tui.output(&format!("SYSTEM: TIMEOUT ERROR (ATTEMPT {}/{}). RETRYING IN {:?}...",
attempt, MAX_TIMEOUT_RETRIES, delay)); attempt, MAX_TIMEOUT_RETRIES, delay));
tui.status("RETRYING"); tui.status("RETRYING");
// Wait before retrying // Wait before retrying
tokio::time::sleep(delay).await; tokio::time::sleep(delay).await;
continue; continue;
} }
// For non-timeout errors or after max retries // For non-timeout errors or after max retries
tui.error(&format!("Task execution failed: {}", e)); tui.error(&format!("Task execution failed: {}", e));
tui.status("ERROR"); tui.status("ERROR");
@@ -482,10 +505,8 @@ async fn run_interactive<W: UiWriter>(
let output = SimpleOutput::new(); let output = SimpleOutput::new();
output.print(""); output.print("");
output.print("🤖 G3 AI Coding Agent - Interactive Mode"); output.print("🪿 G3 AI Coding Agent - Interactive Mode");
output.print( output.print("I solve problems by writing and executing code. what shall we build today?");
"I solve problems by writing and executing code. Tell me what you need to accomplish!",
);
output.print(""); output.print("");
// Display message if README was loaded // Display message if README was loaded
@@ -497,7 +518,7 @@ async fn run_interactive<W: UiWriter>(
// Display provider and model information // Display provider and model information
match agent.get_provider_info() { match agent.get_provider_info() {
Ok((provider, model)) => { Ok((provider, model)) => {
output.print(&format!("🔧 Provider: {} | Model: {}", provider, model)); output.print(&format!("🔧 {} | {}", provider, model));
} }
Err(e) => { Err(e) => {
error!("Failed to get provider info: {}", e); error!("Failed to get provider info: {}", e);
@@ -505,9 +526,7 @@ async fn run_interactive<W: UiWriter>(
} }
output.print(""); output.print("");
output.print("Type 'exit' or 'quit' to exit, use Up/Down arrows for command history"); output.print("CTRL-D to quit; ↑/↓ for history");
output.print("For multiline input: use \\ at the end of a line to continue");
output.print("Submit multiline with Enter (without backslash)");
output.print(""); output.print("");
// Initialize rustyline editor with history // Initialize rustyline editor with history
@@ -640,7 +659,7 @@ async fn execute_task<W: UiWriter>(
loop { loop {
attempt += 1; attempt += 1;
// Execute task with cancellation support // Execute task with cancellation support
let execution_result = tokio::select! { let execution_result = tokio::select! {
result = agent.execute_task_with_timing_cancellable( result = agent.execute_task_with_timing_cancellable(
@@ -668,25 +687,29 @@ async fn execute_task<W: UiWriter>(
output.print("⚠️ Operation cancelled by user"); output.print("⚠️ Operation cancelled by user");
return; return;
} }
// Check if this is a timeout error that we should retry // Check if this is a timeout error that we should retry
let error_type = classify_error(&e); let error_type = classify_error(&e);
if matches!(error_type, ErrorType::Recoverable(RecoverableError::Timeout)) && attempt < MAX_TIMEOUT_RETRIES { if matches!(
error_type,
ErrorType::Recoverable(RecoverableError::Timeout)
) && attempt < MAX_TIMEOUT_RETRIES
{
// Calculate retry delay with exponential backoff // Calculate retry delay with exponential backoff
let delay_ms = 1000 * (2_u64.pow(attempt - 1)); let delay_ms = 1000 * (2_u64.pow(attempt - 1));
let delay = std::time::Duration::from_millis(delay_ms); let delay = std::time::Duration::from_millis(delay_ms);
output.print(&format!( output.print(&format!(
"⏱️ Timeout error detected (attempt {}/{}). Retrying in {:?}...", "⏱️ Timeout error detected (attempt {}/{}). Retrying in {:?}...",
attempt, MAX_TIMEOUT_RETRIES, delay attempt, MAX_TIMEOUT_RETRIES, delay
)); ));
// Wait before retrying // Wait before retrying
tokio::time::sleep(delay).await; tokio::time::sleep(delay).await;
continue; continue;
} }
// For non-timeout errors or after max retries, handle as before // For non-timeout errors or after max retries, handle as before
handle_execution_error(&e, input, output, attempt); handle_execution_error(&e, input, output, attempt);
return; return;
@@ -702,7 +725,7 @@ fn handle_execution_error(e: &anyhow::Error, input: &str, output: &SimpleOutput,
if attempt > 1 { if attempt > 1 {
error!("Failed after {} attempts", attempt); error!("Failed after {} attempts", attempt);
} }
// Log error chain // Log error chain
let mut source = e.source(); let mut source = e.source();
let mut depth = 1; let mut depth = 1;
@@ -721,8 +744,8 @@ fn handle_execution_error(e: &anyhow::Error, input: &str, output: &SimpleOutput,
// If it's a stream error, provide helpful guidance // If it's a stream error, provide helpful guidance
if e.to_string().contains("No response received") || e.to_string().contains("timed out") { 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("💡 This may be a temporary issue. Please try again or check the logs for more details.");
output.print(" Log files are saved in the 'logs/' directory."); output.print(" Log files are saved in the 'logs/' directory.");
} }
} }
@@ -905,17 +928,17 @@ Remember: Be thorough in your review but concise in your feedback. APPROVE if th
.await?; .await?;
output.print("🎓 Coach review completed"); output.print("🎓 Coach review completed");
// Extract the coach feedback using the semantic extraction from TaskResult // Extract the coach feedback using the semantic extraction from TaskResult
let coach_feedback_text = coach_result.extract_last_block(); let coach_feedback_text = coach_result.extract_last_block();
// Log the size of the feedback for debugging // Log the size of the feedback for debugging
info!( info!(
"Coach feedback extracted: {} characters (from {} total)", "Coach feedback extracted: {} characters (from {} total)",
coach_feedback_text.len(), coach_feedback_text.len(),
coach_result.response.len() coach_result.response.len()
); );
// Check if we got empty feedback (this can happen if the coach doesn't call final_output) // Check if we got empty feedback (this can happen if the coach doesn't call final_output)
if coach_feedback_text.is_empty() { if coach_feedback_text.is_empty() {
output.print("⚠️ Coach did not provide feedback. This may be a model issue."); output.print("⚠️ Coach did not provide feedback. This may be a model issue.");
@@ -923,7 +946,7 @@ Remember: Be thorough in your review but concise in your feedback. APPROVE if th
turn += 1; turn += 1;
continue; continue;
} }
output.print(&format!("Coach feedback:\n{}", coach_feedback_text)); output.print(&format!("Coach feedback:\n{}", coach_feedback_text));
// Check if coach approved the implementation // Check if coach approved the implementation