diff --git a/crates/g3-cli/src/lib.rs b/crates/g3-cli/src/lib.rs index 5b87257..085e733 100644 --- a/crates/g3-cli/src/lib.rs +++ b/crates/g3-cli/src/lib.rs @@ -5,7 +5,7 @@ use g3_core::Agent; use rustyline::error::ReadlineError; use rustyline::DefaultEditor; use std::fs::OpenOptions; -use std::io::{Write, BufWriter}; +use std::io::{BufWriter, Write}; use std::path::PathBuf; use std::sync::{Arc, Mutex}; use tokio_util::sync::CancellationToken; @@ -45,7 +45,7 @@ pub async fn run() -> Result<()> { // 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() @@ -223,25 +223,30 @@ async fn run_interactive(mut agent: Agent, show_prompt: bool, show_code: bool) - 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())); - + 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!( + " Please create a requirements.md file with your project requirements at:" + )); logger.log(&format!(" {}", requirements_path.display())); return Ok(()); } @@ -256,11 +261,14 @@ async fn run_autonomous(mut agent: Agent, show_prompt: bool, show_code: bool) -> }; logger.log("📋 Requirements loaded from requirements.md"); - logger.log(&format!("Requirements: {}", logger.truncate_for_log(&requirements, 150))); - + logger.log(&format!( + "Requirements: {}", + logger.truncate_for_log(&requirements, 150) + )); + // Check if there are existing project files (skip first player turn if so) let has_existing_files = check_existing_project_files(&workspace_dir, &logger)?; - + logger.log("🔄 Starting coach-player feedback loop..."); logger.log(""); @@ -272,12 +280,15 @@ async fn run_autonomous(mut agent: Agent, show_prompt: bool, show_code: bool) -> loop { // Skip player turn if we have existing files and this is the first iteration if skip_player_turn { - logger.log_section(&format!("TURN {}/{} - SKIPPING PLAYER MODE", turn, MAX_TURNS)); + logger.log_section(&format!( + "TURN {}/{} - SKIPPING PLAYER MODE", + turn, MAX_TURNS + )); logger.log("📁 Existing project files detected, skipping to coach evaluation"); skip_player_turn = false; // Only skip the first turn } else { 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!( @@ -308,12 +319,12 @@ async fn run_autonomous(mut agent: Agent, show_prompt: bool, show_code: bool) -> // 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. @@ -328,7 +339,8 @@ Review the current state of the project and provide a concise critique focusing 3. Specific improvements needed If the implementation correctly meets all requirements, respond with: 'IMPLEMENTATION_APPROVED' -If improvements are needed, provide specific actionable feedback. +If improvements are needed, provide specific actionable feedback. Don't be overly critical. APPROVE the +implementation if it generally fits the bill, doesn't have compile errors or glaring omissions. Keep your response concise and focused on actionable items.", requirements @@ -362,9 +374,12 @@ Keep your response concise and focused on actionable items.", // 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(&format!( + "📝 Preparing to incorporate feedback in turn {}", + turn + )); logger.log(""); } @@ -374,9 +389,12 @@ Keep your response concise and focused on actionable items.", /// Check if there are existing project files in the workspace directory /// Returns true if project files are found (excluding requirements.md and logs directory) -fn check_existing_project_files(workspace_dir: &PathBuf, logger: &AutonomousLogger) -> Result { +fn check_existing_project_files( + workspace_dir: &PathBuf, + logger: &AutonomousLogger, +) -> Result { logger.log("🔍 Checking for existing project files..."); - + let entries = match std::fs::read_dir(workspace_dir) { Ok(entries) => entries, Err(e) => { @@ -384,24 +402,25 @@ fn check_existing_project_files(workspace_dir: &PathBuf, logger: &AutonomousLogg return Ok(false); } }; - + let mut project_files = Vec::new(); let mut total_files = 0; - + for entry in entries { let entry = entry?; let path = entry.path(); - let file_name = path.file_name() + let file_name = path + .file_name() .and_then(|n| n.to_str()) .unwrap_or("unknown"); - + // Skip requirements.md, logs directory, and hidden files if file_name == "requirements.md" || file_name == "logs" || file_name.starts_with('.') { continue; } - + total_files += 1; - + // Collect project files for logging (limit to first 5) if project_files.len() < 5 { if path.is_dir() { @@ -411,12 +430,16 @@ fn check_existing_project_files(workspace_dir: &PathBuf, logger: &AutonomousLogg } } } - + if total_files > 0 { logger.log(&format!("📁 Found {} existing project files", total_files)); if !project_files.is_empty() { let files_display = if total_files > 5 { - format!("{} (and {} more)", project_files.join(", "), total_files - 5) + format!( + "{} (and {} more)", + project_files.join(", "), + total_files - 5 + ) } else { project_files.join(", ") }; @@ -465,7 +488,10 @@ fn setup_workspace_directory() -> Result { // 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()); + println!( + "📁 Created workspace directory: {}", + workspace_dir.display() + ); } Ok(workspace_dir) @@ -483,29 +509,29 @@ impl AutonomousLogger { 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 }) } - + /// Truncate text to a single line for logging fn truncate_for_log(&self, text: &str, max_chars: usize) -> String { // First, get the first line only let first_line = text.lines().next().unwrap_or("").trim(); - + // Then truncate if too long if first_line.len() <= max_chars { first_line.to_string() @@ -513,14 +539,14 @@ impl AutonomousLogger { format!("{}...", &first_line[..max_chars.saturating_sub(3)]) } } - + fn log(&self, message: &str) { // Ensure single line for console output let single_line_message = self.truncate_for_log(message, 200); - + // Print to console println!("{}", single_line_message); - + // Write to log file with timestamp (also single line) if let Ok(mut writer) = self.log_writer.lock() { let timestamp = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC"); @@ -528,17 +554,17 @@ impl AutonomousLogger { let _ = writer.flush(); } } - + fn log_section(&self, section: &str) { // Sections can be multi-line for visual separation, but content should be single line let single_line_section = self.truncate_for_log(section, 100); let separator = "=".repeat(80); - + // Print to console with visual formatting println!("{}", separator); println!("{}", single_line_section); println!("{}", separator); - + // Log to file as single entries if let Ok(mut writer) = self.log_writer.lock() { let timestamp = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC"); @@ -546,16 +572,16 @@ impl AutonomousLogger { let _ = writer.flush(); } } - + fn log_subsection(&self, subsection: &str) { let single_line_subsection = self.truncate_for_log(subsection, 100); let separator = "-".repeat(60); - + // Print to console with visual formatting println!("{}", separator); println!("{}", single_line_subsection); println!("{}", separator); - + // Log to file as single entry if let Ok(mut writer) = self.log_writer.lock() { let timestamp = chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC"); @@ -564,5 +590,3 @@ impl AutonomousLogger { } } } - - diff --git a/crates/g3-core/src/lib.rs b/crates/g3-core/src/lib.rs index 84627bf..dec3654 100644 --- a/crates/g3-core/src/lib.rs +++ b/crates/g3-core/src/lib.rs @@ -1409,44 +1409,41 @@ The tool will execute immediately and you'll receive the result (success or erro // Helper function to filter JSON tool calls from display content fn filter_json_tool_calls(content: &str) -> String { - let mut filtered_content = String::new(); - let mut in_json_tool_call = false; + // Check if content contains any JSON tool call patterns + let patterns = [ + r#"{"tool":"#, + r#"{ "tool":"#, + r#"{"tool" :"#, + r#"{ "tool" :"#, + ]; - for line in content.lines() { - let trimmed_line = line.trim_start(); + // Check if any pattern is found in the content + let has_tool_call_pattern = patterns.iter().any(|pattern| content.contains(pattern)); + + if has_tool_call_pattern { + // If we detect a JSON tool call pattern anywhere in the content, + // replace the entire content with the indicator + "<>".to_string() + } else { + // Check for partial JSON patterns that might be split across chunks + let trimmed = content.trim(); - // Check if this line starts with a JSON tool call pattern - if trimmed_line.starts_with(r#"{"tool":"#) || - trimmed_line.starts_with(r#"{ "tool":"#) || - trimmed_line.starts_with(r#"{"tool" :"#) || - trimmed_line.starts_with(r#"{ "tool" :"#) { - // This is the start of a JSON tool call - if !in_json_tool_call { - // First line of JSON tool call - replace with indicator - if !filtered_content.is_empty() { - filtered_content.push('\n'); - } - filtered_content.push_str("<>"); - in_json_tool_call = true; - } - // Skip this line and any subsequent lines that are part of the JSON - } else if in_json_tool_call { - // Check if this line ends the JSON tool call - if trimmed_line.ends_with('}') || trimmed_line.trim() == "}" { - // End of JSON tool call - in_json_tool_call = false; - } - // Skip this line (it's part of the JSON tool call) + // Check for partial patterns that might indicate the start of a JSON tool call + if trimmed.starts_with(r#"{"tool"#) || + trimmed.starts_with(r#"{ "tool"#) || + trimmed.starts_with(r#"{"#) && (trimmed.contains("tool") || trimmed.contains("args")) || + trimmed.contains(r#""tool":"#) || + trimmed.contains(r#""args":"#) || + trimmed.contains(r#"file_path"#) || + trimmed.contains(r#"command"#) || + (trimmed.starts_with('{') && trimmed.len() < 50 && (trimmed.contains("tool") || trimmed.contains("args"))) { + // This looks like part of a JSON tool call, suppress it + "".to_string() } else { - // Regular content line - add it - if !filtered_content.is_empty() { - filtered_content.push('\n'); - } - filtered_content.push_str(line); + // Regular content, return as-is + content.to_string() } } - - filtered_content } // Helper function to properly escape shell commands