Refine planner mode UI and error handling

Improve planner mode user experience with better error reporting,
cleaner tool output, and consistent log file placement.

- Propagate and display classified LLM errors to users with
  appropriate icons and context
- Display tool calls on single lines with truncated arguments
- Show LLM text responses without overwriting via UiWriter
- Ensure all logs write to workspace/logs directory consistently
- Set G3_WORKSPACE_PATH early in planning mode initialization
This commit is contained in:
Jochen
2025-12-09 22:44:00 +11:00
parent a9dbe5f7d3
commit 75aa2d983e
10 changed files with 473 additions and 37 deletions

View File

@@ -40,7 +40,7 @@ impl UiWriter for MachineUiWriter {
println!("CONTEXT_THINNING: {}", message);
}
fn print_tool_header(&self, tool_name: &str) {
fn print_tool_header(&self, tool_name: &str, _tool_args: Option<&serde_json::Value>) {
println!("TOOL_CALL: {}", tool_name);
}

View File

@@ -78,7 +78,7 @@ impl UiWriter for ConsoleUiWriter {
let _ = io::stdout().flush();
}
fn print_tool_header(&self, tool_name: &str) {
fn print_tool_header(&self, tool_name: &str, _tool_args: Option<&serde_json::Value>) {
// Store the tool name and clear args for collection
*self.current_tool_name.lock().unwrap() = Some(tool_name.to_string());
self.current_tool_args.lock().unwrap().clear();

View File

@@ -4011,7 +4011,7 @@ impl<W: UiWriter> Agent<W> {
// Skip printing tool call details for final_output
if tool_call.tool != "final_output" {
// Tool call header
self.ui_writer.print_tool_header(&tool_call.tool);
self.ui_writer.print_tool_header(&tool_call.tool, Some(&tool_call.args));
if let Some(args_obj) = tool_call.args.as_object() {
for (key, value) in args_obj {
let value_str = match value {

View File

@@ -21,7 +21,7 @@ pub trait UiWriter: Send + Sync {
fn print_context_thinning(&self, message: &str);
/// Print a tool execution header
fn print_tool_header(&self, tool_name: &str);
fn print_tool_header(&self, tool_name: &str, tool_args: Option<&serde_json::Value>);
/// Print a tool argument
fn print_tool_arg(&self, key: &str, value: &str);
@@ -81,7 +81,7 @@ impl UiWriter for NullUiWriter {
fn print_system_prompt(&self, _prompt: &str) {}
fn print_context_status(&self, _message: &str) {}
fn print_context_thinning(&self, _message: &str) {}
fn print_tool_header(&self, _tool_name: &str) {}
fn print_tool_header(&self, _tool_name: &str, _tool_args: Option<&serde_json::Value>) {}
fn print_tool_arg(&self, _key: &str, _value: &str) {}
fn print_tool_output_header(&self) {}
fn update_tool_output_line(&self, _line: &str) {}

View File

@@ -53,7 +53,7 @@ impl UiWriter for MockUiWriter {
.push(format!("STATUS: {}", message));
}
fn print_context_thinning(&self, _message: &str) {}
fn print_tool_header(&self, _tool_name: &str) {}
fn print_tool_header(&self, _tool_name: &str, _tool_args: Option<&serde_json::Value>) {}
fn print_tool_arg(&self, _key: &str, _value: &str) {}
fn print_tool_output_header(&self) {}
fn update_tool_output_line(&self, _line: &str) {}

View File

@@ -9,6 +9,7 @@ use anyhow::{anyhow, Context, Result};
use g3_config::Config;
use g3_core::project::Project;
use g3_core::Agent;
use g3_core::error_handling::{classify_error, ErrorType};
use g3_providers::{CompletionRequest, LLMProvider, Message, MessageRole};
use crate::prompts;
@@ -205,10 +206,9 @@ impl PlannerUiWriter {
/// Clear the current line and print a status message
fn print_status_line(&self, message: &str) {
use std::io::Write;
// Use carriage return to overwrite previous line, pad to 80 chars to clear old content
print!("\r{:<80}", message);
std::io::stdout().flush().ok();
// Print status message without overwriting previous content
// Use println to ensure each status is on its own line
println!("{:.80}", message);
}
}
@@ -235,12 +235,32 @@ impl g3_core::ui_writer::UiWriter for PlannerUiWriter {
println!("🗜️ {}", message);
}
fn print_tool_header(&self, tool_name: &str) {
fn print_tool_header(&self, tool_name: &str, tool_args: Option<&serde_json::Value>) {
// Increment tool count and show on single line
let count = self.tool_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst) + 1;
// Clear the "Thinking..." line and print tool header on new line
print!("\r{:<80}\n", ""); // Clear status line
println!("🔧 [{}] {}", count, tool_name);
// Format args for display (first 50 chars)
let args_display = if let Some(args) = tool_args {
let args_str = serde_json::to_string(args).unwrap_or_else(|_| "{}".to_string());
if args_str.len() > 50 {
// Truncate at char boundary
let truncated: String = args_str.chars().take(50).collect();
format!("{}", truncated)
} else {
args_str
}
} else {
String::new()
};
// Print on single line with args
use std::io::Write;
if args_display.is_empty() {
println!("🔧 [{}] {}", count, tool_name);
} else {
println!("🔧 [{}] {} {}", count, tool_name, args_display);
}
std::io::stdout().flush().ok();
}
fn print_tool_arg(&self, _key: &str, _value: &str) {}
@@ -258,15 +278,13 @@ impl g3_core::ui_writer::UiWriter for PlannerUiWriter {
fn print_agent_response(&self, content: &str) {
// Display non-tool text messages from LLM
if !content.trim().is_empty() {
print!("{}", content);
use std::io::Write;
std::io::stdout().flush().ok();
println!("{}", content);
}
}
fn notify_sse_received(&self) {
// Show "Thinking..." status on single line
self.print_status_line("💭 Thinking...");
// No-op - we don't want to overwrite previous content
// The "Thinking..." status was causing overwrites
}
fn flush(&self) {
@@ -322,10 +340,29 @@ pub async fn call_refinement_llm_with_tools(
// The agent will have access to tools and execute them
let task = user_message;
let result = agent
let result = match agent
.execute_task_with_timing(&task, None, false, false, false, true, None)
.await
.context("Failed to call refinement LLM")?;
{
Ok(response) => response,
Err(e) => {
// Classify the error
let error_type = classify_error(&e);
// Display user-friendly message based on error type
match error_type {
ErrorType::Recoverable(recoverable) => {
eprintln!("⚠️ Recoverable error: {:?}", recoverable);
eprintln!(" Details: {}", e);
}
ErrorType::NonRecoverable => {
eprintln!("❌ Non-recoverable error: {}", e);
}
}
return Err(e.context("Failed to call refinement LLM"));
}
};
println!("📝 Refinement complete");

View File

@@ -587,9 +587,6 @@ pub async fn run_coach_player_loop(
// Set environment variable for custom todo path
std::env::set_var("G3_TODO_PATH", planner_config.todo_path().display().to_string());
// Set environment variable for workspace path (used for logs)
std::env::set_var("G3_WORKSPACE_PATH", planner_config.codepath.display().to_string());
let mut turn = 1;
let mut coach_feedback = String::new();
@@ -701,19 +698,7 @@ pub async fn run_planning_mode(
print_msg("\n🎯 G3 Planning Mode");
print_msg("==================\n");
// Create the LLM provider for planning
print_msg("🔧 Initializing planner provider...");
let provider = match llm::create_planner_provider(config_path).await {
Ok(p) => p,
Err(e) => {
print_msg(&format!("❌ Failed to initialize provider: {}", e));
print_msg("Please check your configuration file.");
anyhow::bail!("Provider initialization failed: {}", e);
}
};
print_msg(&format!("✅ Provider initialized: {}", provider.name()));
// Get codepath from argument or prompt user
// Get codepath first (needed for setting workspace path early)
let codepath = match codepath {
Some(path) => {
let expanded = expand_codepath(&path)?;
@@ -732,6 +717,30 @@ pub async fn run_planning_mode(
anyhow::bail!("Codepath does not exist: {}", codepath.display());
}
// Set workspace path EARLY for all logging (before provider initialization)
std::env::set_var("G3_WORKSPACE_PATH", codepath.display().to_string());
// Create logs directory and verify it exists
let logs_dir = codepath.join("logs");
if !logs_dir.exists() {
fs::create_dir_all(&logs_dir)
.context("Failed to create logs directory")?;
}
print_msg(&format!("📁 Logs directory: {}", logs_dir.display()));
// Create the LLM provider for planning
print_msg("🔧 Initializing planner provider...");
let provider = match llm::create_planner_provider(config_path).await {
Ok(p) => p,
Err(e) => {
print_msg(&format!("❌ Failed to initialize provider: {}", e));
print_msg("Please check your configuration file.");
anyhow::bail!("Provider initialization failed: {}", e);
}
};
print_msg(&format!("✅ Provider initialized: {}", provider.name()));
// Create configuration
let config = PlannerConfig {
codepath: codepath.clone(),