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",
]
[[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"

View File

@@ -17,3 +17,4 @@ serde_json = { workspace = true }
rustyline = "17.0.1"
dirs = "5.0"
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 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<String>,
/// Task to execute (if provided, runs in single-shot mode instead of interactive)
pub task: Option<String>,
}
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
);
}

View File

@@ -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<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 {
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();