Files
g3/crates/g3-planner/src/llm.rs
Dhanji R. Prasanna c2aa80647a Remove legacy logs/ directory, consolidate all data under .g3/
This change removes the legacy logs/ directory and consolidates all
session data, error logs, and discovery files under the .g3/ directory.

New directory structure:
- .g3/sessions/<session_id>/session.json - session logs
- .g3/errors/ - error logs (was logs/errors/)
- .g3/background_processes/ - background process logs
- .g3/discovery/ - planner discovery files (was workspace/logs/)

Changes:
- paths.rs: Remove get_logs_dir()/logs_dir(), add get_errors_dir(),
  get_background_processes_dir(), get_discovery_dir()
- session.rs: Anonymous sessions now use .g3/sessions/anonymous_<ts>/
- error_handling.rs: Errors now saved to .g3/errors/
- project.rs: Remove logs_dir() and ensure_logs_dir() methods
- feedback_extraction.rs: Remove logs_dir field and fallback logic
- planner: Use .g3/ for workspace data and .g3/discovery/ for reports
- flock.rs: Look for session metrics in .g3/sessions/
- coach_feedback.rs: Remove fallback to logs/ path
- Update all tests to use new paths
- Update README.md and .gitignore
2026-01-12 18:20:08 +05:30

410 lines
14 KiB
Rust
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 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<Box<dyn LLMProvider>> {
// 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<String> {
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.len() > 120 {
format!("{}...", &line[..117])
} else {
line.to_string()
}
})
.collect::<Vec<_>>()
.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<std::sync::atomic::AtomicUsize>,
}
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<String> {
// Build system message with codepath context
let system_prompt = prompts::REFINE_REQUIREMENTS_SYSTEM_PROMPT
.replace("<codepath>", 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}}"));
}
}