This commit is contained in:
Dhanji Prasanna
2025-10-02 15:18:56 +10:00
parent c065532c41
commit dd89067ac1

View File

@@ -1,19 +1,19 @@
use anyhow::Result; use anyhow::Result;
use clap::Parser; use clap::Parser;
use g3_config::Config; use g3_config::Config;
use g3_core::{project::Project, Agent, ui_writer::UiWriter}; use g3_core::{project::Project, ui_writer::UiWriter, Agent};
use rustyline::error::ReadlineError; use rustyline::error::ReadlineError;
use rustyline::DefaultEditor; use rustyline::DefaultEditor;
use std::path::PathBuf; use std::path::PathBuf;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use tracing::{error, info}; use tracing::{error, info};
mod retro_tui;
mod tui; mod tui;
mod ui_writer_impl; mod ui_writer_impl;
mod retro_tui; use retro_tui::RetroTui;
use tui::SimpleOutput; use tui::SimpleOutput;
use ui_writer_impl::{ConsoleUiWriter, RetroTuiWriter}; use ui_writer_impl::{ConsoleUiWriter, RetroTuiWriter};
use retro_tui::RetroTui;
#[derive(Parser)] #[derive(Parser)]
#[command(name = "g3")] #[command(name = "g3")]
@@ -91,14 +91,11 @@ pub async fn run() -> Result<()> {
// In retro mode, we don't want any logging output to interfere with the TUI // In retro mode, we don't want any logging output to interfere with the TUI
// We'll use a no-op subscriber // We'll use a no-op subscriber
use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
// Create a filter that suppresses ALL logs in retro mode // Create a filter that suppresses ALL logs in retro mode
let filter = EnvFilter::from_default_env() let filter = EnvFilter::from_default_env().add_directive("off".parse().unwrap()); // Turn off all logging
.add_directive("off".parse().unwrap()); // Turn off all logging
tracing_subscriber::registry().with(filter).init();
tracing_subscriber::registry()
.with(filter)
.init();
} }
if !cli.retro { if !cli.retro {
@@ -167,7 +164,7 @@ pub async fn run() -> Result<()> {
if !cli.retro { if !cli.retro {
info!("Starting interactive mode"); info!("Starting interactive mode");
} }
if cli.retro { if cli.retro {
// Use retro terminal UI // Use retro terminal UI
run_interactive_retro(config, cli.show_prompt, cli.show_code).await?; run_interactive_retro(config, cli.show_prompt, cli.show_code).await?;
@@ -185,22 +182,22 @@ pub async fn run() -> Result<()> {
async fn run_interactive_retro(config: Config, show_prompt: bool, show_code: bool) -> Result<()> { async fn run_interactive_retro(config: Config, show_prompt: bool, show_code: bool) -> Result<()> {
use crossterm::event::{self, Event, KeyCode, KeyModifiers}; use crossterm::event::{self, Event, KeyCode, KeyModifiers};
use std::time::Duration; use std::time::Duration;
// Set environment variable to suppress println in other crates // Set environment variable to suppress println in other crates
std::env::set_var("G3_RETRO_MODE", "1"); std::env::set_var("G3_RETRO_MODE", "1");
// Initialize the retro terminal UI // Initialize the retro terminal UI
let tui = RetroTui::start().await?; let tui = RetroTui::start().await?;
// Create agent with RetroTuiWriter // Create agent with RetroTuiWriter
let ui_writer = RetroTuiWriter::new(tui.clone()); let ui_writer = RetroTuiWriter::new(tui.clone());
let mut agent = Agent::new(config, ui_writer).await?; let mut agent = Agent::new(config, ui_writer).await?;
// Display initial system messages // Display initial system messages
tui.output("SYSTEM: G3 AI CODING AGENT ONLINE"); tui.output("SYSTEM: G3 AI CODING AGENT ONLINE");
tui.output("SYSTEM: READY FOR INPUT"); tui.output("SYSTEM: READY FOR INPUT");
tui.output(""); tui.output("");
// Display provider and model information // Display provider and model information
match agent.get_provider_info() { match agent.get_provider_info() {
Ok((provider, model)) => { Ok((provider, model)) => {
@@ -210,26 +207,28 @@ async fn run_interactive_retro(config: Config, show_prompt: bool, show_code: boo
tui.update_provider_info("ERROR", &e.to_string()); tui.update_provider_info("ERROR", &e.to_string());
} }
} }
tui.output(""); tui.output("");
tui.output("Type 'exit' or 'quit' to exit, use Up/Down arrows to scroll output");
tui.output("For multiline input: use \\ at the end of a line to continue");
tui.output(""); tui.output("");
// Track multiline input // Track multiline input
let mut multiline_buffer = String::new(); let mut multiline_buffer = String::new();
let mut in_multiline = false; let mut in_multiline = false;
let mut input_buffer = String::new(); let mut input_buffer = String::new();
// Main event loop // Main event loop
loop { loop {
// Update context window display // Update context window display
let context = agent.get_context_window(); let context = agent.get_context_window();
tui.update_context(context.used_tokens, context.total_tokens, context.percentage_used()); tui.update_context(
context.used_tokens,
context.total_tokens,
context.percentage_used(),
);
// Update the displayed input buffer // Update the displayed input buffer
tui.update_input(&input_buffer); tui.update_input(&input_buffer);
// Poll for keyboard events // Poll for keyboard events
if event::poll(Duration::from_millis(50))? { if event::poll(Duration::from_millis(50))? {
if let Event::Key(key) = event::read()? { if let Event::Key(key) = event::read()? {
@@ -245,7 +244,7 @@ async fn run_interactive_retro(config: Config, show_prompt: bool, show_code: boo
KeyCode::Enter => { KeyCode::Enter => {
if !input_buffer.is_empty() { if !input_buffer.is_empty() {
let trimmed = input_buffer.trim_end(); let trimmed = input_buffer.trim_end();
// Check if line ends with backslash for continuation // Check if line ends with backslash for continuation
if trimmed.ends_with('\\') { if trimmed.ends_with('\\') {
// Remove the backslash and add to buffer // Remove the backslash and add to buffer
@@ -257,7 +256,7 @@ async fn run_interactive_retro(config: Config, show_prompt: bool, show_code: boo
tui.status("MULTILINE INPUT"); tui.status("MULTILINE INPUT");
continue; continue;
} }
// If we're in multiline mode and no backslash, this is the final line // If we're in multiline mode and no backslash, this is the final line
let final_input = if in_multiline { let final_input = if in_multiline {
multiline_buffer.push_str(&input_buffer); multiline_buffer.push_str(&input_buffer);
@@ -269,24 +268,34 @@ async fn run_interactive_retro(config: Config, show_prompt: bool, show_code: boo
} else { } else {
input_buffer.clone() input_buffer.clone()
}; };
input_buffer.clear(); input_buffer.clear();
let input = final_input.trim().to_string(); let input = final_input.trim().to_string();
if input.is_empty() { if input.is_empty() {
continue; continue;
} }
if input == "exit" || input == "quit" { if input == "exit" || input == "quit" {
tui.exit(); tui.exit();
break; break;
} }
// Execute the task // Execute the task
tui.output(&format!("> {}", input)); tui.output(&format!("> {}", input));
tui.status("PROCESSING"); tui.status("PROCESSING");
match agent.execute_task_with_timing(&input, None, false, show_prompt, show_code, true).await { match agent
.execute_task_with_timing(
&input,
None,
false,
show_prompt,
show_code,
true,
)
.await
{
Ok(response) => { Ok(response) => {
tui.output(&response); tui.output(&response);
tui.status("READY"); tui.status("READY");
@@ -326,22 +335,26 @@ async fn run_interactive_retro(config: Config, show_prompt: bool, show_code: boo
} }
} }
} }
// Small delay to prevent CPU spinning // Small delay to prevent CPU spinning
tokio::time::sleep(Duration::from_millis(10)).await; tokio::time::sleep(Duration::from_millis(10)).await;
} }
tui.output("SYSTEM: SHUTDOWN INITIATED"); tui.output("SYSTEM: SHUTDOWN INITIATED");
Ok(()) Ok(())
} }
async fn run_interactive<W: UiWriter>(mut agent: Agent<W>, show_prompt: bool, show_code: bool) -> Result<()> { async fn run_interactive<W: UiWriter>(
mut agent: Agent<W>,
show_prompt: bool,
show_code: bool,
) -> Result<()> {
let output = SimpleOutput::new(); let output = SimpleOutput::new();
output.print(""); output.print("");
output.print("🤖 G3 AI Coding Agent - Interactive Mode"); output.print("🤖 G3 AI Coding Agent - Interactive Mode");
output.print( output.print(
"I solve problems by writing and executing code. Tell me what you need to accomplish!" "I solve problems by writing and executing code. Tell me what you need to accomplish!",
); );
output.print(""); output.print("");
@@ -389,7 +402,7 @@ async fn run_interactive<W: UiWriter>(mut agent: Agent<W>, show_prompt: bool, sh
match readline { match readline {
Ok(line) => { Ok(line) => {
let trimmed = line.trim_end(); let trimmed = line.trim_end();
// Check if line ends with backslash for continuation // Check if line ends with backslash for continuation
if trimmed.ends_with('\\') { if trimmed.ends_with('\\') {
// Remove the backslash and add to buffer // Remove the backslash and add to buffer
@@ -399,7 +412,7 @@ async fn run_interactive<W: UiWriter>(mut agent: Agent<W>, show_prompt: bool, sh
in_multiline = true; in_multiline = true;
continue; continue;
} }
// If we're in multiline mode and no backslash, this is the final line // If we're in multiline mode and no backslash, this is the final line
if in_multiline { if in_multiline {
multiline_buffer.push_str(&line); multiline_buffer.push_str(&line);
@@ -407,35 +420,35 @@ async fn run_interactive<W: UiWriter>(mut agent: Agent<W>, show_prompt: bool, sh
// Process the complete multiline input // Process the complete multiline input
let input = multiline_buffer.trim().to_string(); let input = multiline_buffer.trim().to_string();
multiline_buffer.clear(); multiline_buffer.clear();
if input.is_empty() { if input.is_empty() {
continue; continue;
} }
// Add complete multiline to history // Add complete multiline to history
rl.add_history_entry(&input)?; rl.add_history_entry(&input)?;
if input == "exit" || input == "quit" { if input == "exit" || input == "quit" {
break; break;
} }
// Process the multiline input // Process the multiline input
execute_task(&mut agent, &input, show_prompt, show_code, &output).await; execute_task(&mut agent, &input, show_prompt, show_code, &output).await;
} else { } else {
// Single line input // Single line input
let input = line.trim().to_string(); let input = line.trim().to_string();
if input.is_empty() { if input.is_empty() {
continue; continue;
} }
if input == "exit" || input == "quit" { if input == "exit" || input == "quit" {
break; break;
} }
// Add to history // Add to history
rl.add_history_entry(&input)?; rl.add_history_entry(&input)?;
// Process the single line input // Process the single line input
execute_task(&mut agent, &input, show_prompt, show_code, &output).await; execute_task(&mut agent, &input, show_prompt, show_code, &output).await;
} }
@@ -472,7 +485,13 @@ async fn run_interactive<W: UiWriter>(mut agent: Agent<W>, show_prompt: bool, sh
Ok(()) Ok(())
} }
async fn execute_task<W: UiWriter>(agent: &mut Agent<W>, input: &str, show_prompt: bool, show_code: bool, output: &SimpleOutput) { async fn execute_task<W: UiWriter>(
agent: &mut Agent<W>,
input: &str,
show_prompt: bool,
show_code: bool,
output: &SimpleOutput,
) {
// Show thinking indicator immediately // Show thinking indicator immediately
output.print("🤔 Thinking..."); output.print("🤔 Thinking...");
// Note: flush is handled internally by println // Note: flush is handled internally by println
@@ -504,7 +523,7 @@ async fn execute_task<W: UiWriter>(agent: &mut Agent<W>, input: &str, show_promp
// Enhanced error logging with detailed information // Enhanced error logging with detailed information
error!("=== TASK EXECUTION ERROR ==="); error!("=== TASK EXECUTION ERROR ===");
error!("Error: {}", e); error!("Error: {}", e);
// Log error chain // Log error chain
let mut source = e.source(); let mut source = e.source();
let mut depth = 1; let mut depth = 1;
@@ -513,14 +532,14 @@ async fn execute_task<W: UiWriter>(agent: &mut Agent<W>, input: &str, show_promp
source = err.source(); source = err.source();
depth += 1; depth += 1;
} }
// Log additional context // Log additional context
error!("Task input: {}", input); error!("Task input: {}", input);
error!("Error type: {}", std::any::type_name_of_val(&e)); error!("Error type: {}", std::any::type_name_of_val(&e));
// Display user-friendly error message // Display user-friendly error message
output.print(&format!("❌ Error: {}", e)); output.print(&format!("❌ Error: {}", e));
// If it's a stream error, provide helpful guidance // If it's a stream error, provide helpful guidance
if e.to_string().contains("No response received") { if e.to_string().contains("No response received") {
output.print("💡 This may be a temporary issue. Please try again or check the logs for more details."); output.print("💡 This may be a temporary issue. Please try again or check the logs for more details.");
@@ -533,7 +552,11 @@ async fn execute_task<W: UiWriter>(agent: &mut Agent<W>, input: &str, show_promp
fn display_context_progress<W: UiWriter>(agent: &Agent<W>, output: &SimpleOutput) { fn display_context_progress<W: UiWriter>(agent: &Agent<W>, output: &SimpleOutput) {
let context = agent.get_context_window(); let context = agent.get_context_window();
output.print_context(context.used_tokens, context.total_tokens, context.percentage_used()); output.print_context(
context.used_tokens,
context.total_tokens,
context.percentage_used(),
);
} }
/// Set up the workspace directory for autonomous mode /// Set up the workspace directory for autonomous mode
@@ -570,15 +593,21 @@ async fn run_autonomous(
max_turns: usize, max_turns: usize,
) -> Result<()> { ) -> Result<()> {
let output = SimpleOutput::new(); let output = SimpleOutput::new();
output.print("🤖 G3 AI Coding Agent - Autonomous Mode"); output.print("🤖 G3 AI Coding Agent - Autonomous Mode");
output.print(&format!("📁 Using workspace: {}", project.workspace().display())); output.print(&format!(
"📁 Using workspace: {}",
project.workspace().display()
));
// Check if requirements exist // Check if requirements exist
if !project.has_requirements() { if !project.has_requirements() {
output.print("❌ Error: requirements.md not found in workspace directory"); output.print("❌ Error: requirements.md not found in workspace directory");
output.print(" Please create a requirements.md file with your project requirements at:"); output.print(" Please create a requirements.md file with your project requirements at:");
output.print(&format!(" {}/requirements.md", project.workspace().display())); output.print(&format!(
" {}/requirements.md",
project.workspace().display()
));
return Ok(()); return Ok(());
} }
@@ -593,13 +622,16 @@ async fn run_autonomous(
output.print("📋 Requirements loaded from requirements.md"); output.print("📋 Requirements loaded from requirements.md");
output.print("🔄 Starting coach-player feedback loop..."); output.print("🔄 Starting coach-player feedback loop...");
let mut turn = 1; let mut turn = 1;
let mut coach_feedback = String::new(); let mut coach_feedback = String::new();
let mut implementation_approved = false; let mut implementation_approved = false;
loop { loop {
output.print(&format!("\n=== TURN {}/{} - PLAYER MODE ===", turn, max_turns)); output.print(&format!(
"\n=== 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() {
@@ -615,7 +647,7 @@ async fn run_autonomous(
}; };
output.print("🎯 Starting player implementation..."); output.print("🎯 Starting player implementation...");
// Execute player task and handle the result properly // Execute player task and handle the result properly
match agent match 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)
@@ -631,7 +663,7 @@ async fn run_autonomous(
// Continue to coach review even if player had an error // Continue to coach review even if player had an error
} }
} }
// Give some time for file operations to complete // Give some time for file operations to complete
tokio::time::sleep(tokio::time::Duration::from_millis(500)).await; tokio::time::sleep(tokio::time::Duration::from_millis(500)).await;
@@ -643,7 +675,10 @@ async fn run_autonomous(
// Ensure coach agent is also in the workspace directory // Ensure coach agent is also in the workspace directory
project.enter_workspace()?; project.enter_workspace()?;
output.print(&format!("\n=== TURN {}/{} - COACH MODE ===", turn, max_turns)); output.print(&format!(
"\n=== TURN {}/{} - COACH MODE ===",
turn, max_turns
));
// Coach mode: critique the implementation // Coach mode: critique the implementation
let coach_prompt = format!( let coach_prompt = format!(
@@ -704,4 +739,4 @@ Keep your response concise and focused on actionable items.",
} }
Ok(()) Ok(())
} }