From e82821189b3aff1410165d8c61c9d448442bd468 Mon Sep 17 00:00:00 2001 From: Dhanji Prasanna Date: Sat, 27 Sep 2025 13:43:09 +1000 Subject: [PATCH] write/read file support --- Cargo.lock | 1 + crates/g3-cli/Cargo.toml | 1 + crates/g3-cli/src/lib.rs | 155 +++++++++++++++++++++++++++----- crates/g3-core/src/lib.rs | 183 +++++++++++++++++++++++++------------- 4 files changed, 255 insertions(+), 85 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index fa811ed..d8fe145 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -784,6 +784,7 @@ name = "g3-cli" version = "0.1.0" dependencies = [ "anyhow", + "chrono", "clap", "dirs 5.0.1", "g3-config", diff --git a/crates/g3-cli/Cargo.toml b/crates/g3-cli/Cargo.toml index 08e55e8..87c479f 100644 --- a/crates/g3-cli/Cargo.toml +++ b/crates/g3-cli/Cargo.toml @@ -18,3 +18,4 @@ rustyline = "17.0.1" dirs = "5.0" tokio-util = "0.7" indicatif = "0.17" +chrono = { version = "0.4", features = ["serde"] } diff --git a/crates/g3-cli/src/lib.rs b/crates/g3-cli/src/lib.rs index abf309c..f6ac1b4 100644 --- a/crates/g3-cli/src/lib.rs +++ b/crates/g3-cli/src/lib.rs @@ -4,7 +4,10 @@ use g3_config::Config; use g3_core::Agent; use rustyline::error::ReadlineError; use rustyline::DefaultEditor; -use std::io::Write; +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}; @@ -218,36 +221,51 @@ 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<()> { - println!("šŸ¤– G3 AI Coding Agent - Autonomous Mode"); - println!("šŸŽÆ Looking for requirements.md in current directory..."); + // 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 = std::path::Path::new("requirements.md"); + let requirements_path = workspace_dir.join("requirements.md"); if !requirements_path.exists() { - println!("āŒ Error: requirements.md not found in current directory"); - println!(" Please create a requirements.md file with your project requirements"); + 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) { + let requirements = match std::fs::read_to_string(&requirements_path) { Ok(content) => content, Err(e) => { - println!("āŒ Error reading requirements.md: {}", e); + logger.log(&format!("āŒ Error reading requirements.md: {}", e)); return Ok(()); } }; - println!("šŸ“‹ Requirements loaded from requirements.md"); - println!("šŸ”„ Starting coach-player feedback loop..."); - println!(); + 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 { - println!("━━━ Turn {}/{} - Player Mode ━━━", turn, MAX_TURNS); + 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() { @@ -262,18 +280,27 @@ async fn run_autonomous(mut agent: Agent, show_prompt: bool, show_code: bool) -> ) }; + 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?; - println!("\nšŸŽÆ Player implementation completed"); - println!(); + 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)?; - println!("━━━ Turn {}/{} - Coach Mode ━━━", turn, MAX_TURNS); + logger.log_section(&format!("TURN {}/{} - COACH MODE", turn, MAX_TURNS)); // Coach mode: critique the implementation let coach_prompt = format!( @@ -295,23 +322,28 @@ 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?; - println!("\nšŸŽ“ Coach review completed"); + logger.log("šŸŽ“ Coach review completed"); + logger.log(&format!("Coach feedback: {}", coach_result)); // Check if coach approved the implementation if coach_result.contains("IMPLEMENTATION_APPROVED") { - println!("\nāœ… Coach approved the implementation!"); - println!("šŸŽ‰ Autonomous mode completed successfully"); + 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 { - println!("\nā° Maximum turns ({}) reached", MAX_TURNS); - println!("šŸ”„ Autonomous mode completed (max iterations)"); + logger.log_section("SESSION COMPLETED - MAX TURNS REACHED"); + logger.log(&format!("ā° Maximum turns ({}) reached", MAX_TURNS)); + logger.log("šŸ”„ Autonomous mode completed (max iterations)"); break; } @@ -319,11 +351,12 @@ Keep your response concise and focused on actionable items.", coach_feedback = coach_result; turn += 1; - println!("\nšŸ”„ Coach provided feedback for next iteration"); - println!("šŸ“ Preparing to incorporate feedback in turn {}", turn); - println!(); + 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(()) } @@ -347,4 +380,80 @@ fn display_context_progress(agent: &Agent) { ); } +/// 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); + } +} + diff --git a/crates/g3-core/src/lib.rs b/crates/g3-core/src/lib.rs index 0d4000f..974175c 100644 --- a/crates/g3-core/src/lib.rs +++ b/crates/g3-core/src/lib.rs @@ -853,32 +853,32 @@ The tool will execute immediately and you'll receive the result (success or erro "required": ["file_path", "content"] }), }, - Tool { - name: "edit_file".to_string(), - description: "Edit a specific range of lines in a file".to_string(), - input_schema: json!({ - "type": "object", - "properties": { - "file_path": { - "type": "string", - "description": "The path to the file to edit" - }, - "start_line": { - "type": "integer", - "description": "The starting line number (1-based) of the range to replace" - }, - "end_line": { - "type": "integer", - "description": "The ending line number (1-based) of the range to replace" - }, - "new_text": { - "type": "string", - "description": "The new text to replace the specified range" - } - }, - "required": ["file_path", "start_line", "end_line", "new_text"] - }), - }, + // Tool { + // name: "edit_file".to_string(), + // description: "Edit a specific range of lines in a file".to_string(), + // input_schema: json!({ + // "type": "object", + // "properties": { + // "file_path": { + // "type": "string", + // "description": "The path to the file to edit" + // }, + // "start_line": { + // "type": "integer", + // "description": "The starting line number (1-based) of the range to replace" + // }, + // "end_line": { + // "type": "integer", + // "description": "The ending line number (1-based) of the range to replace" + // }, + // "new_text": { + // "type": "string", + // "description": "The new text to replace the specified range" + // } + // }, + // "required": ["file_path", "start_line", "end_line", "new_text"] + // }), + // }, Tool { name: "final_output".to_string(), description: "Signal task completion with a detailed summary".to_string(), @@ -1074,7 +1074,7 @@ The tool will execute immediately and you'll receive the result (success or erro } else { s.clone() } - }, + } _ => value.to_string(), }; println!("│ {}", value_str); @@ -1288,7 +1288,10 @@ The tool will execute immediately and you'll receive the result (success or erro match std::fs::read_to_string(path_str) { Ok(content) => { let line_count = content.lines().count(); - Ok(format!("šŸ“„ File content ({} lines):\n{}", line_count, content)) + Ok(format!( + "šŸ“„ File content ({} lines):\n{}", + line_count, content + )) } Err(e) => Ok(format!("āŒ Failed to read file '{}': {}", path_str, e)), } @@ -1303,24 +1306,34 @@ The tool will execute immediately and you'll receive the result (success or erro debug!("Processing write_file tool call"); let file_path = tool_call.args.get("file_path"); let content = tool_call.args.get("content"); - + if let (Some(path_val), Some(content_val)) = (file_path, content) { - if let (Some(path_str), Some(content_str)) = (path_val.as_str(), content_val.as_str()) { + if let (Some(path_str), Some(content_str)) = + (path_val.as_str(), content_val.as_str()) + { debug!("Writing to file: {}", path_str); - + // Create parent directories if they don't exist if let Some(parent) = std::path::Path::new(path_str).parent() { if let Err(e) = std::fs::create_dir_all(parent) { - return Ok(format!("āŒ Failed to create parent directories for '{}': {}", path_str, e)); + return Ok(format!( + "āŒ Failed to create parent directories for '{}': {}", + path_str, e + )); } } - + match std::fs::write(path_str, content_str) { Ok(()) => { let line_count = content_str.lines().count(); - Ok(format!("āœ… Successfully wrote {} lines to '{}'", line_count, path_str)) + Ok(format!( + "āœ… Successfully wrote {} lines to '{}'", + line_count, path_str + )) + } + Err(e) => { + Ok(format!("āŒ Failed to write to file '{}': {}", path_str, e)) } - Err(e) => Ok(format!("āŒ Failed to write to file '{}': {}", path_str, e)), } } else { Ok("āŒ Invalid file_path or content argument".to_string()) @@ -1331,85 +1344,131 @@ The tool will execute immediately and you'll receive the result (success or erro } "edit_file" => { debug!("Processing edit_file tool call"); + debug!("Raw tool_call.args: {:?}", tool_call.args); + let file_path = tool_call.args.get("file_path"); let start_line = tool_call.args.get("start_line"); let end_line = tool_call.args.get("end_line"); let new_text = tool_call.args.get("new_text"); - - if let (Some(path_val), Some(start_val), Some(end_val), Some(text_val)) = - (file_path, start_line, end_line, new_text) { - - if let (Some(path_str), Some(start_num), Some(end_num), Some(text_str)) = - (path_val.as_str(), start_val.as_i64(), end_val.as_i64(), text_val.as_str()) { - - debug!("Editing file: {} (lines {}-{})", path_str, start_num, end_num); - + + debug!("Extracted values - file_path: {:?}, start_line: {:?}, end_line: {:?}, new_text: {:?}", + file_path, start_line, end_line, new_text); + + if let (Some(path_val), Some(start_val), Some(end_val), Some(text_val)) = + (file_path, start_line, end_line, new_text) + { + debug!("All required arguments present"); + debug!( + "path_val: {:?}, start_val: {:?}, end_val: {:?}, text_val: {:?}", + path_val, start_val, end_val, text_val + ); + + if let (Some(path_str), Some(start_num), Some(end_num), Some(text_str)) = ( + path_val.as_str(), + start_val.as_i64(), + end_val.as_i64(), + text_val.as_str(), + ) { + debug!("Successfully converted types - path: {}, start: {}, end: {}, text_len: {}", + path_str, start_num, end_num, text_str.len()); + // Validate line numbers if start_num < 1 || end_num < 1 || start_num > end_num { return Ok("āŒ Invalid line numbers: start_line and end_line must be >= 1 and start_line <= end_line".to_string()); } - + // Read the current file content let original_content = match std::fs::read_to_string(path_str) { Ok(content) => content, - Err(e) => return Ok(format!("āŒ Failed to read file '{}': {}", path_str, e)), + Err(e) => { + return Ok(format!("āŒ Failed to read file '{}': {}", path_str, e)) + } }; - + let lines: Vec<&str> = original_content.lines().collect(); let total_lines = lines.len(); - + debug!("File has {} lines", total_lines); + // Convert to 0-based indexing let start_idx = (start_num - 1) as usize; let end_idx = (end_num - 1) as usize; - + debug!( + "Using 0-based indices: start_idx={}, end_idx={}", + start_idx, end_idx + ); + // Validate line ranges if start_idx >= total_lines { - return Ok(format!("āŒ start_line {} is beyond file length ({} lines)", start_num, total_lines)); + return Ok(format!( + "āŒ start_line {} is beyond file length ({} lines)", + start_num, total_lines + )); } if end_idx >= total_lines { - return Ok(format!("āŒ end_line {} is beyond file length ({} lines)", end_num, total_lines)); + return Ok(format!( + "āŒ end_line {} is beyond file length ({} lines)", + end_num, total_lines + )); } - + // Split new_text into lines let new_lines: Vec<&str> = if text_str.is_empty() { vec![] } else { text_str.lines().collect() }; - + let new_lines_count = new_lines.len(); - + debug!("New text has {} lines", new_lines_count); + // Create the new content let mut new_content_lines = Vec::new(); - + // Add lines before the edit range new_content_lines.extend_from_slice(&lines[..start_idx]); - + // Add the new lines new_content_lines.extend(new_lines); - + // Add lines after the edit range if end_idx + 1 < lines.len() { new_content_lines.extend_from_slice(&lines[end_idx + 1..]); } - + // Join the lines back together let new_content = new_content_lines.join("\n"); - + debug!("New content length: {} characters", new_content.len()); + // Write the modified content back to the file match std::fs::write(path_str, &new_content) { Ok(()) => { let old_range_size = end_idx - start_idx + 1; - Ok(format!("āœ… Successfully edited '{}': replaced {} lines ({}:{}) with {} lines", + Ok(format!("āœ… Successfully edited '{}': replaced {} lines ({}:{}) with {} lines", path_str, old_range_size, start_num, end_num, new_lines_count)) } - Err(e) => Ok(format!("āŒ Failed to write edited content to '{}': {}", path_str, e)), + Err(e) => Ok(format!( + "āŒ Failed to write edited content to '{}': {}", + path_str, e + )), } } else { + debug!("Type conversion failed:"); + debug!(" path_val.as_str(): {:?}", path_val.as_str()); + debug!(" start_val.as_i64(): {:?}", start_val.as_i64()); + debug!(" end_val.as_i64(): {:?}", end_val.as_i64()); + debug!(" text_val.as_str(): {:?}", text_val.as_str()); Ok("āŒ Invalid argument types: file_path must be string, start_line and end_line must be integers, new_text must be string".to_string()) } } else { - Ok("āŒ Missing required arguments: file_path, start_line, end_line, new_text".to_string()) + debug!("Missing required arguments:"); + debug!(" file_path present: {}", file_path.is_some()); + debug!(" start_line present: {}", start_line.is_some()); + debug!(" end_line present: {}", end_line.is_some()); + debug!(" new_text present: {}", new_text.is_some()); + Ok( + "āŒ Missing required arguments: file_path, start_line, end_line, new_text" + .to_string(), + ) } } "final_output" => {