diff --git a/crates/goose/src/providers/base.rs b/crates/goose/src/providers/base.rs index 9525486821b9..ea108e3fb9f0 100644 --- a/crates/goose/src/providers/base.rs +++ b/crates/goose/src/providers/base.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; use super::errors::ProviderError; use crate::message::Message; use crate::model::ModelConfig; +use crate::utils::safe_truncate; use rmcp::model::Tool; use utoipa::ToSchema; @@ -338,6 +339,48 @@ pub trait Provider: Send + Sync { self.get_model_config().model_name } } + + /// Generate a session name/description based on the conversation history + /// This method can be overridden by providers to implement custom session naming strategies. + /// The default implementation creates a prompt asking for a concise description in 4 words or less. + async fn generate_session_name(&self, messages: &[Message]) -> Result { + // Create a prompt for a concise description + let mut description_prompt = "Based on the conversation so far, provide a concise description of this session in 4 words or less. This will be used for finding the session later in a UI with limited space - reply *ONLY* with the description".to_string(); + + // Get context from the first 3 user messages + let context: Vec = messages + .iter() + .filter(|m| m.role == rmcp::model::Role::User) + .take(3) + .map(|m| m.as_concat_text()) + .collect(); + + if !context.is_empty() { + description_prompt = format!( + "Here are the first few user messages:\n{}\n\n{}", + context.join("\n"), + description_prompt + ); + } + + let message = Message::user().with_text(&description_prompt); + let result = self + .complete( + "Reply with only a description in four words or less", + &[message], + &[], + ) + .await?; + + let description = result.0.as_concat_text(); + let sanitized_description = if description.chars().count() > 100 { + safe_truncate(&description, 100) + } else { + description + }; + + Ok(sanitized_description) + } } /// A message stream yields partial text content but complete tool calls, all within the Message object diff --git a/crates/goose/src/session/storage.rs b/crates/goose/src/session/storage.rs index 507e3872423a..5d50d1d83437 100644 --- a/crates/goose/src/session/storage.rs +++ b/crates/goose/src/session/storage.rs @@ -1294,52 +1294,15 @@ pub async fn generate_description_with_schedule_id( )); } - // Create a special message asking for a 3-word description - let mut description_prompt = "Based on the conversation so far, provide a concise description of this session in 4 words or less. This will be used for finding the session later in a UI with limited space - reply *ONLY* with the description".to_string(); - - // get context from messages so far, limiting each message to 300 chars for security - let context: Vec = messages - .iter() - .filter(|m| m.role == rmcp::model::Role::User) - .take(3) // Use up to first 3 user messages for context - .map(|m| { - let text = m.as_concat_text(); - safe_truncate(&text, 300) - }) - .collect(); - - if !context.is_empty() { - description_prompt = format!( - "Here are the first few user messages:\n{}\n\n{}", - context.join("\n"), - description_prompt - ); - } - - // Generate the description with error handling - let message = Message::user().with_text(&description_prompt); - let result = provider - .complete( - "Reply with only a description in four words or less", - &[message], - &[], - ) + // Use the provider's session naming capability + let sanitized_description = provider + .generate_session_name(messages) .await .map_err(|e| { tracing::error!("Failed to generate session description: {}", e); anyhow::anyhow!("Failed to generate session description") })?; - let description = result.0.as_concat_text(); - - // Validate description length for security - let sanitized_description = if description.chars().count() > 100 { - tracing::warn!("Generated description too long, truncating"); - safe_truncate(&description, 100) - } else { - description - }; - // Create metadata with proper working_dir or read existing and update let mut metadata = if secure_path.exists() { read_metadata(&secure_path)?