write/read file support
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -784,6 +784,7 @@ name = "g3-cli"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"clap",
|
||||
"dirs 5.0.1",
|
||||
"g3-config",
|
||||
|
||||
@@ -18,3 +18,4 @@ rustyline = "17.0.1"
|
||||
dirs = "5.0"
|
||||
tokio-util = "0.7"
|
||||
indicatif = "0.17"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
|
||||
@@ -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?;
|
||||
|
||||
println!("━━━ Turn {}/{} - Coach Mode ━━━", turn, MAX_TURNS);
|
||||
// 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!(
|
||||
@@ -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<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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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)),
|
||||
}
|
||||
@@ -1305,22 +1308,32 @@ The tool will execute immediately and you'll receive the result (success or erro
|
||||
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,18 +1344,33 @@ 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");
|
||||
|
||||
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) {
|
||||
(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!("Editing file: {} (lines {}-{})", path_str, start_num, end_num);
|
||||
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 {
|
||||
@@ -1352,22 +1380,35 @@ The tool will execute immediately and you'll receive the result (success or erro
|
||||
// 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
|
||||
@@ -1378,6 +1419,7 @@ The tool will execute immediately and you'll receive the result (success or erro
|
||||
};
|
||||
|
||||
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();
|
||||
@@ -1395,6 +1437,7 @@ The tool will execute immediately and you'll receive the result (success or erro
|
||||
|
||||
// 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) {
|
||||
@@ -1403,13 +1446,29 @@ The tool will execute immediately and you'll receive the result (success or erro
|
||||
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" => {
|
||||
|
||||
Reference in New Issue
Block a user