//! LLM integration for planning mode //! //! This module provides LLM-based functionality for: //! - Requirements refinement //! - Generating requirements summaries //! - Generating git commit messages use anyhow::{anyhow, Context, Result}; use std::io::Write; 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; /// Create an LLM provider for the planner based on config pub async fn create_planner_provider( config_path: Option<&str>, ) -> Result> { // Load configuration let config = Config::load(config_path) .context("Failed to load configuration")?; // Get planner provider reference (or default) let provider_ref = config.get_planner_provider(); // If no explicit planner provider, notify user about fallback if config.providers.planner.is_none() { let msg = "Note: No 'planner' provider specified in config. Using default_provider '{provider}' for planning mode." .replace("{provider}", provider_ref); println!("â„šī¸ {}", msg); } // Parse the provider reference let (provider_type, config_name) = Config::parse_provider_reference(provider_ref)?; // Create the appropriate provider match provider_type.as_str() { "anthropic" => { let anthropic_config = config .get_anthropic_config(&config_name) .ok_or_else(|| anyhow!("Anthropic config '{}' not found", config_name))?; let provider = g3_providers::AnthropicProvider::new_with_name( format!("anthropic.{}", config_name), anthropic_config.api_key.clone(), Some(anthropic_config.model.clone()), anthropic_config.max_tokens, anthropic_config.temperature, anthropic_config.cache_config.clone(), anthropic_config.enable_1m_context, anthropic_config.thinking_budget_tokens, )?; Ok(Box::new(provider)) } "openai" => { let openai_config = config .get_openai_config(&config_name) .ok_or_else(|| anyhow!("OpenAI config '{}' not found", config_name))?; let provider = g3_providers::OpenAIProvider::new_with_name( format!("openai.{}", config_name), openai_config.api_key.clone(), Some(openai_config.model.clone()), openai_config.base_url.clone(), openai_config.max_tokens, openai_config.temperature, )?; Ok(Box::new(provider)) } "databricks" => { let databricks_config = config .get_databricks_config(&config_name) .ok_or_else(|| anyhow!("Databricks config '{}' not found", config_name))?; let provider = if let Some(token) = &databricks_config.token { g3_providers::DatabricksProvider::from_token_with_name( format!("databricks.{}", config_name), databricks_config.host.clone(), token.clone(), databricks_config.model.clone(), databricks_config.max_tokens, databricks_config.temperature, )? } else { g3_providers::DatabricksProvider::from_oauth_with_name( format!("databricks.{}", config_name), databricks_config.host.clone(), databricks_config.model.clone(), databricks_config.max_tokens, databricks_config.temperature, ) .await? }; Ok(Box::new(provider)) } _ => { Err(anyhow!( "Unsupported provider type '{}' for planner. Supported: anthropic, openai, databricks", provider_type )) } } } /// Generate a summary of requirements for planner_history.txt /// /// Uses the planner LLM to generate a concise summary of the requirements. /// The summary is at most 5 lines, each at most 120 characters. pub async fn generate_requirements_summary( provider: &dyn LLMProvider, requirements: &str, ) -> Result { let prompt = prompts::GENERATE_REQUIREMENTS_SUMMARY_PROMPT .replace("{requirements}", requirements); let messages = vec![Message::new(MessageRole::User, prompt)]; let request = CompletionRequest { messages, max_tokens: Some(500), // Summary should be short temperature: Some(0.3), // Low temperature for consistent output stream: false, tools: None, disable_thinking: false, }; let response = provider .complete(request) .await .context("Failed to generate requirements summary")?; // Clean up the response - ensure max 5 lines, each max 120 chars let summary = response .content .lines() .take(5) .map(|line| { if line.chars().count() > 120 { let chars: String = line.chars().take(117).collect(); format!("{}...", chars) } else { line.to_string() } }) .collect::>() .join("\n"); Ok(summary) } /// Generate a git commit message based on the requirements /// /// Uses the planner LLM to generate a commit summary and description. /// Returns (summary, description) tuple. pub async fn generate_commit_message( provider: &dyn LLMProvider, requirements: &str, requirements_file: &str, todo_file: &str, ) -> Result<(String, String)> { let prompt = prompts::GENERATE_COMMIT_MESSAGE_PROMPT .replace("{requirements}", requirements) .replace("{requirements_file}", requirements_file) .replace("{todo_file}", todo_file); let messages = vec![Message::new(MessageRole::User, prompt)]; let request = CompletionRequest { messages, max_tokens: Some(1000), temperature: Some(0.3), stream: false, tools: None, disable_thinking: false, }; let response = provider .complete(request) .await .context("Failed to generate commit message")?; // Parse the response using the existing parse_commit_message function Ok(crate::planner::parse_commit_message(&response.content)) } /// A simple UiWriter implementation for planner output /// Uses single-line status updates during LLM processing #[derive(Clone)] pub struct PlannerUiWriter { tool_count: std::sync::Arc, } impl Default for PlannerUiWriter { fn default() -> Self { Self::new() } } impl PlannerUiWriter { pub fn new() -> Self { Self { tool_count: std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0)), } } /// Clear the current line and print a status message #[allow(dead_code)] fn print_status_line(&self, message: &str) { // Print status message without overwriting previous content // Use println to ensure each status is on its own line println!("{:.80}", message); } } impl g3_core::ui_writer::UiWriter for PlannerUiWriter { fn print(&self, message: &str) { println!("{}", message); } fn println(&self, message: &str) { println!("{}", message); } fn print_inline(&self, message: &str) { print!("{}", message); } fn print_system_prompt(&self, _prompt: &str) {} fn print_context_status(&self, message: &str) { println!("📊 {}", message); } fn print_context_thinning(&self, message: &str) { println!("đŸ—œī¸ {}", message); } fn print_tool_header(&self, tool_name: &str, tool_args: Option<&serde_json::Value>) { let count = self.tool_count.fetch_add(1, std::sync::atomic::Ordering::SeqCst) + 1; // Format args for display (first 50 chars, must be safe char boundary) 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() > 100 { // Use char_indices to safely truncate at char boundary let truncate_idx = args_str.char_indices() .nth(100) .map(|(idx, _)| idx) .unwrap_or(args_str.len()); args_str[..truncate_idx].to_string() } else { args_str } } else { "{}".to_string() }; // Print on EXACTLY one line using ui_writer.println self.println(&format!("🔧 [{}] \x1b[38;5;240m{} {}\x1b[39m", count, tool_name, args_display)); } fn print_tool_arg(&self, _key: &str, _value: &str) {} fn print_tool_output_header(&self) {} fn update_tool_output_line(&self, _line: &str) {} 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, _tokens_delta: u32, _context_percentage: f32) {} fn print_agent_prompt(&self) { // No-op - don't add extra blank lines } // NOTE: this is a partial response, so don't print newlines. Ideally we'd accumulate the // message and only then print it. fn print_agent_response(&self, content: &str) { // Display non-tool text messages from LLM without adding extra newlines let trimmed = content.trim_end(); if !trimmed.is_empty() { // Strip ALL trailing whitespace and DON'T add any back. // Tool headers already use println!() which adds their own newline. // Adding newlines here causes cumulative blank lines between tool calls. print!("{}", trimmed); std::io::stdout().flush().ok(); } } fn notify_sse_received(&self) { // No-op - we don't want to overwrite previous content // The "Thinking..." status was causing overwrites } fn flush(&self) { use std::io::Write; std::io::stdout().flush().ok(); } fn prompt_user_yes_no(&self, _message: &str) -> bool { true // Default to yes for automated planner } fn prompt_user_choice(&self, _message: &str, _options: &[&str]) -> usize { 0 // Default to first option } } /// Call LLM to refine requirements using a full Agent with tool execution pub async fn call_refinement_llm_with_tools( config: &Config, codepath: &str, workspace: &str, ) -> Result { // Build system message with codepath context let system_prompt = prompts::REFINE_REQUIREMENTS_SYSTEM_PROMPT .replace("", codepath); // Build user message let user_message = build_refinement_user_message(codepath); // Create agent with planner config let planner_config = config.for_planner()?; let ui_writer = PlannerUiWriter::new(); // CRITICAL FIX: Use the actual workspace directory, NOT codepath! // The workspace is where session data should be written (e.g., /tmp/g3_test_workspace) // The codepath is where the source code lives (e.g., ~/RustroverProjects/g3) let workspace_path = std::path::PathBuf::from(workspace); let project = Project::new(workspace_path.clone()); project.ensure_workspace_exists()?; project.enter_workspace()?; // Create agent - not autonomous mode, just regular agent with tools let mut agent = Agent::new_with_readme_and_quiet( planner_config, ui_writer, Some(system_prompt), false, // not quiet ) .await?; // Execute the refinement task // The agent will have access to tools and execute them let task = user_message; let result = match agent .execute_task_with_timing(&task, None, false, false, false, true, None) .await { 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"); Ok(result.response) } /// Build the user message for requirements refinement /// /// This message instructs the LLM to read the codebase and refine requirements. pub fn build_refinement_user_message(codepath: &str) -> String { format!( r#"Please refine the requirements for the codebase at: {codepath} Before making suggestions, please: 1. Read the codebase structure using shell commands like `ls`, `find`, or `tree` 2. Read `{codepath}/g3-plan/planner_history.txt` to understand past planning activities 3. Read any `{codepath}/g3-plan/completed_requirements_*.md` files to see what was implemented before 4. Read `{codepath}/g3-plan/new_requirements.md` which contains the requirements to refine After understanding the context, update the `{codepath}/g3-plan/new_requirements.md` file by prepending your refined requirements under the heading `{{{{CURRENT REQUIREMENTS}}}}`. When you are done, provide a brief summary of the refinements you made."#, codepath = codepath ) } #[cfg(test)] mod tests { use super::*; #[test] fn test_build_refinement_user_message() { let msg = build_refinement_user_message("/test/project"); assert!(msg.contains("/test/project")); assert!(msg.contains("planner_history.txt")); assert!(msg.contains("new_requirements.md")); assert!(msg.contains("{{CURRENT REQUIREMENTS}}")); } }