UI writer abstraction instead of printlns everywhere

This commit is contained in:
Dhanji Prasanna
2025-10-02 11:06:14 +10:00
parent e324ddd99d
commit 8c7dd146f8
5 changed files with 200 additions and 49 deletions

13
TODO
View File

@@ -1,11 +1,18 @@
next tasks next tasks
- get something working with autonomous mode x get something working with autonomous mode
- g3d - g3d
x ui abstraction from core
- context token counting bug - context token counting bug
- embedded model
- prompt rewriting
- generates status messages "ruffling feathers..."
- project description?
- treesitter + friends
- error where it just gives up turn - error where it just gives up turn
- "project" behaviors (read readme first) - "project" behaviors (read readme first)
- ui - advance project mgmt
- git - git for reverting
- swarm - swarm
- ui tests / computer controller

View File

@@ -1,7 +1,7 @@
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}; use g3_core::{project::Project, Agent, ui_writer::UiWriter};
use rustyline::error::ReadlineError; use rustyline::error::ReadlineError;
use rustyline::DefaultEditor; use rustyline::DefaultEditor;
use std::path::PathBuf; use std::path::PathBuf;
@@ -9,7 +9,9 @@ use tokio_util::sync::CancellationToken;
use tracing::{error, info}; use tracing::{error, info};
mod tui; mod tui;
mod ui_writer_impl;
use tui::SimpleOutput; use tui::SimpleOutput;
use ui_writer_impl::ConsoleUiWriter;
#[derive(Parser)] #[derive(Parser)]
#[command(name = "g3")] #[command(name = "g3")]
@@ -108,7 +110,8 @@ 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 mut agent = Agent::new(config).await?; let ui_writer = ConsoleUiWriter::new();
let mut agent = Agent::new(config, ui_writer).await?;
// Execute task, autonomous mode, or start interactive mode // Execute task, autonomous mode, or start interactive mode
if cli.autonomous { if cli.autonomous {
@@ -141,7 +144,7 @@ pub async fn run() -> Result<()> {
Ok(()) Ok(())
} }
async fn run_interactive(mut agent: Agent, 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("");
@@ -278,7 +281,7 @@ async fn run_interactive(mut agent: Agent, show_prompt: bool, show_code: bool) -
Ok(()) Ok(())
} }
async fn execute_task(agent: &mut Agent, 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
@@ -337,7 +340,7 @@ async fn execute_task(agent: &mut Agent, input: &str, show_prompt: bool, show_co
} }
} }
fn display_context_progress(agent: &Agent, 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());
} }
@@ -369,7 +372,7 @@ fn setup_workspace_directory() -> Result<PathBuf> {
// Simplified autonomous mode implementation // Simplified autonomous mode implementation
async fn run_autonomous( async fn run_autonomous(
mut agent: Agent, mut agent: Agent<ConsoleUiWriter>,
project: Project, project: Project,
show_prompt: bool, show_prompt: bool,
show_code: bool, show_code: bool,
@@ -443,7 +446,8 @@ async fn run_autonomous(
// Create a new agent instance for coach mode to ensure fresh context // Create a new agent instance for coach mode to ensure fresh context
let config = g3_config::Config::load(None)?; let config = g3_config::Config::load(None)?;
let mut coach_agent = Agent::new(config).await?; let ui_writer = ConsoleUiWriter::new();
let mut coach_agent = Agent::new(config, ui_writer).await?;
// Ensure coach agent is also in the workspace directory // Ensure coach agent is also in the workspace directory
project.enter_workspace()?; project.enter_workspace()?;

View File

@@ -0,0 +1,81 @@
use g3_core::ui_writer::UiWriter;
use std::io::{self, Write};
/// Console implementation of UiWriter that prints to stdout
pub struct ConsoleUiWriter;
impl ConsoleUiWriter {
pub fn new() -> Self {
Self
}
}
impl UiWriter for ConsoleUiWriter {
fn print(&self, message: &str) {
println!("{}", message);
}
fn println(&self, message: &str) {
println!("{}", message);
}
fn print_inline(&self, message: &str) {
print!("{}", message);
let _ = io::stdout().flush();
}
fn print_system_prompt(&self, prompt: &str) {
println!("🔍 System Prompt:");
println!("================");
println!("{}", prompt);
println!("================");
println!();
}
fn print_context_status(&self, message: &str) {
println!("{}", message);
}
fn print_tool_header(&self, tool_name: &str) {
println!("┌─ {}", tool_name);
}
fn print_tool_arg(&self, key: &str, value: &str) {
println!("{}: {}", key, value);
}
fn print_tool_output_header(&self) {
println!("├─ output:");
}
fn print_tool_output_line(&self, line: &str) {
println!("{}", line);
}
fn print_tool_output_summary(&self, hidden_count: usize) {
println!(
"│ ... ({} more line{} hidden)",
hidden_count,
if hidden_count == 1 { "" } else { "s" }
);
}
fn print_tool_timing(&self, duration_str: &str) {
println!("└─ ⚡️ {}", duration_str);
println!();
}
fn print_agent_prompt(&self) {
print!("🤖 ");
let _ = io::stdout().flush();
}
fn print_agent_response(&self, content: &str) {
print!("{}", content);
let _ = io::stdout().flush();
}
fn flush(&self) {
let _ = io::stdout().flush();
}
}

View File

@@ -1,5 +1,7 @@
pub mod error_handling; pub mod error_handling;
pub mod project; pub mod project;
pub mod ui_writer;
use crate::ui_writer::UiWriter;
#[cfg(test)] #[cfg(test)]
mod error_handling_test; mod error_handling_test;
@@ -331,15 +333,16 @@ Format this as a detailed but concise summary that can be used to resume the con
} }
} }
pub struct Agent { pub struct Agent<W: UiWriter> {
providers: ProviderRegistry, providers: ProviderRegistry,
context_window: ContextWindow, context_window: ContextWindow,
session_id: Option<String>, session_id: Option<String>,
tool_call_metrics: Vec<(String, Duration, bool)>, // (tool_name, duration, success) tool_call_metrics: Vec<(String, Duration, bool)>, // (tool_name, duration, success)
ui_writer: W,
} }
impl Agent { impl<W: UiWriter> Agent<W> {
pub async fn new(config: Config) -> Result<Self> { pub async fn new(config: Config, ui_writer: W) -> Result<Self> {
let mut providers = ProviderRegistry::new(); let mut providers = ProviderRegistry::new();
// Only register providers that are configured AND selected as the default provider // Only register providers that are configured AND selected as the default provider
@@ -428,6 +431,7 @@ impl Agent {
context_window, context_window,
session_id: None, session_id: None,
tool_call_metrics: Vec::new(), tool_call_metrics: Vec::new(),
ui_writer,
}) })
} }
@@ -630,7 +634,7 @@ The tool will execute immediately and you'll receive the result (success or erro
- **str_replace**: Replace text in a file using a diff - **str_replace**: Replace text in a file using a diff
- Format: {\"tool\": \"str_replace\", \"args\": {\"file_path\": \"path/to/file\", \"diff\": \"--- old\\n-old text\\n+++ new\\n+new text\"}} - Format: {\"tool\": \"str_replace\", \"args\": {\"file_path\": \"path/to/file\", \"diff\": \"--- old\\n-old text\\n+++ new\\n+new text\"}}
- Example: {\"tool\": \"str_replace\", \"args\": {\"file_path\": \"src/main.rs\", \"diff\": \"--- old\\n-println!(\\\"old\\\");\\n+++ new\\n+println!(\\\"new\\\");\"}} - Example: {\"tool\": \"str_replace\", \"args\": {\"file_path\": \"src/main.rs\", \"diff\": \"--- old\\n-old_code();\\n+++ new\\n+new_code();\"}}
- **final_output**: Signal task completion with a detailed summary of work done in markdown format - **final_output**: Signal task completion with a detailed summary of work done in markdown format
- Format: {\"tool\": \"final_output\", \"args\": {\"summary\": \"what_was_accomplished\"}} - Format: {\"tool\": \"final_output\", \"args\": {\"summary\": \"what_was_accomplished\"}}
@@ -651,11 +655,7 @@ The tool will execute immediately and you'll receive the result (success or erro
}; };
if show_prompt { if show_prompt {
println!("🔍 System Prompt:"); self.ui_writer.print_system_prompt(&system_prompt);
println!("================");
println!("{}", system_prompt);
println!("================");
println!();
} }
// Add system message to context window // Add system message to context window
@@ -1029,7 +1029,6 @@ The tool will execute immediately and you'll receive the result (success or erro
mut request: CompletionRequest, mut request: CompletionRequest,
) -> Result<(String, Duration)> { ) -> Result<(String, Duration)> {
use crate::error_handling::ErrorContext; use crate::error_handling::ErrorContext;
use std::io::{self, Write};
use tokio_stream::StreamExt; use tokio_stream::StreamExt;
debug!("Starting stream_completion_with_tools"); debug!("Starting stream_completion_with_tools");
@@ -1052,10 +1051,10 @@ The tool will execute immediately and you'll receive the result (success or erro
); );
// Notify user about summarization // Notify user about summarization
println!( self.ui_writer.print_context_status(&format!(
"\n📊 Context window reaching capacity ({}%). Creating summary...", "\n📊 Context window reaching capacity ({}%). Creating summary...",
self.context_window.percentage_used() as u32 self.context_window.percentage_used() as u32
); ));
// Create summary request with FULL history // Create summary request with FULL history
let summary_prompt = self.context_window.create_summary_prompt(); let summary_prompt = self.context_window.create_summary_prompt();
@@ -1135,7 +1134,7 @@ The tool will execute immediately and you'll receive the result (success or erro
// Get the summary // Get the summary
match provider.complete(summary_request).await { match provider.complete(summary_request).await {
Ok(summary_response) => { Ok(summary_response) => {
println!("✅ Summary created successfully. Resetting context window...\n"); self.ui_writer.print_context_status("✅ Summary created successfully. Resetting context window...\n");
// Extract the latest user message from the request // Extract the latest user message from the request
let latest_user_msg = request let latest_user_msg = request
@@ -1152,11 +1151,11 @@ The tool will execute immediately and you'll receive the result (success or erro
// Update the request with new context // Update the request with new context
request.messages = self.context_window.conversation_history.clone(); request.messages = self.context_window.conversation_history.clone();
println!("🔄 Context reset complete. Continuing with your request...\n"); self.ui_writer.print_context_status("🔄 Context reset complete. Continuing with your request...\n");
} }
Err(e) => { Err(e) => {
error!("Failed to create summary: {}", e); error!("Failed to create summary: {}", e);
println!("⚠️ Unable to create summary. Consider starting a new session if you continue to see errors.\n"); self.ui_writer.print_context_status("⚠️ Unable to create summary. Consider starting a new session if you continue to see errors.\n");
// Don't continue with the original request if summarization failed // Don't continue with the original request if summarization failed
// as we're likely at token limit // as we're likely at token limit
return Err(anyhow::anyhow!("Context window at capacity and summarization failed. Please start a new session.")); return Err(anyhow::anyhow!("Context window at capacity and summarization failed. Please start a new session."));
@@ -1318,20 +1317,20 @@ The tool will execute immediately and you'll receive the result (success or erro
if !new_content.trim().is_empty() { if !new_content.trim().is_empty() {
if !response_started { if !response_started {
print!("\r🤖 "); self.ui_writer.print_agent_prompt();
response_started = true; response_started = true;
} }
print!("{}", new_content); self.ui_writer.print_agent_response(&new_content);
io::stdout().flush()?; self.ui_writer.flush();
} }
// Execute the tool with formatted output // Execute the tool with formatted output
println!(); // New line before tool execution self.ui_writer.println(""); // New line before tool execution
// Skip printing tool call details for final_output // Skip printing tool call details for final_output
if tool_call.tool != "final_output" { if tool_call.tool != "final_output" {
// Tool call header // Tool call header
println!("┌─ {}", tool_call.tool); self.ui_writer.print_tool_header(&tool_call.tool);
if let Some(args_obj) = tool_call.args.as_object() { if let Some(args_obj) = tool_call.args.as_object() {
for (key, value) in args_obj { for (key, value) in args_obj {
let value_str = match value { let value_str = match value {
@@ -1356,10 +1355,10 @@ The tool will execute immediately and you'll receive the result (success or erro
} }
_ => value.to_string(), _ => value.to_string(),
}; };
println!("{}: {}", key, value_str); self.ui_writer.print_tool_arg(key, &value_str);
} }
} }
println!("├─ output:"); self.ui_writer.print_tool_output_header();
} }
let exec_start = Instant::now(); let exec_start = Instant::now();
@@ -1382,18 +1381,14 @@ The tool will execute immediately and you'll receive the result (success or erro
if output_lines.len() <= MAX_LINES { if output_lines.len() <= MAX_LINES {
for line in output_lines { for line in output_lines {
println!("{}", line); self.ui_writer.print_tool_output_line(line);
} }
} else { } else {
for line in output_lines.iter().take(MAX_LINES) { for line in output_lines.iter().take(MAX_LINES) {
println!("{}", line); self.ui_writer.print_tool_output_line(line);
} }
let hidden_count = output_lines.len() - MAX_LINES; let hidden_count = output_lines.len() - MAX_LINES;
println!( self.ui_writer.print_tool_output_summary(hidden_count);
"│ ... ({} more line{} hidden)",
hidden_count,
if hidden_count == 1 { "" } else { "s" }
);
} }
} }
@@ -1405,7 +1400,7 @@ The tool will execute immediately and you'll receive the result (success or erro
full_response.push_str(&format!("\n\n=> {}", summary_str)); full_response.push_str(&format!("\n\n=> {}", summary_str));
} }
} }
println!(); self.ui_writer.println("");
let ttft = let ttft =
first_token_time.unwrap_or_else(|| stream_start.elapsed()); first_token_time.unwrap_or_else(|| stream_start.elapsed());
return Ok((full_response, ttft)); return Ok((full_response, ttft));
@@ -1413,10 +1408,8 @@ The tool will execute immediately and you'll receive the result (success or erro
// Closure marker with timing // Closure marker with timing
if tool_call.tool != "final_output" { if tool_call.tool != "final_output" {
println!("└─ ⚡️ {}", Self::format_duration(exec_duration)); self.ui_writer.print_tool_timing(&Self::format_duration(exec_duration));
println!(); self.ui_writer.print_agent_prompt();
print!("🤖 ");
io::stdout().flush()?;
} }
// Add the tool call and result to the context window // Add the tool call and result to the context window
@@ -1482,12 +1475,12 @@ The tool will execute immediately and you'll receive the result (success or erro
if !filtered_content.is_empty() { if !filtered_content.is_empty() {
if !response_started { if !response_started {
print!("\r🤖 "); self.ui_writer.print_agent_prompt();
response_started = true; response_started = true;
} }
print!("{}", filtered_content); self.ui_writer.print_agent_response(&filtered_content);
let _ = io::stdout().flush(); self.ui_writer.flush();
current_response.push_str(&filtered_content); current_response.push_str(&filtered_content);
} }
} }
@@ -1599,7 +1592,7 @@ The tool will execute immediately and you'll receive the result (success or erro
full_response.push_str(&current_response); full_response.push_str(&current_response);
} }
println!(); self.ui_writer.println("");
let ttft = let ttft =
first_token_time.unwrap_or_else(|| stream_start.elapsed()); first_token_time.unwrap_or_else(|| stream_start.elapsed());
return Ok((full_response, ttft)); return Ok((full_response, ttft));
@@ -1643,7 +1636,7 @@ The tool will execute immediately and you'll receive the result (success or erro
); );
} else { } else {
full_response.push_str(&current_response); full_response.push_str(&current_response);
println!(); self.ui_writer.println("");
} }
let ttft = first_token_time.unwrap_or_else(|| stream_start.elapsed()); let ttft = first_token_time.unwrap_or_else(|| stream_start.elapsed());

View File

@@ -0,0 +1,66 @@
/// Interface for UI output operations
/// This trait abstracts all UI operations to allow different implementations
/// (console, TUI, web, etc.) without coupling the core logic to specific output methods.
pub trait UiWriter: Send + Sync {
/// Print a simple message
fn print(&self, message: &str);
/// Print a message with a newline
fn println(&self, message: &str);
/// Print without newline (for progress indicators)
fn print_inline(&self, message: &str);
/// Print a system prompt section
fn print_system_prompt(&self, prompt: &str);
/// Print a context window status message
fn print_context_status(&self, message: &str);
/// Print a tool execution header
fn print_tool_header(&self, tool_name: &str);
/// Print a tool argument
fn print_tool_arg(&self, key: &str, value: &str);
/// Print tool output header
fn print_tool_output_header(&self);
/// Print a tool output line
fn print_tool_output_line(&self, line: &str);
/// Print tool output summary (when output is truncated)
fn print_tool_output_summary(&self, hidden_count: usize);
/// Print tool execution timing
fn print_tool_timing(&self, duration_str: &str);
/// Print the agent prompt indicator
fn print_agent_prompt(&self);
/// Print agent response inline (for streaming)
fn print_agent_response(&self, content: &str);
/// Flush any buffered output
fn flush(&self);
}
/// A no-op implementation for when UI output is not needed
pub struct NullUiWriter;
impl UiWriter for NullUiWriter {
fn print(&self, _message: &str) {}
fn println(&self, _message: &str) {}
fn print_inline(&self, _message: &str) {}
fn print_system_prompt(&self, _prompt: &str) {}
fn print_context_status(&self, _message: &str) {}
fn print_tool_header(&self, _tool_name: &str) {}
fn print_tool_arg(&self, _key: &str, _value: &str) {}
fn print_tool_output_header(&self) {}
fn print_tool_output_line(&self, _line: &str) {}
fn print_tool_output_summary(&self, _hidden_count: usize) {}
fn print_tool_timing(&self, _duration_str: &str) {}
fn print_agent_prompt(&self) {}
fn print_agent_response(&self, _content: &str) {}
fn flush(&self) {}
}