diff --git a/crates/goose/src/providers/claude_code.rs b/crates/goose/src/providers/claude_code.rs index 67c1d5d18f14..7b8d6b4a4807 100644 --- a/crates/goose/src/providers/claude_code.rs +++ b/crates/goose/src/providers/claude_code.rs @@ -510,50 +510,6 @@ impl ClaudeCodeProvider { Ok(lines) } - /// Generate a simple session description without calling subprocess - fn generate_simple_session_description( - &self, - messages: &[Message], - ) -> Result<(Message, ProviderUsage), ProviderError> { - // Extract the first user message text - let description = messages - .iter() - .find(|m| m.role == Role::User) - .and_then(|m| { - m.content.iter().find_map(|c| match c { - MessageContent::Text(text_content) => Some(&text_content.text), - _ => None, - }) - }) - .map(|text| { - // Take first few words, limit to 4 words - text.split_whitespace() - .take(4) - .collect::>() - .join(" ") - }) - .unwrap_or_else(|| "Simple task".to_string()); - - if std::env::var("GOOSE_CLAUDE_CODE_DEBUG").is_ok() { - println!("=== CLAUDE CODE PROVIDER DEBUG ==="); - println!("Generated simple session description: {}", description); - println!("Skipped subprocess call for session description"); - println!("================================"); - } - - let message = Message::new( - Role::Assistant, - chrono::Utc::now().timestamp(), - vec![MessageContent::text(description.clone())], - ); - - let usage = Usage::default(); - - Ok(( - message, - ProviderUsage::new(self.model.model_name.clone(), usage), - )) - } } /// Extract model aliases from the CLI's initialize control_response. @@ -785,9 +741,11 @@ impl Provider for ClaudeCodeProvider { messages: &[Message], tools: &[Tool], ) -> Result<(Message, ProviderUsage), ProviderError> { - // Check if this is a session description request (short system prompt asking for 4 words or less) - if system.contains("four words or less") || system.contains("4 words or less") { - return self.generate_simple_session_description(messages); + if super::cli_common::is_session_description_request(system) { + return super::cli_common::generate_simple_session_description( + &model_config.model_name, + messages, + ); } // session_id is None before a session is created (e.g. model listing). diff --git a/crates/goose/src/providers/cli_common.rs b/crates/goose/src/providers/cli_common.rs new file mode 100644 index 000000000000..92744714c42e --- /dev/null +++ b/crates/goose/src/providers/cli_common.rs @@ -0,0 +1,46 @@ +use super::base::{ProviderUsage, Usage}; +use super::errors::ProviderError; +use crate::conversation::message::{Message, MessageContent}; +use rmcp::model::Role; + +pub fn is_session_description_request(system: &str) -> bool { + system.contains("four words or less") || system.contains("4 words or less") +} + +pub fn generate_simple_session_description( + model_name: &str, + messages: &[Message], +) -> Result<(Message, ProviderUsage), ProviderError> { + let description = messages + .iter() + .find(|m| m.role == Role::User) + .and_then(|m| { + m.content.iter().find_map(|c| match c { + MessageContent::Text(text_content) => Some(&text_content.text), + _ => None, + }) + }) + .map(|text| { + text.split_whitespace() + .take(4) + .collect::>() + .join(" ") + }) + .unwrap_or_else(|| "Simple task".to_string()); + + tracing::debug!( + description = %description, + "Generated simple session description, skipped subprocess" + ); + + let message = Message::new( + Role::Assistant, + chrono::Utc::now().timestamp(), + vec![MessageContent::text(description)], + ); + + Ok(( + message, + ProviderUsage::new(model_name.to_string(), Usage::default()), + )) +} diff --git a/crates/goose/src/providers/codex.rs b/crates/goose/src/providers/codex.rs index 453d99641a78..d8081585c446 100644 --- a/crates/goose/src/providers/codex.rs +++ b/crates/goose/src/providers/codex.rs @@ -410,50 +410,6 @@ impl CodexProvider { Ok((message, usage)) } - /// Generate a simple session description without calling subprocess - fn generate_simple_session_description( - &self, - messages: &[Message], - ) -> Result<(Message, ProviderUsage), ProviderError> { - // Extract the first user message text - let description = messages - .iter() - .find(|m| m.role == Role::User) - .and_then(|m| { - m.content.iter().find_map(|c| match c { - MessageContent::Text(text_content) => Some(&text_content.text), - _ => None, - }) - }) - .map(|text| { - // Take first few words, limit to 4 words - text.split_whitespace() - .take(4) - .collect::>() - .join(" ") - }) - .unwrap_or_else(|| "Simple task".to_string()); - - if std::env::var("GOOSE_CODEX_DEBUG").is_ok() { - println!("=== CODEX PROVIDER DEBUG ==="); - println!("Generated simple session description: {}", description); - println!("Skipped subprocess call for session description"); - println!("============================"); - } - - let message = Message::new( - Role::Assistant, - chrono::Utc::now().timestamp(), - vec![MessageContent::text(description.clone())], - ); - - let usage = Usage::default(); - - Ok(( - message, - ProviderUsage::new(self.model.model_name.clone(), usage), - )) - } } /// Builds the text prompt and extracts images to temp files in a single pass. @@ -724,9 +680,11 @@ impl Provider for CodexProvider { messages: &[Message], tools: &[Tool], ) -> Result<(Message, ProviderUsage), ProviderError> { - // Check if this is a session description request - if system.contains("four words or less") || system.contains("4 words or less") { - return self.generate_simple_session_description(messages); + if super::cli_common::is_session_description_request(system) { + return super::cli_common::generate_simple_session_description( + &model_config.model_name, + messages, + ); } let lines = self.execute_command(system, messages, tools).await?; @@ -1192,15 +1150,6 @@ mod tests { #[test] fn test_session_description_generation() { - let provider = CodexProvider { - command: PathBuf::from("codex"), - model: ModelConfig::new("gpt-5.2-codex").unwrap(), - name: "codex".to_string(), - reasoning_effort: "high".to_string(), - skip_git_check: false, - mcp_config_overrides: Vec::new(), - }; - let messages = vec![Message::new( Role::User, chrono::Utc::now().timestamp(), @@ -1209,12 +1158,15 @@ mod tests { )], )]; - let result = provider.generate_simple_session_description(&messages); + let result = super::cli_common::generate_simple_session_description( + "gpt-5.2-codex", + &messages, + ); assert!(result.is_ok()); - let (message, _usage) = result.unwrap(); + let (message, usage) = result.unwrap(); + assert_eq!(usage.model, "gpt-5.2-codex"); if let MessageContent::Text(text) = &message.content[0] { - // Should be truncated to 4 words let word_count = text.text.split_whitespace().count(); assert!(word_count <= 4); } else { @@ -1224,18 +1176,12 @@ mod tests { #[test] fn test_session_description_empty_messages() { - let provider = CodexProvider { - command: PathBuf::from("codex"), - model: ModelConfig::new("gpt-5.2-codex").unwrap(), - name: "codex".to_string(), - reasoning_effort: "high".to_string(), - skip_git_check: false, - mcp_config_overrides: Vec::new(), - }; - let messages: Vec = vec![]; - let result = provider.generate_simple_session_description(&messages); + let result = super::cli_common::generate_simple_session_description( + "gpt-5.2-codex", + &messages, + ); assert!(result.is_ok()); let (message, _usage) = result.unwrap(); diff --git a/crates/goose/src/providers/cursor_agent.rs b/crates/goose/src/providers/cursor_agent.rs index 500904e0dd4b..117b42ac070c 100644 --- a/crates/goose/src/providers/cursor_agent.rs +++ b/crates/goose/src/providers/cursor_agent.rs @@ -276,50 +276,6 @@ impl CursorAgentProvider { Ok(lines) } - /// Generate a simple session description without calling subprocess - fn generate_simple_session_description( - &self, - messages: &[Message], - ) -> Result<(Message, ProviderUsage), ProviderError> { - // Extract the first user message text - let description = messages - .iter() - .find(|m| m.role == Role::User) - .and_then(|m| { - m.content.iter().find_map(|c| match c { - MessageContent::Text(text_content) => Some(&text_content.text), - _ => None, - }) - }) - .map(|text| { - // Take first few words, limit to 4 words - text.split_whitespace() - .take(4) - .collect::>() - .join(" ") - }) - .unwrap_or_else(|| "Simple task".to_string()); - - if std::env::var("GOOSE_CURSOR_AGENT_DEBUG").is_ok() { - println!("=== CURSOR AGENT PROVIDER DEBUG ==="); - println!("Generated simple session description: {}", description); - println!("Skipped subprocess call for session description"); - println!("================================"); - } - - let message = Message::new( - Role::Assistant, - chrono::Utc::now().timestamp(), - vec![MessageContent::text(description.clone())], - ); - - let usage = Usage::default(); - - Ok(( - message, - ProviderUsage::new(self.model.model_name.clone(), usage), - )) - } } impl ProviderDef for CursorAgentProvider { @@ -378,9 +334,11 @@ impl Provider for CursorAgentProvider { messages: &[Message], tools: &[Tool], ) -> Result<(Message, ProviderUsage), ProviderError> { - // Check if this is a session description request (short system prompt asking for 4 words or less) - if system.contains("four words or less") || system.contains("4 words or less") { - return self.generate_simple_session_description(messages); + if super::cli_common::is_session_description_request(system) { + return super::cli_common::generate_simple_session_description( + &model_config.model_name, + messages, + ); } let lines = self.execute_command(system, messages, tools).await?; diff --git a/crates/goose/src/providers/gemini_cli.rs b/crates/goose/src/providers/gemini_cli.rs index 8a4fdc77bc64..b35ddd203418 100644 --- a/crates/goose/src/providers/gemini_cli.rs +++ b/crates/goose/src/providers/gemini_cli.rs @@ -95,48 +95,6 @@ impl GeminiCliProvider { .unwrap_or_default() } - fn is_session_description_request(system: &str) -> bool { - system.contains("four words or less") || system.contains("4 words or less") - } - - fn generate_simple_session_description( - &self, - messages: &[Message], - ) -> Result<(Message, ProviderUsage), ProviderError> { - let description = messages - .iter() - .find(|m| m.role == Role::User) - .and_then(|m| { - m.content.iter().find_map(|c| match c { - MessageContent::Text(text_content) => Some(&text_content.text), - _ => None, - }) - }) - .map(|text| { - text.split_whitespace() - .take(4) - .collect::>() - .join(" ") - }) - .unwrap_or_else(|| "Simple task".to_string()); - - tracing::debug!( - description = %description, - "Generated simple session description, skipped subprocess" - ); - - let message = Message::new( - Role::Assistant, - chrono::Utc::now().timestamp(), - vec![MessageContent::text(description)], - ); - - Ok(( - message, - ProviderUsage::new(self.model.model_name.clone(), Usage::default()), - )) - } - /// Build the prompt for the CLI invocation. When resuming a session the CLI /// maintains conversation context internally, so only the latest user /// message is needed. On the first turn (no session yet) the system prompt @@ -380,8 +338,11 @@ impl Provider for GeminiCliProvider { messages: &[Message], tools: &[Tool], ) -> Result<(Message, ProviderUsage), ProviderError> { - if Self::is_session_description_request(system) { - return self.generate_simple_session_description(messages); + if super::cli_common::is_session_description_request(system) { + return super::cli_common::generate_simple_session_description( + &model_config.model_name, + messages, + ); } let payload = json!({ diff --git a/crates/goose/src/providers/mod.rs b/crates/goose/src/providers/mod.rs index 4da0241a08f3..b5af7da6e9bd 100644 --- a/crates/goose/src/providers/mod.rs +++ b/crates/goose/src/providers/mod.rs @@ -8,6 +8,7 @@ pub mod bedrock; pub mod canonical; pub mod chatgpt_codex; pub mod claude_code; +pub mod cli_common; pub mod codex; pub mod cursor_agent; pub mod databricks;