edit file fixes

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

View File

@@ -246,16 +246,16 @@ impl ContextWindow {
/// More accurate token estimation /// More accurate token estimation
fn estimate_tokens(text: &str) -> u32 { fn estimate_tokens(text: &str) -> u32 {
// Better heuristic: // Better heuristic:
// - Average English text: ~4 characters per token // - Average English text: ~4 characters per token
// - Code/JSON: ~3 characters per token (more symbols) // - Code/JSON: ~3 characters per token (more symbols)
// - Add 10% buffer for safety // - Add 10% buffer for safety
let base_estimate = if text.contains("{") || text.contains("```") || text.contains("fn ") { let base_estimate = if text.contains("{") || text.contains("```") || text.contains("fn ") {
(text.len() as f32 / 3.0).ceil() as u32 // Code/JSON (text.len() as f32 / 3.0).ceil() as u32 // Code/JSON
} else { } else {
(text.len() as f32 / 4.0).ceil() as u32 // Regular text (text.len() as f32 / 4.0).ceil() as u32 // Regular text
}; };
(base_estimate as f32 * 1.1).ceil() as u32 // Add 10% buffer (base_estimate as f32 * 1.1).ceil() as u32 // Add 10% buffer
} }
pub fn update_usage(&mut self, usage: &g3_providers::Usage) { pub fn update_usage(&mut self, usage: &g3_providers::Usage) {
@@ -275,20 +275,19 @@ 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
// This prevents issues with models that have large contexts but still hit limits // This prevents issues with models that have large contexts but still hit limits
let percentage_trigger = self.percentage_used() >= 80.0; let percentage_trigger = self.percentage_used() >= 80.0;
// Also trigger if we're approaching common token limits // Also trigger if we're approaching common token limits
// Most models start having issues around 150k tokens // Most models start having issues around 150k tokens
let absolute_trigger = self.used_tokens > 150_000; let absolute_trigger = self.used_tokens > 150_000;
percentage_trigger || absolute_trigger percentage_trigger || absolute_trigger
} }
/// Create a summary request prompt for the current conversation /// Create a summary request prompt for the current conversation
pub fn create_summary_prompt(&self) -> String { pub fn create_summary_prompt(&self) -> String {
"Please provide a comprehensive summary of our conversation so far. Include: "Please provide a comprehensive summary of our conversation so far. Include:
@@ -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
@@ -947,42 +947,47 @@ The tool will execute immediately and you'll receive the result (success or erro
// Check if we need to summarize before starting // Check if we need to summarize before starting
if self.context_window.should_summarize() { if self.context_window.should_summarize() {
info!( info!(
"Context window at {}% ({}/{} tokens), triggering auto-summarization", "Context window at {}% ({}/{} tokens), triggering auto-summarization",
self.context_window.percentage_used() as u32, self.context_window.percentage_used() as u32,
self.context_window.used_tokens, self.context_window.used_tokens,
self.context_window.total_tokens self.context_window.total_tokens
); );
// 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<_>>()
.join("\n\n"); .join("\n\n");
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
), ),
}, },
]; ];
let provider = self.providers.get(None)?; let provider = self.providers.get(None)?;
// Dynamically calculate max_tokens for summary based on what's left // Dynamically calculate max_tokens for summary based on what's left
// We need to ensure: used_tokens + max_tokens <= total_context_limit // We need to ensure: used_tokens + max_tokens <= total_context_limit
let summary_max_tokens = match provider.name() { let summary_max_tokens = match provider.name() {
@@ -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))
} }
@@ -1011,10 +1020,12 @@ The tool will execute immediately and you'll receive the result (success or erro
Some(available.min(5000)) Some(available.min(5000))
} }
}; };
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,
max_tokens: summary_max_tokens, max_tokens: summary_max_tokens,
@@ -1603,93 +1614,113 @@ 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();
let file_path = match args_obj.get("file_path").and_then(|v| v.as_str()) {
// Get file_path Some(path) => path,
let file_path = args_obj.get("file_path") None => return Ok("❌ Missing or invalid file_path argument".to_string()),
.and_then(|v| v.as_str()); };
if file_path.is_none() {
return Ok("❌ Missing file_path argument".to_string()); let content = match args_obj.get("content").and_then(|v| v.as_str()) {
} Some(c) => c,
let file_path = file_path.unwrap(); None => return Ok("❌ Missing or invalid content argument".to_string()),
};
// Get content
let content = args_obj.get("content") let start_line = match args_obj.get("start_of_range").and_then(|v| v.as_i64()) {
.and_then(|v| v.as_str()); Some(n) if n >= 1 => n as usize,
if content.is_none() { Some(_) => {
return Ok("❌ Missing content argument".to_string()); return Ok(
} "❌ start_of_range must be >= 1 (lines are 1-indexed)".to_string()
let content = content.unwrap(); )
}
// Get start_of_range None => return Ok("❌ Missing or invalid start_of_range argument".to_string()),
let start_of_range = args_obj.get("start_of_range") };
.and_then(|v| v.as_i64())
.map(|v| v as usize); let end_line = match args_obj.get("end_of_range").and_then(|v| v.as_i64()) {
if start_of_range.is_none() { Some(n) if n >= start_line as i64 => n as usize,
return Ok("Missing or invalid start_of_range argument".to_string()); Some(_) => return Ok("end_of_range must be >= start_of_range".to_string()),
} None => return Ok("❌ Missing or invalid end_of_range argument".to_string()),
let start_of_range = start_of_range.unwrap(); };
// Get end_of_range debug!(
let end_of_range = args_obj.get("end_of_range") "edit_file: path={}, start={}, end={}",
.and_then(|v| v.as_i64()) file_path, start_line, end_line
.map(|v| v as usize); );
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
if start_of_range < 1 {
return Ok("❌ start_of_range must be >= 1 (lines are 1-indexed)".to_string());
}
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) {
Ok(content) => content, Ok(content) => content,
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();
// Check if range is valid let original_line_count = lines.len();
if start_of_range > lines.len() + 1 {
return Ok(format!("❌ start_of_range {} exceeds file length ({} lines)", start_of_range, lines.len())); // Validate the range
if start_line > 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
lines.extend(content.lines().map(|s| s.to_string()));
// Write back to file
let new_content = lines.join("\n");
match std::fs::write(file_path, &new_content) {
Ok(()) => {
let lines_added = content.lines().count();
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()
));
}
} }
// Prepare new content lines // Split the new content into lines
let new_lines: Vec<String> = content.lines().map(|s| s.to_string()).collect(); let new_lines: Vec<String> = content.lines().map(|s| s.to_string()).collect();
// Calculate the actual end of range (capped at file length) // Perform the replacement
let actual_end = end_of_range.min(lines.len()); // 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
// Replace the range with new content let start_idx = start_line - 1;
// Convert to 0-indexed for vector operations let end_idx = (end_line + 1).min(lines.len() + 1); // +1 because splice end is exclusive
let start_idx = start_of_range - 1; let actual_end_line = end_line.min(lines.len()); // For reporting
let end_idx = actual_end; let lines_being_replaced = actual_end_line - start_line + 1;
// Remove old lines and insert new ones debug!(
"Replacing lines {}..={} (0-indexed splice: {}..{})",
start_line, end_line, start_idx, end_idx
);
lines.splice(start_idx..end_idx, new_lines.clone()); lines.splice(start_idx..end_idx, new_lines.clone());
// Write back to file // Write the result back to the 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 = new_lines.len();
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)),