diff --git a/crates/g3-cli/src/ui_writer_impl.rs b/crates/g3-cli/src/ui_writer_impl.rs index 9827ee3..dc60958 100644 --- a/crates/g3-cli/src/ui_writer_impl.rs +++ b/crates/g3-cli/src/ui_writer_impl.rs @@ -59,18 +59,18 @@ impl UiWriter for ConsoleUiWriter { // Collect arguments instead of printing immediately // Filter out any keys that look like they might be agent message content // (e.g., keys that are suspiciously long or contain message-like content) - let is_valid_arg_key = key.len() < 50 && - !key.contains('\n') && - !key.contains("I'll") && - !key.contains("Let me") && - !key.contains("Here's") && - !key.contains("I can"); - + let is_valid_arg_key = key.len() < 50 + && !key.contains('\n') + && !key.contains("I'll") + && !key.contains("Let me") + && !key.contains("Here's") + && !key.contains("I can"); + if is_valid_arg_key { self.current_tool_args - .lock() - .unwrap() - .push((key.to_string(), value.to_string())); + .lock() + .unwrap() + .push((key.to_string(), value.to_string())); } } @@ -90,14 +90,14 @@ impl UiWriter for ConsoleUiWriter { if let Some((_, value)) = important_arg { // For multi-line values, only show the first line let first_line = value.lines().next().unwrap_or(""); - + // Truncate long values for display let display_value = if first_line.len() > 80 { format!("{}...", &first_line[..77]) } else { first_line.to_string() }; - + // Print with bold green formatting using ANSI escape codes println!("┌─\x1b[1;32m {} | {}\x1b[0m", tool_name, display_value); } else { @@ -110,31 +110,31 @@ impl UiWriter for ConsoleUiWriter { fn update_tool_output_line(&self, line: &str) { let mut current_line = self.current_output_line.lock().unwrap(); let mut line_printed = self.output_line_printed.lock().unwrap(); - + // If we've already printed a line, clear it first if *line_printed { // Move cursor up one line and clear it print!("\x1b[1A\x1b[2K"); } - + // Print the new line println!("│ \x1b[2m{}\x1b[0m", line); let _ = io::stdout().flush(); - + // Update state *current_line = Some(line.to_string()); *line_printed = true; } - + fn print_tool_output_line(&self, line: &str) { println!("│ \x1b[2m{}\x1b[0m", line); } - fn print_tool_output_summary(&self, hidden_count: usize) { + fn print_tool_output_summary(&self, count: usize) { println!( - "│ \x1b[2m... ({} more line{})\x1b[0m", - hidden_count, - if hidden_count == 1 { "" } else { "s" } + "│ \x1b[2m({} line{})\x1b[0m", + count, + if count == 1 { "" } else { "s" } ); } @@ -233,20 +233,20 @@ impl UiWriter for RetroTuiWriter { fn print_tool_arg(&self, key: &str, value: &str) { // Filter out any keys that look like they might be agent message content // (e.g., keys that are suspiciously long or contain message-like content) - let is_valid_arg_key = key.len() < 50 && - !key.contains('\n') && - !key.contains("I'll") && - !key.contains("Let me") && - !key.contains("Here's") && - !key.contains("I can"); - + let is_valid_arg_key = key.len() < 50 + && !key.contains('\n') + && !key.contains("I'll") + && !key.contains("Let me") + && !key.contains("Here's") + && !key.contains("I can"); + if is_valid_arg_key { self.current_tool_output .lock() .unwrap() .push(format!("{}: {}", key, value)); } - + // Build caption from first argument (usually the most important one) let mut caption = self.current_tool_caption.lock().unwrap(); if caption.is_empty() && (key == "file_path" || key == "command" || key == "path") { @@ -279,7 +279,10 @@ impl UiWriter for RetroTuiWriter { fn update_tool_output_line(&self, line: &str) { // For retro mode, we'll just add to the output buffer - self.current_tool_output.lock().unwrap().push(line.to_string()); + self.current_tool_output + .lock() + .unwrap() + .push(line.to_string()); } fn print_tool_output_line(&self, line: &str) { diff --git a/crates/g3-core/src/lib.rs b/crates/g3-core/src/lib.rs index d155880..fbabaf4 100644 --- a/crates/g3-core/src/lib.rs +++ b/crates/g3-core/src/lib.rs @@ -706,7 +706,7 @@ impl Agent { let provider = self.providers.get(None)?; let system_prompt = if provider.has_native_tool_calling() { // For native tool calling providers, use a more explicit system prompt - "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 of the same skill level as a seasoned engineer at a major technology company. You analyze given tasks and write code to achieve 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. @@ -1278,7 +1278,7 @@ The tool will execute immediately and you'll receive the result (success or erro // Add a small delay between iterations to prevent "model busy" errors if iteration_count > 1 { - tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + tokio::time::sleep(tokio::time::Duration::from_millis(50)).await; } let provider = self.providers.get(None)?; @@ -1392,14 +1392,6 @@ The tool will execute immediately and you'll receive the result (success or erro ); } - // Log raw chunk data for debugging - if chunks_received <= 5 || chunk.finished { - debug!( - "Chunk #{}: content={:?}, finished={}, tool_calls={:?}", - chunks_received, chunk.content, chunk.finished, chunk.tool_calls - ); - } - // Process chunk with the new parser let completed_tools = parser.process_chunk(&chunk); @@ -1529,21 +1521,16 @@ The tool will execute immediately and you'll receive the result (success or erro const MAX_LINES: usize = 5; const MAX_LINE_WIDTH: usize = 80; + let output_len = output_lines.len(); - if output_lines.len() <= MAX_LINES { - for line in output_lines { - // Clip line to max width - let clipped_line = truncate_line(line, MAX_LINE_WIDTH); - self.ui_writer.print_tool_output_line(&clipped_line); - } - } else { - for line in output_lines.iter().take(MAX_LINES) { - // Clip line to max width - let clipped_line = truncate_line(line, MAX_LINE_WIDTH); - self.ui_writer.print_tool_output_line(&clipped_line); - } - let hidden_count = output_lines.len() - MAX_LINES; - self.ui_writer.print_tool_output_summary(hidden_count); + for line in output_lines { + // Clip line to max width + let clipped_line = truncate_line(line, MAX_LINE_WIDTH); + self.ui_writer.update_tool_output_line(&clipped_line); + } + + if output_len > MAX_LINES { + self.ui_writer.print_tool_output_summary(output_len); } } @@ -1552,14 +1539,7 @@ The tool will execute immediately and you'll receive the result (success or erro full_response.push_str(final_display_content); if let Some(summary) = tool_call.args.get("summary") { if let Some(summary_str) = summary.as_str() { - // Don't add the "=> " prefix in autonomous mode - // as it interferes with coach feedback parsing - if !self.is_autonomous { - full_response - .push_str(&format!("\n\n=> {}", summary_str)); - } else { - full_response.push_str(&format!("\n\n{}", summary_str)); - } + full_response.push_str(&format!("\n\n{}", summary_str)); } } self.ui_writer.println(""); @@ -1569,7 +1549,7 @@ The tool will execute immediately and you'll receive the result (success or erro // Add timing if needed let final_response = if show_timing { format!( - "{}\n\n⏱️ {} | 💭 {}", + "{}\n\n🕝 {} | 💭 {}", full_response, Self::format_duration(total_execution_time), Self::format_duration(_ttft) @@ -1969,23 +1949,26 @@ The tool will execute immediately and you'll receive the result (success or erro let escaped_command = shell_escape_command(command_str); let executor = CodeExecutor::new(); - + // Create a receiver for streaming output struct ToolOutputReceiver<'a, W: UiWriter> { ui_writer: &'a W, } - + impl<'a, W: UiWriter> g3_execution::OutputReceiver for ToolOutputReceiver<'a, W> { fn on_output_line(&self, line: &str) { self.ui_writer.update_tool_output_line(line); } } - + let receiver = ToolOutputReceiver { ui_writer: &self.ui_writer, }; - - match executor.execute_bash_streaming(&escaped_command, &receiver).await { + + match executor + .execute_bash_streaming(&escaped_command, &receiver) + .await + { Ok(result) => { if result.success { Ok(if result.stdout.is_empty() { @@ -2292,120 +2275,6 @@ The tool will execute immediately and you'll receive the result (success or erro )) } } - "edit_file" => { - debug!("Processing edit_file tool call"); - - // Extract arguments with better error handling - let args_obj = match tool_call.args.as_object() { - Some(obj) => obj, - None => return Ok("❌ Invalid arguments: expected object".to_string()), - }; - - let file_path = match args_obj.get("file_path").and_then(|v| v.as_str()) { - Some(path) => path, - None => return Ok("❌ Missing or invalid file_path argument".to_string()), - }; - - let content = match args_obj.get("content").and_then(|v| v.as_str()) { - Some(c) => c, - None => return Ok("❌ Missing or invalid content argument".to_string()), - }; - - let start_line = match args_obj.get("start_of_range").and_then(|v| v.as_i64()) { - Some(n) if n >= 1 => n as usize, - Some(_) => { - return Ok( - "❌ start_of_range must be >= 1 (lines are 1-indexed)".to_string() - ) - } - None => return Ok("❌ Missing or invalid start_of_range argument".to_string()), - }; - - let end_line = match args_obj.get("end_of_range").and_then(|v| v.as_i64()) { - Some(n) if n >= start_line as i64 => n as usize, - 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()), - }; - - debug!( - "edit_file: path={}, start={}, end={}", - file_path, start_line, end_line - ); - - // Read the existing file - let existing_content = match std::fs::read_to_string(file_path) { - Ok(content) => content, - Err(e) => return Ok(format!("❌ Failed to read file '{}': {}", file_path, e)), - }; - - // Split into lines, preserving empty lines - let mut lines: Vec = - existing_content.lines().map(|s| s.to_string()).collect(); - let original_line_count = 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() - )); - } - } - - // Split the new content into lines - let new_lines: Vec = 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!( - "✅ Successfully edited '{}': replaced {} lines ({}-{}) with {} lines. File now has {} lines (was {} lines)", - 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)), - } - } "str_replace" => { debug!("Processing str_replace tool call"); @@ -2464,10 +2333,10 @@ The tool will execute immediately and you'll receive the result (success or erro if let Some(summary_str) = summary.as_str() { Ok(format!("{}", summary_str)) } else { - Ok("✅ Task completed".to_string()) + Ok("✅ Turn completed".to_string()) } } else { - Ok("✅ Task completed".to_string()) + Ok("✅ Turn completed".to_string()) } } _ => {