Skip to content
5 changes: 4 additions & 1 deletion crates/goose/src/agents/prompt_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}
}

Expand Down Expand Up @@ -81,6 +82,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<ExtensionInfo> = extensions_info
.into_iter()
Expand Down
99 changes: 99 additions & 0 deletions crates/goose/src/agents/reply_parts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,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;

Expand Down Expand Up @@ -266,3 +271,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<String> = 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(())
}
}
Loading