edit file fixes

This commit is contained in:
Dhanji Prasanna
2025-09-30 20:41:14 +10:00
parent 3c4da6f974
commit b3c2c0ad30

View File

@@ -275,7 +275,6 @@ impl ContextWindow {
self.total_tokens.saturating_sub(self.used_tokens) self.total_tokens.saturating_sub(self.used_tokens)
} }
/// Check if we should trigger summarization (at 80% capacity) /// Check if we should trigger summarization (at 80% capacity)
pub fn should_summarize(&self) -> bool { pub fn should_summarize(&self) -> bool {
// Trigger at 80% OR if we're getting close to absolute limits // Trigger at 80% OR if we're getting close to absolute limits
@@ -578,6 +577,7 @@ impl Agent {
"You are G3, an AI programming agent. Your goal is to analyze, write and modify code to achieve given goals. "You are G3, an AI programming agent. Your goal is to analyze, write and modify code to achieve given goals.
You have access to tools. When you need to accomplish a task, you MUST use the appropriate tool. Do not just describe what you would do - actually use the tools. You have access to tools. When you need to accomplish a task, you MUST use the appropriate tool. Do not just describe what you would do - actually use the tools.
Always start by reading the project's README. Create one if this is a new project or making major changes.
IMPORTANT: You must call tools to achieve goals. When you receive a request: IMPORTANT: You must call tools to achieve goals. When you receive a request:
1. Analyze and identify what needs to be done 1. Analyze and identify what needs to be done
@@ -954,14 +954,18 @@ The tool will execute immediately and you'll receive the result (success or erro
); );
// Notify user about summarization // Notify user about summarization
println!("\n📊 Context window reaching capacity ({}%). Creating summary...", println!(
self.context_window.percentage_used() as u32); "\n📊 Context window reaching capacity ({}%). Creating summary...",
self.context_window.percentage_used() as u32
);
// Create summary request with FULL history // Create summary request with FULL history
let summary_prompt = self.context_window.create_summary_prompt(); let summary_prompt = self.context_window.create_summary_prompt();
// Get the full conversation history // Get the full conversation history
let conversation_text = self.context_window.conversation_history let conversation_text = self
.context_window
.conversation_history
.iter() .iter()
.map(|m| format!("{:?}: {}", m.role, m.content)) .map(|m| format!("{:?}: {}", m.role, m.content))
.collect::<Vec<_>>() .collect::<Vec<_>>()
@@ -970,13 +974,14 @@ The tool will execute immediately and you'll receive the result (success or erro
let summary_messages = vec![ let summary_messages = vec![
Message { Message {
role: MessageRole::System, role: MessageRole::System,
content: "You are a helpful assistant that creates concise summaries.".to_string(), content: "You are a helpful assistant that creates concise summaries."
.to_string(),
}, },
Message { Message {
role: MessageRole::User, role: MessageRole::User,
content: format!("Based on this conversation history, {}\n\nConversation:\n{}", content: format!(
summary_prompt, "Based on this conversation history, {}\n\nConversation:\n{}",
conversation_text summary_prompt, conversation_text
), ),
}, },
]; ];
@@ -992,7 +997,9 @@ The tool will execute immediately and you'll receive the result (success or erro
let model_limit = 200_000u32; let model_limit = 200_000u32;
let current_usage = self.context_window.used_tokens; let current_usage = self.context_window.used_tokens;
// Leave some buffer (5k tokens) for safety // Leave some buffer (5k tokens) for safety
let available = model_limit.saturating_sub(current_usage).saturating_sub(5000); let available = model_limit
.saturating_sub(current_usage)
.saturating_sub(5000);
// Cap at a reasonable summary size (10k tokens max) // Cap at a reasonable summary size (10k tokens max)
Some(available.min(10_000)) Some(available.min(10_000))
} }
@@ -1001,7 +1008,9 @@ The tool will execute immediately and you'll receive the result (success or erro
let model_limit = self.context_window.total_tokens; let model_limit = self.context_window.total_tokens;
let current_usage = self.context_window.used_tokens; let current_usage = self.context_window.used_tokens;
// Leave 1k buffer // Leave 1k buffer
let available = model_limit.saturating_sub(current_usage).saturating_sub(1000); let available = model_limit
.saturating_sub(current_usage)
.saturating_sub(1000);
// Cap at 3k for embedded models // Cap at 3k for embedded models
Some(available.min(3000)) Some(available.min(3000))
} }
@@ -1012,8 +1021,10 @@ The tool will execute immediately and you'll receive the result (success or erro
} }
}; };
info!("Requesting summary with max_tokens: {:?} (current usage: {} tokens)", info!(
summary_max_tokens, self.context_window.used_tokens); "Requesting summary with max_tokens: {:?} (current usage: {} tokens)",
summary_max_tokens, self.context_window.used_tokens
);
let summary_request = CompletionRequest { let summary_request = CompletionRequest {
messages: summary_messages, messages: summary_messages,
@@ -1604,54 +1615,42 @@ 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");
// Extract arguments // Extract arguments with better error handling
let args_obj = tool_call.args.as_object(); let args_obj = match tool_call.args.as_object() {
if args_obj.is_none() { Some(obj) => obj,
return Ok("❌ Invalid arguments: expected object".to_string()); None => return Ok("❌ Invalid arguments: expected object".to_string()),
} };
let args_obj = args_obj.unwrap();
// Get file_path let file_path = match args_obj.get("file_path").and_then(|v| v.as_str()) {
let file_path = args_obj.get("file_path") Some(path) => path,
.and_then(|v| v.as_str()); None => return Ok("❌ Missing or invalid file_path argument".to_string()),
if file_path.is_none() { };
return Ok("❌ Missing file_path argument".to_string());
}
let file_path = file_path.unwrap();
// Get content let content = match args_obj.get("content").and_then(|v| v.as_str()) {
let content = args_obj.get("content") Some(c) => c,
.and_then(|v| v.as_str()); None => return Ok("❌ Missing or invalid content argument".to_string()),
if content.is_none() { };
return Ok("❌ Missing content argument".to_string());
}
let content = content.unwrap();
// Get start_of_range let start_line = match args_obj.get("start_of_range").and_then(|v| v.as_i64()) {
let start_of_range = args_obj.get("start_of_range") Some(n) if n >= 1 => n as usize,
.and_then(|v| v.as_i64()) Some(_) => {
.map(|v| v as usize); return Ok(
if start_of_range.is_none() { "❌ start_of_range must be >= 1 (lines are 1-indexed)".to_string()
return Ok("❌ Missing or invalid start_of_range argument".to_string()); )
} }
let start_of_range = start_of_range.unwrap(); None => return Ok("❌ Missing or invalid start_of_range argument".to_string()),
};
// Get end_of_range let end_line = match args_obj.get("end_of_range").and_then(|v| v.as_i64()) {
let end_of_range = args_obj.get("end_of_range") Some(n) if n >= start_line as i64 => n as usize,
.and_then(|v| v.as_i64()) Some(_) => return Ok("❌ end_of_range must be >= start_of_range".to_string()),
.map(|v| v as usize); None => return Ok("❌ Missing or invalid end_of_range argument".to_string()),
if end_of_range.is_none() { };
return Ok("❌ Missing or invalid end_of_range argument".to_string());
}
let end_of_range = end_of_range.unwrap();
// Validate range debug!(
if start_of_range < 1 { "edit_file: path={}, start={}, end={}",
return Ok("❌ start_of_range must be >= 1 (lines are 1-indexed)".to_string()); file_path, start_line, end_line
} );
if end_of_range < start_of_range {
return Ok("❌ end_of_range must be >= start_of_range".to_string());
}
// Read the existing file // Read the existing file
let existing_content = match std::fs::read_to_string(file_path) { let existing_content = match std::fs::read_to_string(file_path) {
@@ -1659,37 +1658,69 @@ The tool will execute immediately and you'll receive the result (success or erro
Err(e) => return Ok(format!("❌ Failed to read file '{}': {}", file_path, e)), Err(e) => return Ok(format!("❌ Failed to read file '{}': {}", file_path, e)),
}; };
// Split into lines // Split into lines, preserving empty lines
let mut lines: Vec<String> = existing_content.lines().map(|s| s.to_string()).collect(); let mut lines: Vec<String> =
existing_content.lines().map(|s| s.to_string()).collect();
let original_line_count = lines.len();
// Check if range is valid // Validate the range
if start_of_range > lines.len() + 1 { if start_line > lines.len() {
return Ok(format!("❌ start_of_range {} exceeds file length ({} lines)", start_of_range, lines.len())); // Allow appending at the end if start_line == lines.len() + 1
} if start_line == lines.len() + 1 && end_line == start_line {
// This is an append operation
// Prepare new content lines lines.extend(content.lines().map(|s| s.to_string()));
let new_lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
// Calculate the actual end of range (capped at file length)
let actual_end = end_of_range.min(lines.len());
// Replace the range with new content
// Convert to 0-indexed for vector operations
let start_idx = start_of_range - 1;
let end_idx = actual_end;
// Remove old lines and insert new ones
lines.splice(start_idx..end_idx, new_lines.clone());
// Write back to file // Write back to file
let new_content = lines.join("\n"); let new_content = lines.join("\n");
match std::fs::write(file_path, &new_content) { match std::fs::write(file_path, &new_content) {
Ok(()) => { Ok(()) => {
let lines_replaced = end_idx - start_idx; let lines_added = content.lines().count();
let lines_added = new_lines.len(); return Ok(format!(
"✅ Successfully appended {} lines to '{}'. File now has {} lines (was {} lines)",
lines_added, file_path, lines.len(), original_line_count
));
}
Err(e) => {
return Ok(format!(
"❌ Failed to write to file '{}': {}",
file_path, e
))
}
}
} else {
return Ok(format!(
"❌ start_of_range {} exceeds file length ({} lines)",
start_line,
lines.len()
));
}
}
// Split the new content into lines
let new_lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
// Perform the replacement
// Convert from 1-indexed (inclusive) to 0-indexed range for splice
// splice takes start..end where end is EXCLUSIVE, so for inclusive end_line, we need end_line + 1
let start_idx = start_line - 1;
let end_idx = (end_line + 1).min(lines.len() + 1); // +1 because splice end is exclusive
let actual_end_line = end_line.min(lines.len()); // For reporting
let lines_being_replaced = actual_end_line - start_line + 1;
debug!(
"Replacing lines {}..={} (0-indexed splice: {}..{})",
start_line, end_line, start_idx, end_idx
);
lines.splice(start_idx..end_idx, new_lines.clone());
// Write the result back to the file
let new_content = lines.join("\n");
match std::fs::write(file_path, &new_content) {
Ok(()) => {
Ok(format!( Ok(format!(
"✅ Successfully edited '{}': replaced {} lines ({}-{}) with {} lines. File now has {} lines", "✅ Successfully edited '{}': replaced {} lines ({}-{}) with {} lines. File now has {} lines (was {} lines)",
file_path, lines_replaced, start_of_range, actual_end, lines_added, lines.len() file_path, lines_being_replaced, start_line, actual_end_line,
new_lines.len(), lines.len(), original_line_count
)) ))
} }
Err(e) => Ok(format!("❌ Failed to write to file '{}': {}", file_path, e)), Err(e) => Ok(format!("❌ Failed to write to file '{}': {}", file_path, e)),