diff --git a/crates/goose/src/agents/prompt_manager.rs b/crates/goose/src/agents/prompt_manager.rs index 5ff0e911aa..3b5e26aed8 100644 --- a/crates/goose/src/agents/prompt_manager.rs +++ b/crates/goose/src/agents/prompt_manager.rs @@ -25,7 +25,8 @@ impl PromptManager { system_prompt_override: None, system_prompt_extras: Vec::new(), // Use the fixed current date time so that prompt cache can be used. - current_date_timestamp: Utc::now().format("%Y-%m-%d %H:%M:%S").to_string(), + // Filtering to an hour to balance user time accuracy and multi session prompt cache hits. + current_date_timestamp: Utc::now().format("%Y-%m-%d %H:00").to_string(), } } @@ -58,6 +59,8 @@ impl PromptManager { false, )); } + // Stable tool ordering is important for multi session prompt caching. + extensions_info.sort_by(|a, b| a.name.cmp(&b.name)); let sanitized_extensions_info: Vec = extensions_info .into_iter() diff --git a/crates/goose/src/agents/reply_parts.rs b/crates/goose/src/agents/reply_parts.rs index 9b5d1403bf..bc443d8374 100644 --- a/crates/goose/src/agents/reply_parts.rs +++ b/crates/goose/src/agents/reply_parts.rs @@ -60,6 +60,11 @@ impl Agent { tools.push(frontend_tool.tool.clone()); } + if !router_enabled { + // Stable tool ordering is important for multi session prompt caching. + tools.sort_by(|a, b| a.name.cmp(&b.name)); + } + // Prepare system prompt let extensions_info = self.extension_manager.get_extensions_info().await; @@ -282,3 +287,97 @@ impl Agent { Ok(()) } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::conversation::message::Message; + use crate::model::ModelConfig; + use crate::providers::base::{Provider, ProviderUsage, Usage}; + use crate::providers::errors::ProviderError; + use async_trait::async_trait; + use rmcp::object; + + #[derive(Clone)] + struct MockProvider { + model_config: ModelConfig, + } + + #[async_trait] + impl Provider for MockProvider { + fn metadata() -> crate::providers::base::ProviderMetadata { + crate::providers::base::ProviderMetadata::empty() + } + + fn get_model_config(&self) -> ModelConfig { + self.model_config.clone() + } + + async fn complete_with_model( + &self, + _model_config: &ModelConfig, + _system: &str, + _messages: &[Message], + _tools: &[Tool], + ) -> anyhow::Result<(Message, ProviderUsage), ProviderError> { + Ok(( + Message::assistant().with_text("ok"), + ProviderUsage::new("mock".to_string(), Usage::default()), + )) + } + } + + #[tokio::test] + async fn prepare_tools_sorts_when_router_disabled_and_includes_frontend_and_list_tools( + ) -> anyhow::Result<()> { + let agent = crate::agents::Agent::new(); + + let model_config = ModelConfig::new("test-model").unwrap(); + let provider = std::sync::Arc::new(MockProvider { model_config }); + agent.update_provider(provider).await?; + + // Disable the router to trigger sorting + agent.disable_router_for_recipe().await; + + // Add unsorted frontend tools + let frontend_tools = vec![ + Tool::new( + "frontend__z_tool".to_string(), + "Z tool".to_string(), + object!({ "type": "object", "properties": { } }), + ), + Tool::new( + "frontend__a_tool".to_string(), + "A tool".to_string(), + object!({ "type": "object", "properties": { } }), + ), + ]; + + agent + .add_extension(crate::agents::extension::ExtensionConfig::Frontend { + name: "frontend".to_string(), + description: "desc".to_string(), + tools: frontend_tools, + instructions: None, + bundled: None, + available_tools: vec![], + }) + .await + .unwrap(); + + let (tools, _toolshim_tools, _system_prompt) = agent.prepare_tools_and_prompt().await?; + + // Ensure both platform and frontend tools are present + let names: Vec = tools.iter().map(|t| t.name.clone().into_owned()).collect(); + assert!(names.iter().any(|n| n.starts_with("platform__"))); + assert!(names.iter().any(|n| n == "frontend__a_tool")); + assert!(names.iter().any(|n| n == "frontend__z_tool")); + + // Verify the names are sorted ascending + let mut sorted = names.clone(); + sorted.sort(); + assert_eq!(names, sorted); + + Ok(()) + } +}