write/read file support

This commit is contained in:
Dhanji Prasanna
2025-09-27 13:43:09 +10:00
parent 7595ee083e
commit e82821189b
4 changed files with 255 additions and 85 deletions

1
Cargo.lock generated
View File

@@ -784,6 +784,7 @@ name = "g3-cli"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono",
"clap", "clap",
"dirs 5.0.1", "dirs 5.0.1",
"g3-config", "g3-config",

View File

@@ -18,3 +18,4 @@ rustyline = "17.0.1"
dirs = "5.0" dirs = "5.0"
tokio-util = "0.7" tokio-util = "0.7"
indicatif = "0.17" indicatif = "0.17"
chrono = { version = "0.4", features = ["serde"] }

View File

@@ -4,7 +4,10 @@ use g3_config::Config;
use g3_core::Agent; use g3_core::Agent;
use rustyline::error::ReadlineError; use rustyline::error::ReadlineError;
use rustyline::DefaultEditor; 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 tokio_util::sync::CancellationToken;
use tracing::{error, info}; 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<()> { async fn run_autonomous(mut agent: Agent, show_prompt: bool, show_code: bool) -> Result<()> {
println!("🤖 G3 AI Coding Agent - Autonomous Mode"); // Set up workspace directory
println!("🎯 Looking for requirements.md in current 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 // 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() { if !requirements_path.exists() {
println!("❌ Error: requirements.md not found in current directory"); logger.log("❌ Error: requirements.md not found in workspace directory");
println!(" Please create a requirements.md file with your project requirements"); logger.log(&format!(" Please create a requirements.md file with your project requirements at:"));
logger.log(&format!(" {}", requirements_path.display()));
return Ok(()); return Ok(());
} }
// Read requirements.md // 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, Ok(content) => content,
Err(e) => { Err(e) => {
println!("❌ Error reading requirements.md: {}", e); logger.log(&format!("❌ Error reading requirements.md: {}", e));
return Ok(()); return Ok(());
} }
}; };
println!("📋 Requirements loaded from requirements.md"); logger.log("📋 Requirements loaded from requirements.md");
println!("🔄 Starting coach-player feedback loop..."); logger.log(&format!("Requirements content:\n{}", requirements));
println!(); logger.log("🔄 Starting coach-player feedback loop...");
logger.log("");
const MAX_TURNS: usize = 5; const MAX_TURNS: usize = 5;
let mut turn = 1; let mut turn = 1;
let mut coach_feedback = String::new(); let mut coach_feedback = String::new();
loop { 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) // Player mode: implement requirements (with coach feedback if available)
let player_prompt = if coach_feedback.is_empty() { 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 let _player_result = agent
.execute_task_with_timing(&player_prompt, None, false, show_prompt, show_code, true) .execute_task_with_timing(&player_prompt, None, false, show_prompt, show_code, true)
.await?; .await?;
println!("\n🎯 Player implementation completed"); logger.log("🎯 Player implementation completed");
println!(); logger.log("");
// Create a new agent instance for coach mode to ensure fresh context // 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 config = g3_config::Config::load(None)?;
let mut coach_agent = Agent::new(config).await?; 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 // Coach mode: critique the implementation
let coach_prompt = format!( let coach_prompt = format!(
@@ -295,23 +322,28 @@ Keep your response concise and focused on actionable items.",
requirements requirements
); );
logger.log("🎓 Starting coach review...");
let coach_result = coach_agent let coach_result = coach_agent
.execute_task_with_timing(&coach_prompt, None, false, show_prompt, show_code, true) .execute_task_with_timing(&coach_prompt, None, false, show_prompt, show_code, true)
.await?; .await?;
println!("\n🎓 Coach review completed"); logger.log("🎓 Coach review completed");
logger.log(&format!("Coach feedback: {}", coach_result));
// Check if coach approved the implementation // Check if coach approved the implementation
if coach_result.contains("IMPLEMENTATION_APPROVED") { if coach_result.contains("IMPLEMENTATION_APPROVED") {
println!("\n✅ Coach approved the implementation!"); logger.log_section("SESSION COMPLETED - IMPLEMENTATION APPROVED");
println!("🎉 Autonomous mode completed successfully"); logger.log("✅ Coach approved the implementation!");
logger.log("🎉 Autonomous mode completed successfully");
break; break;
} }
// Check if we've reached max turns // Check if we've reached max turns
if turn >= MAX_TURNS { if turn >= MAX_TURNS {
println!("\n⏰ Maximum turns ({}) reached", MAX_TURNS); logger.log_section("SESSION COMPLETED - MAX TURNS REACHED");
println!("🔄 Autonomous mode completed (max iterations)"); logger.log(&format!("⏰ Maximum turns ({}) reached", MAX_TURNS));
logger.log("🔄 Autonomous mode completed (max iterations)");
break; break;
} }
@@ -319,11 +351,12 @@ Keep your response concise and focused on actionable items.",
coach_feedback = coach_result; coach_feedback = coach_result;
turn += 1; turn += 1;
println!("\n🔄 Coach provided feedback for next iteration"); logger.log("🔄 Coach provided feedback for next iteration");
println!("📝 Preparing to incorporate feedback in turn {}", turn); logger.log(&format!("📝 Preparing to incorporate feedback in turn {}", turn));
println!(); logger.log("");
} }
logger.log_section("G3 AUTONOMOUS MODE SESSION ENDED");
Ok(()) 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<PathBuf> {
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<Mutex<BufWriter<std::fs::File>>>,
}
impl AutonomousLogger {
fn new(workspace_dir: &PathBuf) -> Result<Self> {
// 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);
}
}

View File

@@ -853,32 +853,32 @@ The tool will execute immediately and you'll receive the result (success or erro
"required": ["file_path", "content"] "required": ["file_path", "content"]
}), }),
}, },
Tool { // Tool {
name: "edit_file".to_string(), // name: "edit_file".to_string(),
description: "Edit a specific range of lines in a file".to_string(), // description: "Edit a specific range of lines in a file".to_string(),
input_schema: json!({ // input_schema: json!({
"type": "object", // "type": "object",
"properties": { // "properties": {
"file_path": { // "file_path": {
"type": "string", // "type": "string",
"description": "The path to the file to edit" // "description": "The path to the file to edit"
}, // },
"start_line": { // "start_line": {
"type": "integer", // "type": "integer",
"description": "The starting line number (1-based) of the range to replace" // "description": "The starting line number (1-based) of the range to replace"
}, // },
"end_line": { // "end_line": {
"type": "integer", // "type": "integer",
"description": "The ending line number (1-based) of the range to replace" // "description": "The ending line number (1-based) of the range to replace"
}, // },
"new_text": { // "new_text": {
"type": "string", // "type": "string",
"description": "The new text to replace the specified range" // "description": "The new text to replace the specified range"
} // }
}, // },
"required": ["file_path", "start_line", "end_line", "new_text"] // "required": ["file_path", "start_line", "end_line", "new_text"]
}), // }),
}, // },
Tool { Tool {
name: "final_output".to_string(), name: "final_output".to_string(),
description: "Signal task completion with a detailed summary".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 { } else {
s.clone() s.clone()
} }
}, }
_ => value.to_string(), _ => value.to_string(),
}; };
println!("{}", value_str); 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) { match std::fs::read_to_string(path_str) {
Ok(content) => { Ok(content) => {
let line_count = content.lines().count(); 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)), 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"); debug!("Processing write_file tool call");
let file_path = tool_call.args.get("file_path"); let file_path = tool_call.args.get("file_path");
let content = tool_call.args.get("content"); let content = tool_call.args.get("content");
if let (Some(path_val), Some(content_val)) = (file_path, 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); debug!("Writing to file: {}", path_str);
// Create parent directories if they don't exist // Create parent directories if they don't exist
if let Some(parent) = std::path::Path::new(path_str).parent() { if let Some(parent) = std::path::Path::new(path_str).parent() {
if let Err(e) = std::fs::create_dir_all(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) { match std::fs::write(path_str, content_str) {
Ok(()) => { Ok(()) => {
let line_count = content_str.lines().count(); 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 { } else {
Ok("❌ Invalid file_path or content argument".to_string()) 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" => { "edit_file" => {
debug!("Processing edit_file tool call"); debug!("Processing edit_file tool call");
debug!("Raw tool_call.args: {:?}", tool_call.args);
let file_path = tool_call.args.get("file_path"); let file_path = tool_call.args.get("file_path");
let start_line = tool_call.args.get("start_line"); let start_line = tool_call.args.get("start_line");
let end_line = tool_call.args.get("end_line"); let end_line = tool_call.args.get("end_line");
let new_text = tool_call.args.get("new_text"); let new_text = tool_call.args.get("new_text");
if let (Some(path_val), Some(start_val), Some(end_val), Some(text_val)) = debug!("Extracted values - file_path: {:?}, start_line: {:?}, end_line: {:?}, new_text: {:?}",
(file_path, start_line, end_line, new_text) { file_path, start_line, end_line, new_text);
if let (Some(path_str), Some(start_num), Some(end_num), Some(text_str)) = if let (Some(path_val), Some(start_val), Some(end_val), Some(text_val)) =
(path_val.as_str(), start_val.as_i64(), end_val.as_i64(), text_val.as_str()) { (file_path, start_line, end_line, new_text)
{
debug!("Editing file: {} (lines {}-{})", path_str, start_num, end_num); 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 // Validate line numbers
if start_num < 1 || end_num < 1 || start_num > end_num { 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()); 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 // Read the current file content
let original_content = match std::fs::read_to_string(path_str) { let original_content = match std::fs::read_to_string(path_str) {
Ok(content) => content, 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 lines: Vec<&str> = original_content.lines().collect();
let total_lines = lines.len(); let total_lines = lines.len();
debug!("File has {} lines", total_lines);
// Convert to 0-based indexing // Convert to 0-based indexing
let start_idx = (start_num - 1) as usize; let start_idx = (start_num - 1) as usize;
let end_idx = (end_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 // Validate line ranges
if start_idx >= total_lines { 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 { 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 // Split new_text into lines
let new_lines: Vec<&str> = if text_str.is_empty() { let new_lines: Vec<&str> = if text_str.is_empty() {
vec![] vec![]
} else { } else {
text_str.lines().collect() text_str.lines().collect()
}; };
let new_lines_count = new_lines.len(); let new_lines_count = new_lines.len();
debug!("New text has {} lines", new_lines_count);
// Create the new content // Create the new content
let mut new_content_lines = Vec::new(); let mut new_content_lines = Vec::new();
// Add lines before the edit range // Add lines before the edit range
new_content_lines.extend_from_slice(&lines[..start_idx]); new_content_lines.extend_from_slice(&lines[..start_idx]);
// Add the new lines // Add the new lines
new_content_lines.extend(new_lines); new_content_lines.extend(new_lines);
// Add lines after the edit range // Add lines after the edit range
if end_idx + 1 < lines.len() { if end_idx + 1 < lines.len() {
new_content_lines.extend_from_slice(&lines[end_idx + 1..]); new_content_lines.extend_from_slice(&lines[end_idx + 1..]);
} }
// Join the lines back together // Join the lines back together
let new_content = new_content_lines.join("\n"); let new_content = new_content_lines.join("\n");
debug!("New content length: {} characters", new_content.len());
// Write the modified content back to the file // Write the modified content back to the file
match std::fs::write(path_str, &new_content) { match std::fs::write(path_str, &new_content) {
Ok(()) => { Ok(()) => {
let old_range_size = end_idx - start_idx + 1; 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)) 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 { } 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()) 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 { } 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" => { "final_output" => {