diff --git a/Cargo.lock b/Cargo.lock index 915e6ea..8cec021 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -321,6 +321,19 @@ dependencies = [ "yaml-rust2", ] +[[package]] +name = "console" +version = "0.15.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "054ccb5b10f9f2cbf51eb355ca1d05c2d279ce1804688d0db74b4733a5aeafd8" +dependencies = [ + "encode_unicode", + "libc", + "once_cell", + "unicode-width", + "windows-sys 0.59.0", +] + [[package]] name = "const-random" version = "0.1.18" @@ -488,6 +501,12 @@ version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" +[[package]] +name = "encode_unicode" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0" + [[package]] name = "encoding_rs" version = "0.8.35" @@ -685,6 +704,7 @@ dependencies = [ "dirs 5.0.1", "g3-config", "g3-core", + "indicatif", "rustyline", "serde", "serde_json", @@ -1053,6 +1073,19 @@ dependencies = [ "hashbrown 0.15.5", ] +[[package]] +name = "indicatif" +version = "0.17.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "183b3088984b400f4cfac3620d5e076c84da5364016b4f49473de574b2586235" +dependencies = [ + "console", + "number_prefix", + "portable-atomic", + "unicode-width", + "web-time", +] + [[package]] name = "io-uring" version = "0.7.10" @@ -1335,6 +1368,12 @@ dependencies = [ "libc", ] +[[package]] +name = "number_prefix" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3" + [[package]] name = "object" version = "0.36.7" @@ -1513,6 +1552,12 @@ version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" +[[package]] +name = "portable-atomic" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483" + [[package]] name = "potential_utf" version = "0.1.3" @@ -2472,6 +2517,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "which" version = "4.4.2" diff --git a/crates/g3-cli/Cargo.toml b/crates/g3-cli/Cargo.toml index 37be2d9..818f0cd 100644 --- a/crates/g3-cli/Cargo.toml +++ b/crates/g3-cli/Cargo.toml @@ -17,3 +17,4 @@ serde_json = { workspace = true } rustyline = "17.0.1" dirs = "5.0" tokio-util = "0.7" +indicatif = "0.17" diff --git a/crates/g3-cli/src/lib.rs b/crates/g3-cli/src/lib.rs index b590b8a..debeea6 100644 --- a/crates/g3-cli/src/lib.rs +++ b/crates/g3-cli/src/lib.rs @@ -1,11 +1,12 @@ -use clap::Parser; -use g3_core::Agent; -use g3_config::Config; use anyhow::Result; -use tracing::{info, error}; +use clap::Parser; +use g3_config::Config; +use g3_core::Agent; +use indicatif::{ProgressBar, ProgressStyle}; use rustyline::error::ReadlineError; use rustyline::DefaultEditor; use tokio_util::sync::CancellationToken; +use tracing::{error, info}; #[derive(Parser)] #[command(name = "g3")] @@ -15,103 +16,107 @@ pub struct Cli { /// Enable verbose logging #[arg(short, long)] pub verbose: bool, - + /// Show the system prompt being sent to the LLM #[arg(long)] pub show_prompt: bool, - + /// Show the generated code before execution #[arg(long)] pub show_code: bool, - + /// Configuration file path #[arg(short, long)] pub config: Option, - + /// Task to execute (if provided, runs in single-shot mode instead of interactive) pub task: Option, } pub async fn run() -> Result<()> { let cli = Cli::parse(); - + // Initialize logging let level = if cli.verbose { tracing::Level::DEBUG } else { tracing::Level::INFO }; - - tracing_subscriber::fmt() - .with_max_level(level) - .init(); - + + tracing_subscriber::fmt().with_max_level(level).init(); + info!("Starting G3 AI Coding Agent"); - + // Load configuration let config = Config::load(cli.config.as_deref())?; - + // Initialize agent - let agent = Agent::new(config).await?; - + let mut agent = Agent::new(config).await?; + // Execute task or start interactive mode if let Some(task) = cli.task { // Single-shot mode info!("Executing task: {}", task); - let result = agent.execute_task_with_timing(&task, None, false, cli.show_prompt, cli.show_code, true).await?; + let result = agent + .execute_task_with_timing(&task, None, false, cli.show_prompt, cli.show_code, true) + .await?; println!("{}", result); } else { // Interactive mode (default) info!("Starting interactive mode"); run_interactive(agent, cli.show_prompt, cli.show_code).await?; } - + Ok(()) } -async fn run_interactive(agent: Agent, show_prompt: bool, show_code: bool) -> Result<()> { +async fn run_interactive(mut agent: Agent, show_prompt: bool, show_code: bool) -> Result<()> { println!("🤖 G3 AI Coding Agent - Interactive Mode"); - println!("I solve problems by writing and executing code. Tell me what you need to accomplish!"); + println!( + "I solve problems by writing and executing code. Tell me what you need to accomplish!" + ); println!(); println!("Type 'exit' or 'quit' to exit, use Up/Down arrows for command history"); println!("Press ESC during operations to cancel the current request"); println!(); - + // Initialize rustyline editor with history let mut rl = DefaultEditor::new()?; - + // Try to load history from a file in the user's home directory - let history_file = dirs::home_dir() - .map(|mut path| { - path.push(".g3_history"); - path - }); - + let history_file = dirs::home_dir().map(|mut path| { + path.push(".g3_history"); + path + }); + if let Some(ref history_path) = history_file { let _ = rl.load_history(history_path); } - + loop { + // Display context window progress bar before each prompt + display_context_progress(&agent); + let readline = rl.readline("g3> "); match readline { Ok(line) => { let input = line.trim(); - + if input == "exit" || input == "quit" { break; } - + if input.is_empty() { continue; } - + // Add to history rl.add_history_entry(input)?; - + // Create cancellation token for this request let cancellation_token = CancellationToken::new(); let cancel_token_clone = cancellation_token.clone(); - + // Spawn a task to monitor for ESC key during execution let esc_monitor = tokio::spawn(async move { // This is a simplified approach - in a real implementation, @@ -119,7 +124,7 @@ async fn run_interactive(agent: Agent, show_prompt: bool, show_code: bool) -> Re // For now, we'll just provide the cancellation infrastructure tokio::time::sleep(tokio::time::Duration::from_secs(3600)).await; }); - + // Execute task with cancellation support let execution_result = tokio::select! { result = agent.execute_task_with_timing_cancellable( @@ -135,7 +140,7 @@ async fn run_interactive(agent: Agent, show_prompt: bool, show_code: bool) -> Re continue; } }; - + match execution_result { Ok(response) => println!("{}", response), Err(e) => { @@ -146,27 +151,47 @@ async fn run_interactive(agent: Agent, show_prompt: bool, show_code: bool) -> Re } } } - }, + } Err(ReadlineError::Interrupted) => { println!("CTRL-C"); continue; - }, + } Err(ReadlineError::Eof) => { println!("CTRL-D"); break; - }, + } Err(err) => { error!("Error: {:?}", err); break; } } } - + // Save history before exiting if let Some(ref history_path) = history_file { let _ = rl.save_history(history_path); } - + println!("👋 Goodbye!"); Ok(()) } + +fn display_context_progress(agent: &Agent) { + let context = agent.get_context_window(); + let percentage = context.percentage_used(); + + // Create a simple visual progress bar using the requested characters (10 dots max) + let bar_width = 10; + let filled_width = ((percentage / 100.0) * bar_width as f32) as usize; + let empty_width = bar_width - filled_width; + + let filled_chars = "●".repeat(filled_width); + let empty_chars = "○".repeat(empty_width); + let progress_bar = format!("{}{}", filled_chars, empty_chars); + + // Print context info with visual progress bar + println!( + "Context: {} {:.1}% | {}/{} tokens", + progress_bar, percentage, context.used_tokens, context.total_tokens + ); +} diff --git a/crates/g3-core/src/lib.rs b/crates/g3-core/src/lib.rs index 876eed1..9257220 100644 --- a/crates/g3-core/src/lib.rs +++ b/crates/g3-core/src/lib.rs @@ -9,9 +9,51 @@ use tracing::field::debug; use tracing::info; use tokio_util::sync::CancellationToken; +#[derive(Debug, Clone)] +pub struct ContextWindow { + pub used_tokens: u32, + pub total_tokens: u32, + pub conversation_history: Vec, +} + +impl ContextWindow { + pub fn new(total_tokens: u32) -> Self { + Self { + used_tokens: 0, + total_tokens, + conversation_history: Vec::new(), + } + } + + pub fn add_message(&mut self, message: Message) { + // Simple token estimation: ~4 characters per token + let estimated_tokens = (message.content.len() as f32 / 4.0).ceil() as u32; + self.used_tokens += estimated_tokens; + self.conversation_history.push(message); + } + + pub fn update_usage(&mut self, usage: &g3_providers::Usage) { + // Update with actual token usage from the provider + self.used_tokens = usage.total_tokens; + } + + pub fn percentage_used(&self) -> f32 { + if self.total_tokens == 0 { + 0.0 + } else { + (self.used_tokens as f32 / self.total_tokens as f32) * 100.0 + } + } + + pub fn remaining_tokens(&self) -> u32 { + self.total_tokens.saturating_sub(self.used_tokens) + } +} + pub struct Agent { providers: ProviderRegistry, config: Config, + context_window: ContextWindow, } impl Agent { @@ -52,11 +94,18 @@ impl Agent { // Set default provider providers.set_default(&config.providers.default_provider)?; - Ok(Self { providers, config }) + // Initialize context window with configured max context length + let context_window = ContextWindow::new(config.agent.max_context_length as u32); + + Ok(Self { + providers, + config, + context_window, + }) } pub async fn execute_task( - &self, + &mut self, description: &str, language: Option<&str>, _auto_execute: bool, @@ -66,7 +115,7 @@ impl Agent { } pub async fn execute_task_with_options( - &self, + &mut self, description: &str, language: Option<&str>, _auto_execute: bool, @@ -85,7 +134,7 @@ impl Agent { } pub async fn execute_task_with_timing( - &self, + &mut self, description: &str, language: Option<&str>, _auto_execute: bool, @@ -108,7 +157,7 @@ impl Agent { } pub async fn execute_task_with_timing_cancellable( - &self, + &mut self, description: &str, language: Option<&str>, _auto_execute: bool, @@ -161,16 +210,21 @@ with nothing afterwards.", println!(); } - let messages = vec![ - Message { - role: MessageRole::System, - content: system_prompt, - }, - Message { - role: MessageRole::User, - content: format!("Task: {}", description), - }, - ]; + // Add system message to context window + let system_message = Message { + role: MessageRole::System, + content: system_prompt.clone(), + }; + self.context_window.add_message(system_message.clone()); + + // Add user message to context window + let user_message = Message { + role: MessageRole::User, + content: format!("Task: {}", description), + }; + self.context_window.add_message(user_message.clone()); + + let messages = vec![system_message, user_message]; let request = CompletionRequest { messages, @@ -189,6 +243,16 @@ with nothing afterwards.", }; let llm_duration = llm_start.elapsed(); + // Update context window with actual token usage + self.context_window.update_usage(&response.usage); + + // Add assistant response to context window + let assistant_message = Message { + role: MessageRole::Assistant, + content: response.content.clone(), + }; + self.context_window.add_message(assistant_message); + // Time the code execution with cancellation support let exec_start = Instant::now(); let executor = CodeExecutor::new(); @@ -215,6 +279,10 @@ with nothing afterwards.", } } + pub fn get_context_window(&self) -> &ContextWindow { + &self.context_window + } + fn format_duration(duration: Duration) -> String { let total_ms = duration.as_millis();