Context usage progress bar

This commit is contained in:
Dhanji Prasanna
2025-09-06 14:36:02 +10:00
parent 873d640595
commit 6674eb3df2
4 changed files with 207 additions and 58 deletions

55
Cargo.lock generated
View File

@@ -321,6 +321,19 @@ dependencies = [
"yaml-rust2", "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]] [[package]]
name = "const-random" name = "const-random"
version = "0.1.18" version = "0.1.18"
@@ -488,6 +501,12 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
[[package]]
name = "encode_unicode"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.35" version = "0.8.35"
@@ -685,6 +704,7 @@ dependencies = [
"dirs 5.0.1", "dirs 5.0.1",
"g3-config", "g3-config",
"g3-core", "g3-core",
"indicatif",
"rustyline", "rustyline",
"serde", "serde",
"serde_json", "serde_json",
@@ -1053,6 +1073,19 @@ dependencies = [
"hashbrown 0.15.5", "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]] [[package]]
name = "io-uring" name = "io-uring"
version = "0.7.10" version = "0.7.10"
@@ -1335,6 +1368,12 @@ dependencies = [
"libc", "libc",
] ]
[[package]]
name = "number_prefix"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "830b246a0e5f20af87141b25c173cd1b609bd7779a4617d6ec582abaf90870f3"
[[package]] [[package]]
name = "object" name = "object"
version = "0.36.7" version = "0.36.7"
@@ -1513,6 +1552,12 @@ version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "portable-atomic"
version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
[[package]] [[package]]
name = "potential_utf" name = "potential_utf"
version = "0.1.3" version = "0.1.3"
@@ -2472,6 +2517,16 @@ dependencies = [
"wasm-bindgen", "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]] [[package]]
name = "which" name = "which"
version = "4.4.2" version = "4.4.2"

View File

@@ -17,3 +17,4 @@ serde_json = { workspace = true }
rustyline = "17.0.1" rustyline = "17.0.1"
dirs = "5.0" dirs = "5.0"
tokio-util = "0.7" tokio-util = "0.7"
indicatif = "0.17"

View File

@@ -1,11 +1,12 @@
use clap::Parser;
use g3_core::Agent;
use g3_config::Config;
use anyhow::Result; 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::error::ReadlineError;
use rustyline::DefaultEditor; use rustyline::DefaultEditor;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
use tracing::{error, info};
#[derive(Parser)] #[derive(Parser)]
#[command(name = "g3")] #[command(name = "g3")]
@@ -42,9 +43,7 @@ pub async fn run() -> Result<()> {
tracing::Level::INFO tracing::Level::INFO
}; };
tracing_subscriber::fmt() tracing_subscriber::fmt().with_max_level(level).init();
.with_max_level(level)
.init();
info!("Starting G3 AI Coding Agent"); info!("Starting G3 AI Coding Agent");
@@ -52,13 +51,15 @@ pub async fn run() -> Result<()> {
let config = Config::load(cli.config.as_deref())?; let config = Config::load(cli.config.as_deref())?;
// Initialize agent // Initialize agent
let agent = Agent::new(config).await?; let mut agent = Agent::new(config).await?;
// Execute task or start interactive mode // Execute task or start interactive mode
if let Some(task) = cli.task { if let Some(task) = cli.task {
// Single-shot mode // Single-shot mode
info!("Executing task: {}", task); 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); println!("{}", result);
} else { } else {
// Interactive mode (default) // Interactive mode (default)
@@ -69,9 +70,11 @@ pub async fn run() -> Result<()> {
Ok(()) 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!("🤖 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!();
println!("Type 'exit' or 'quit' to exit, use Up/Down arrows for command history"); 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!("Press ESC during operations to cancel the current request");
@@ -81,8 +84,7 @@ async fn run_interactive(agent: Agent, show_prompt: bool, show_code: bool) -> Re
let mut rl = DefaultEditor::new()?; let mut rl = DefaultEditor::new()?;
// Try to load history from a file in the user's home directory // Try to load history from a file in the user's home directory
let history_file = dirs::home_dir() let history_file = dirs::home_dir().map(|mut path| {
.map(|mut path| {
path.push(".g3_history"); path.push(".g3_history");
path path
}); });
@@ -92,6 +94,9 @@ async fn run_interactive(agent: Agent, show_prompt: bool, show_code: bool) -> Re
} }
loop { loop {
// Display context window progress bar before each prompt
display_context_progress(&agent);
let readline = rl.readline("g3> "); let readline = rl.readline("g3> ");
match readline { match readline {
Ok(line) => { Ok(line) => {
@@ -146,15 +151,15 @@ async fn run_interactive(agent: Agent, show_prompt: bool, show_code: bool) -> Re
} }
} }
} }
}, }
Err(ReadlineError::Interrupted) => { Err(ReadlineError::Interrupted) => {
println!("CTRL-C"); println!("CTRL-C");
continue; continue;
}, }
Err(ReadlineError::Eof) => { Err(ReadlineError::Eof) => {
println!("CTRL-D"); println!("CTRL-D");
break; break;
}, }
Err(err) => { Err(err) => {
error!("Error: {:?}", err); error!("Error: {:?}", err);
break; break;
@@ -170,3 +175,23 @@ async fn run_interactive(agent: Agent, show_prompt: bool, show_code: bool) -> Re
println!("👋 Goodbye!"); println!("👋 Goodbye!");
Ok(()) 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
);
}

View File

@@ -9,9 +9,51 @@ use tracing::field::debug;
use tracing::info; use tracing::info;
use tokio_util::sync::CancellationToken; use tokio_util::sync::CancellationToken;
#[derive(Debug, Clone)]
pub struct ContextWindow {
pub used_tokens: u32,
pub total_tokens: u32,
pub conversation_history: Vec<Message>,
}
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 { pub struct Agent {
providers: ProviderRegistry, providers: ProviderRegistry,
config: Config, config: Config,
context_window: ContextWindow,
} }
impl Agent { impl Agent {
@@ -52,11 +94,18 @@ impl Agent {
// Set default provider // Set default provider
providers.set_default(&config.providers.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( pub async fn execute_task(
&self, &mut self,
description: &str, description: &str,
language: Option<&str>, language: Option<&str>,
_auto_execute: bool, _auto_execute: bool,
@@ -66,7 +115,7 @@ impl Agent {
} }
pub async fn execute_task_with_options( pub async fn execute_task_with_options(
&self, &mut self,
description: &str, description: &str,
language: Option<&str>, language: Option<&str>,
_auto_execute: bool, _auto_execute: bool,
@@ -85,7 +134,7 @@ impl Agent {
} }
pub async fn execute_task_with_timing( pub async fn execute_task_with_timing(
&self, &mut self,
description: &str, description: &str,
language: Option<&str>, language: Option<&str>,
_auto_execute: bool, _auto_execute: bool,
@@ -108,7 +157,7 @@ impl Agent {
} }
pub async fn execute_task_with_timing_cancellable( pub async fn execute_task_with_timing_cancellable(
&self, &mut self,
description: &str, description: &str,
language: Option<&str>, language: Option<&str>,
_auto_execute: bool, _auto_execute: bool,
@@ -161,16 +210,21 @@ with nothing afterwards.",
println!(); println!();
} }
let messages = vec![ // Add system message to context window
Message { let system_message = Message {
role: MessageRole::System, role: MessageRole::System,
content: system_prompt, content: system_prompt.clone(),
}, };
Message { self.context_window.add_message(system_message.clone());
// Add user message to context window
let user_message = Message {
role: MessageRole::User, role: MessageRole::User,
content: format!("Task: {}", description), content: format!("Task: {}", description),
}, };
]; self.context_window.add_message(user_message.clone());
let messages = vec![system_message, user_message];
let request = CompletionRequest { let request = CompletionRequest {
messages, messages,
@@ -189,6 +243,16 @@ with nothing afterwards.",
}; };
let llm_duration = llm_start.elapsed(); 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 // Time the code execution with cancellation support
let exec_start = Instant::now(); let exec_start = Instant::now();
let executor = CodeExecutor::new(); 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 { fn format_duration(duration: Duration) -> String {
let total_ms = duration.as_millis(); let total_ms = duration.as_millis();