use anyhow::Result; use clap::Parser; use g3_config::Config; use g3_core::Agent; use rustyline::error::ReadlineError; use rustyline::DefaultEditor; use std::fs::OpenOptions; use std::io::{Write, BufWriter}; use std::path::PathBuf; use std::sync::{Arc, Mutex}; use tokio_util::sync::CancellationToken; use tracing::{error, info}; #[derive(Parser)] #[command(name = "g3")] #[command(about = "A modular, composable AI coding agent")] #[command(version)] pub struct Cli { /// Enable verbose logging #[arg(short, long)] pub verbose: bool, /// Show the system prompt being sent to the LLM #[arg(long)] pub show_prompt: bool, /// Show the generated code before execution #[arg(long)] pub show_code: bool, /// Configuration file path #[arg(short, long)] pub config: Option, /// Task to execute (if provided, runs in single-shot mode instead of interactive) pub task: Option, /// Enable autonomous mode with coach-player feedback loop #[arg(long)] pub autonomous: bool, } pub async fn run() -> Result<()> { let cli = Cli::parse(); // Initialize logging with filtering use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; // Create a filter that suppresses llama_cpp logs unless in verbose mode let filter = if cli.verbose { EnvFilter::from_default_env() .add_directive(format!("{}=debug", env!("CARGO_PKG_NAME")).parse().unwrap()) .add_directive("g3_core=debug".parse().unwrap()) .add_directive("g3_cli=debug".parse().unwrap()) .add_directive("g3_execution=debug".parse().unwrap()) .add_directive("g3_providers=debug".parse().unwrap()) } else { EnvFilter::from_default_env() .add_directive(format!("{}=info", env!("CARGO_PKG_NAME")).parse().unwrap()) .add_directive("g3_core=info".parse().unwrap()) .add_directive("g3_cli=info".parse().unwrap()) .add_directive("g3_execution=info".parse().unwrap()) .add_directive("g3_providers=info".parse().unwrap()) .add_directive("llama_cpp=off".parse().unwrap()) // Suppress all llama_cpp logs .add_directive("llama=off".parse().unwrap()) // Suppress all llama.cpp logs }; tracing_subscriber::registry() .with(tracing_subscriber::fmt::layer()) .with(filter) .init(); info!("Starting G3 AI Coding Agent"); // Load configuration let config = Config::load(cli.config.as_deref())?; // Initialize agent let mut agent = Agent::new(config).await?; // Execute task, autonomous mode, or start interactive mode if cli.autonomous { // Autonomous mode with coach-player feedback loop info!("Starting autonomous mode"); run_autonomous(agent, cli.show_prompt, cli.show_code).await?; } else if let Some(task) = cli.task { // Single-shot mode info!("Executing task: {}", task); let result = agent .execute_task_with_timing(&task, None, false, cli.show_prompt, cli.show_code, true) .await?; println!("{}", result); } else { // Interactive mode (default) info!("Starting interactive mode"); run_interactive(agent, cli.show_prompt, cli.show_code).await?; } Ok(()) } async fn run_interactive(mut agent: Agent, show_prompt: bool, show_code: bool) -> Result<()> { println!("šŸ¤– G3 AI Coding Agent - Interactive Mode"); println!( "I solve problems by writing and executing code. Tell me what you need to accomplish!" ); println!(); // Display provider and model information match agent.get_provider_info() { Ok((provider, model)) => { println!("šŸ”§ Provider: {} | Model: {}", provider, model); } Err(e) => { error!("Failed to get provider info: {}", e); } } println!(); println!("Type 'exit' or 'quit' to exit, use Up/Down arrows for command history"); println!(); // Initialize rustyline editor with history let mut rl = DefaultEditor::new()?; // Try to load history from a file in the user's home directory let history_file = dirs::home_dir().map(|mut path| { path.push(".g3_history"); path }); if let Some(ref history_path) = history_file { let _ = rl.load_history(history_path); } loop { // Display context window progress bar before each prompt display_context_progress(&agent); let readline = rl.readline("g3> "); match readline { Ok(line) => { let input = line.trim(); if input == "exit" || input == "quit" { break; } if input.is_empty() { continue; } // Add to history rl.add_history_entry(input)?; // Show thinking indicator immediately print!("šŸ¤” Thinking..."); std::io::stdout().flush()?; // Create cancellation token for this request let cancellation_token = CancellationToken::new(); let cancel_token_clone = cancellation_token.clone(); // Spawn a task to monitor for ESC key during execution let esc_monitor = tokio::spawn(async move { // This is a simplified approach - in a real implementation, // we'd need to handle raw terminal input to detect ESC // For now, we'll just provide the cancellation infrastructure tokio::time::sleep(tokio::time::Duration::from_secs(3600)).await; }); // Execute task with cancellation support let execution_result = tokio::select! { result = agent.execute_task_with_timing_cancellable( input, None, false, show_prompt, show_code, true, cancellation_token ) => { esc_monitor.abort(); result } _ = tokio::signal::ctrl_c() => { cancel_token_clone.cancel(); esc_monitor.abort(); println!("\nāš ļø Operation cancelled by user (Ctrl+C)"); continue; } }; match execution_result { Ok(response) => println!("{}", response), Err(e) => { if e.to_string().contains("cancelled") { println!("āš ļø Operation cancelled by user"); } else { error!("Error: {}", e); } } } } Err(ReadlineError::Interrupted) => { println!("CTRL-C"); continue; } Err(ReadlineError::Eof) => { println!("CTRL-D"); break; } Err(err) => { error!("Error: {:?}", err); break; } } } // Save history before exiting if let Some(ref history_path) = history_file { let _ = rl.save_history(history_path); } println!("šŸ‘‹ Goodbye!"); Ok(()) } async fn run_autonomous(mut agent: Agent, show_prompt: bool, show_code: bool) -> Result<()> { // Set up workspace directory let workspace_dir = setup_workspace_directory()?; // Set up logging let logger = AutonomousLogger::new(&workspace_dir)?; logger.log_section("G3 AUTONOMOUS MODE SESSION STARTED"); logger.log(&format!("šŸ¤– G3 AI Coding Agent - Autonomous Mode")); logger.log(&format!("šŸ“ Using workspace directory: {}", workspace_dir.display())); // Change to workspace directory std::env::set_current_dir(&workspace_dir)?; logger.log("šŸ“‚ Changed to workspace directory"); logger.log("šŸŽÆ Looking for requirements.md in workspace directory..."); // Check if requirements.md exists let requirements_path = workspace_dir.join("requirements.md"); if !requirements_path.exists() { logger.log("āŒ Error: requirements.md not found in workspace directory"); logger.log(&format!(" Please create a requirements.md file with your project requirements at:")); logger.log(&format!(" {}", requirements_path.display())); return Ok(()); } // Read requirements.md let requirements = match std::fs::read_to_string(&requirements_path) { Ok(content) => content, Err(e) => { logger.log(&format!("āŒ Error reading requirements.md: {}", e)); return Ok(()); } }; logger.log("šŸ“‹ Requirements loaded from requirements.md"); logger.log(&format!("Requirements content:\n{}", requirements)); logger.log("šŸ”„ Starting coach-player feedback loop..."); logger.log(""); const MAX_TURNS: usize = 5; let mut turn = 1; let mut coach_feedback = String::new(); loop { logger.log_section(&format!("TURN {}/{} - PLAYER MODE", turn, MAX_TURNS)); // Player mode: implement requirements (with coach feedback if available) let player_prompt = if coach_feedback.is_empty() { format!( "You are G3 in implementation mode. Read and implement the following requirements:\n\n{}\n\nImplement this step by step, creating all necessary files and code.", requirements ) } else { format!( "You are G3 in implementation mode. You need to address the coach's feedback and improve your implementation.\n\nORIGINAL REQUIREMENTS:\n{}\n\nCOACH FEEDBACK TO ADDRESS:\n{}\n\nPlease make the necessary improvements to address the coach's feedback while ensuring all original requirements are met.", requirements, coach_feedback ) }; logger.log("šŸŽÆ Starting player implementation..."); if !coach_feedback.is_empty() { logger.log("šŸ“ Incorporating coach feedback from previous turn"); } let _player_result = agent .execute_task_with_timing(&player_prompt, None, false, show_prompt, show_code, true) .await?; logger.log("šŸŽÆ Player implementation completed"); logger.log(""); // Create a new agent instance for coach mode to ensure fresh context // Make sure the coach agent also operates in the workspace directory let config = g3_config::Config::load(None)?; let mut coach_agent = Agent::new(config).await?; // Ensure coach agent is also in the workspace directory std::env::set_current_dir(&workspace_dir)?; logger.log_section(&format!("TURN {}/{} - COACH MODE", turn, MAX_TURNS)); // Coach mode: critique the implementation let coach_prompt = format!( "You are G3 in coach mode. Your role is to critique and review implementations against requirements. REQUIREMENTS: {} IMPLEMENTATION REVIEW: Review the current state of the project and provide a concise critique focusing on: 1. Whether the requirements are correctly implemented 2. What's missing or incorrect 3. Specific improvements needed If the implementation correctly meets all requirements, respond with: 'IMPLEMENTATION_APPROVED' If improvements are needed, provide specific actionable feedback. Keep your response concise and focused on actionable items.", requirements ); logger.log("šŸŽ“ Starting coach review..."); let coach_result = coach_agent .execute_task_with_timing(&coach_prompt, None, false, show_prompt, show_code, true) .await?; logger.log("šŸŽ“ Coach review completed"); logger.log(&format!("Coach feedback: {}", coach_result)); // Check if coach approved the implementation if coach_result.contains("IMPLEMENTATION_APPROVED") { logger.log_section("SESSION COMPLETED - IMPLEMENTATION APPROVED"); logger.log("āœ… Coach approved the implementation!"); logger.log("šŸŽ‰ Autonomous mode completed successfully"); break; } // Check if we've reached max turns if turn >= MAX_TURNS { logger.log_section("SESSION COMPLETED - MAX TURNS REACHED"); logger.log(&format!("ā° Maximum turns ({}) reached", MAX_TURNS)); logger.log("šŸ”„ Autonomous mode completed (max iterations)"); break; } // Store coach feedback for next iteration coach_feedback = coach_result; turn += 1; logger.log("šŸ”„ Coach provided feedback for next iteration"); logger.log(&format!("šŸ“ Preparing to incorporate feedback in turn {}", turn)); logger.log(""); } logger.log_section("G3 AUTONOMOUS MODE SESSION ENDED"); Ok(()) } fn display_context_progress(agent: &Agent) { let context = agent.get_context_window(); let percentage = context.percentage_used(); // Create a simple visual progress bar using the requested characters (10 dots max) let bar_width = 10; let filled_width = ((percentage / 100.0) * bar_width as f32) as usize; let empty_width = bar_width - filled_width; let filled_chars = "ā—".repeat(filled_width); let empty_chars = "ā—‹".repeat(empty_width); let progress_bar = format!("{}{}", filled_chars, empty_chars); // Print context info with visual progress bar println!( "Context: {} {:.1}% | {}/{} tokens", progress_bar, percentage, context.used_tokens, context.total_tokens ); } /// Set up the workspace directory for autonomous mode /// Uses G3_WORKSPACE environment variable or defaults to ~/tmp/workspace fn setup_workspace_directory() -> Result { let workspace_dir = if let Ok(env_workspace) = std::env::var("G3_WORKSPACE") { PathBuf::from(env_workspace) } else { // Default to ~/tmp/workspace let home_dir = dirs::home_dir() .ok_or_else(|| anyhow::anyhow!("Could not determine home directory"))?; home_dir.join("tmp").join("workspace") }; // Create the directory if it doesn't exist if !workspace_dir.exists() { std::fs::create_dir_all(&workspace_dir)?; println!("šŸ“ Created workspace directory: {}", workspace_dir.display()); } Ok(workspace_dir) } /// Logger for autonomous mode that writes to both console and log file struct AutonomousLogger { log_writer: Arc>>, } impl AutonomousLogger { fn new(workspace_dir: &PathBuf) -> Result { // Create logs subdirectory let logs_dir = workspace_dir.join("logs"); if !logs_dir.exists() { std::fs::create_dir_all(&logs_dir)?; } // Create log file with timestamp in logs subdirectory let timestamp = chrono::Utc::now().format("%Y%m%d_%H%M%S"); let log_path = logs_dir.join(format!("g3_autonomous_{}.log", timestamp)); let file = OpenOptions::new() .create(true) .write(true) .append(true) .open(&log_path)?; let log_writer = Arc::new(Mutex::new(BufWriter::new(file))); println!("šŸ“ Logging autonomous session to: {}", log_path.display()); Ok(Self { log_writer }) } fn log(&self, message: &str) { // Print to console println!("{}", message); // Write to log file with timestamp if let Ok(mut writer) = self.log_writer.lock() { let timestamp = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC"); let _ = writeln!(writer, "[{}] {}", timestamp, message); let _ = writer.flush(); } } fn log_section(&self, section: &str) { let separator = "=".repeat(80); let message = format!("{}\n{}\n{}", separator, section, separator); self.log(&message); } fn log_subsection(&self, subsection: &str) { let separator = "-".repeat(60); let message = format!("{}\n{}\n{}", separator, subsection, separator); self.log(&message); } }