remove Tool Selection Strategy preview (#6250)

This commit is contained in:
Angie Jones
2025-12-23 14:09:06 -06:00
committed by GitHub
parent 8e1eb1ebb6
commit 40246432dc
32 changed files with 10 additions and 1270 deletions
@@ -1154,11 +1154,6 @@ pub fn remove_extension_dialog() -> anyhow::Result<()> {
pub async fn configure_settings_dialog() -> anyhow::Result<()> {
let setting_type = cliclack::select("What setting would you like to configure?")
.item("goose_mode", "goose mode", "Configure goose mode")
.item(
"goose_router_strategy",
"Router Tool Selection Strategy",
"Experimental: configure a strategy for auto selecting tools to use",
)
.item(
"tool_permission",
"Tool Permission",
@@ -1197,9 +1192,6 @@ pub async fn configure_settings_dialog() -> anyhow::Result<()> {
"goose_mode" => {
configure_goose_mode_dialog()?;
}
"goose_router_strategy" => {
configure_goose_router_strategy_dialog()?;
}
"tool_permission" => {
configure_tool_permissions_dialog().await.and(Ok(()))?;
// No need to print config file path since it's already handled.
@@ -1271,33 +1263,6 @@ pub fn configure_goose_mode_dialog() -> anyhow::Result<()> {
Ok(())
}
pub fn configure_goose_router_strategy_dialog() -> anyhow::Result<()> {
let config = Config::global();
let enable_router = cliclack::select("Would you like to enable smart tool routing?")
.item(
true,
"Enable Router",
"Use LLM-based intelligence to select tools",
)
.item(
false,
"Disable Router",
"Use the default tool selection strategy",
)
.interact()?;
config.set_param("GOOSE_ENABLE_ROUTER", enable_router)?;
let msg = if enable_router {
"Router enabled - using LLM-based intelligence for tool selection"
} else {
"Router disabled - using default tool selection"
};
cliclack::outro(msg)?;
Ok(())
}
pub fn configure_tool_output_dialog() -> anyhow::Result<()> {
let config = Config::global();
-2
View File
@@ -386,7 +386,6 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
.set_context(PlatformExtensionContext {
session_id: Some(session_id.clone()),
extension_manager: Some(Arc::downgrade(&agent.extension_manager)),
tool_route_manager: Some(Arc::downgrade(&agent.tool_route_manager)),
})
.await;
@@ -425,7 +424,6 @@ pub async fn build_session(session_config: SessionBuilderConfig) -> CliSession {
// Extensions need to be added after the session is created because we change directory when resuming a session
// If we get extensions_override, only run those extensions and none other
let extensions_to_run: Vec<_> = if let Some(extensions) = session_config.extensions_override {
agent.disable_router_for_recipe().await;
extensions.into_iter().collect()
} else if session_config.resume {
match SessionManager::get_session(&session_id, false).await {
-2
View File
@@ -361,7 +361,6 @@ derive_utoipa!(Icon as IconSchema);
super::routes::agent::agent_add_extension,
super::routes::agent::agent_remove_extension,
super::routes::agent::update_agent_provider,
super::routes::agent::update_router_tool_selector,
super::routes::action_required::confirm_tool_action,
super::routes::reply::reply,
super::routes::session::list_sessions,
@@ -525,7 +524,6 @@ derive_utoipa!(Icon as IconSchema);
super::routes::agent::ReadResourceResponse,
super::routes::agent::CallToolRequest,
super::routes::agent::CallToolResponse,
super::routes::agent::UpdateRouterToolSelectorRequest,
super::routes::agent::StartAgentRequest,
super::routes::agent::ResumeAgentRequest,
super::routes::agent::UpdateFromSessionRequest,
+1 -39
View File
@@ -53,11 +53,6 @@ pub struct GetToolsQuery {
session_id: String,
}
#[derive(Deserialize, utoipa::ToSchema)]
pub struct UpdateRouterToolSelectorRequest {
session_id: String,
}
#[derive(Deserialize, utoipa::ToSchema)]
pub struct StartAgentRequest {
working_dir: String,
@@ -288,7 +283,7 @@ async fn resume_agent(
})?;
agent
.load_provider(provider)
.update_provider(provider, &payload.session_id)
.await
.map_err(|e| ErrorResponse {
message: format!("Could not configure agent: {}", e),
@@ -501,35 +496,6 @@ async fn update_agent_provider(
Ok(())
}
#[utoipa::path(
post,
path = "/agent/update_router_tool_selector",
request_body = UpdateRouterToolSelectorRequest,
responses(
(status = 200, description = "Tool selection strategy updated successfully", body = String),
(status = 401, description = "Unauthorized - invalid secret key"),
(status = 424, description = "Agent not initialized"),
(status = 500, description = "Internal server error")
)
)]
async fn update_router_tool_selector(
State(state): State<Arc<AppState>>,
Json(payload): Json<UpdateRouterToolSelectorRequest>,
) -> Result<Json<String>, StatusCode> {
let agent = state.get_agent_for_route(payload.session_id).await?;
agent
.update_router_tool_selector(None, Some(true))
.await
.map_err(|e| {
error!("Failed to update tool selection strategy: {}", e);
StatusCode::INTERNAL_SERVER_ERROR
})?;
Ok(Json(
"Tool selection strategy updated successfully".to_string(),
))
}
#[utoipa::path(
post,
path = "/agent/add_extension",
@@ -695,10 +661,6 @@ pub fn routes(state: Arc<AppState>) -> Router {
.route("/agent/read_resource", post(read_resource))
.route("/agent/call_tool", post(call_tool))
.route("/agent/update_provider", post(update_agent_provider))
.route(
"/agent/update_router_tool_selector",
post(update_router_tool_selector),
)
.route("/agent/update_from_session", post(update_from_session))
.route("/agent/add_extension", post(agent_add_extension))
.route("/agent/remove_extension", post(agent_remove_extension))
+1 -93
View File
@@ -12,20 +12,17 @@ use super::final_output_tool::FinalOutputTool;
use super::platform_tools;
use super::tool_execution::{ToolCallResult, CHAT_MODE_TOOL_SKIPPED_RESPONSE, DECLINED_RESPONSE};
use crate::action_required_manager::ActionRequiredManager;
use crate::agents::extension::{ExtensionConfig, ExtensionError, ExtensionResult, ToolInfo};
use crate::agents::extension::{ExtensionConfig, ExtensionResult, ToolInfo};
use crate::agents::extension_manager::{get_parameter_names, ExtensionManager};
use crate::agents::extension_manager_extension::MANAGE_EXTENSIONS_TOOL_NAME_COMPLETE;
use crate::agents::final_output_tool::{FINAL_OUTPUT_CONTINUATION_MESSAGE, FINAL_OUTPUT_TOOL_NAME};
use crate::agents::platform_tools::PLATFORM_MANAGE_SCHEDULE_TOOL_NAME;
use crate::agents::prompt_manager::PromptManager;
use crate::agents::retry::{RetryManager, RetryResult};
use crate::agents::router_tools::ROUTER_LLM_SEARCH_TOOL_NAME;
use crate::agents::subagent_task_config::TaskConfig;
use crate::agents::subagent_tool::{
create_subagent_tool, handle_subagent_tool, SUBAGENT_TOOL_NAME,
};
use crate::agents::tool_route_manager::ToolRouteManager;
use crate::agents::tool_router_index_manager::ToolRouterIndexManager;
use crate::agents::types::SessionConfig;
use crate::agents::types::{FrontendTool, SharedProvider, ToolResultReceiver};
use crate::config::{get_enabled_extensions, Config, GooseMode};
@@ -95,7 +92,6 @@ pub struct Agent {
pub(super) tool_result_tx: mpsc::Sender<(String, ToolResult<CallToolResult>)>,
pub(super) tool_result_rx: ToolResultReceiver,
pub tool_route_manager: Arc<ToolRouteManager>,
pub(super) scheduler_service: Mutex<Option<Arc<dyn SchedulerTrait>>>,
pub(super) retry_manager: RetryManager,
pub(super) tool_inspection_manager: ToolInspectionManager,
@@ -169,7 +165,6 @@ impl Agent {
confirmation_rx: Mutex::new(confirm_rx),
tool_result_tx: tool_tx,
tool_result_rx: Arc::new(Mutex::new(tool_rx)),
tool_route_manager: Arc::new(ToolRouteManager::new()),
scheduler_service: Mutex::new(None),
retry_manager: RetryManager::new(),
tool_inspection_manager: Self::create_default_tool_inspection_manager(),
@@ -369,10 +364,6 @@ impl Agent {
*scheduler_service = Some(scheduler);
}
pub async fn disable_router_for_recipe(&self) {
self.tool_route_manager.disable_router_for_recipe().await;
}
/// Get a reference count clone to the provider
pub async fn provider(&self) -> Result<Arc<dyn Provider>, anyhow::Error> {
match &*self.provider.lock().await {
@@ -518,15 +509,6 @@ impl Agent {
"Frontend tool execution required".to_string(),
None,
)))
} else if tool_call.name == ROUTER_LLM_SEARCH_TOOL_NAME {
match self
.tool_route_manager
.dispatch_route_search_tool(tool_call.arguments.unwrap_or_default())
.await
{
Ok(tool_result) => tool_result,
Err(e) => return (request_id, Err(e)),
}
} else {
// Clone the result to ensure no references to extension_manager are returned
let result = self
@@ -617,28 +599,6 @@ impl Agent {
}
}
// If LLM tool selection is functional, index the tools
if self.tool_route_manager.is_router_functional().await {
let selector = self.tool_route_manager.get_router_tool_selector().await;
if let Some(selector) = selector {
let selector = Arc::new(selector);
if let Err(e) = ToolRouterIndexManager::update_extension_tools(
&selector,
&self.extension_manager,
&extension.name(),
"add",
)
.await
{
return Err(ExtensionError::SetupError(format!(
"Failed to index tools for extension {}: {}",
extension.name(),
e
)));
}
}
}
Ok(())
}
@@ -702,29 +662,8 @@ impl Agent {
prefixed_tools
}
pub async fn list_tools_for_router(&self) -> Vec<Tool> {
self.tool_route_manager
.list_tools_for_router(&self.extension_manager)
.await
}
pub async fn remove_extension(&self, name: &str) -> Result<()> {
self.extension_manager.remove_extension(name).await?;
// If LLM tool selection is functional, remove tools from the index
if self.tool_route_manager.is_router_functional().await {
let selector = self.tool_route_manager.get_router_tool_selector().await;
if let Some(selector) = selector {
ToolRouterIndexManager::update_extension_tools(
&selector,
&self.extension_manager,
name,
"remove",
)
.await?;
}
}
Ok(())
}
@@ -1052,10 +991,6 @@ impl Agent {
remaining_requests,
filtered_response,
} = self.categorize_tools(&response, &tools).await;
let requests_to_record: Vec<ToolRequest> = frontend_requests.iter().chain(remaining_requests.iter()).cloned().collect();
self.tool_route_manager
.record_tool_requests(&requests_to_record)
.await;
yield AgentEvent::Message(filtered_response.clone());
tokio::task::yield_now().await;
@@ -1377,9 +1312,6 @@ impl Agent {
let mut current_provider = self.provider.lock().await;
*current_provider = Some(provider.clone());
self.update_router_tool_selector(Some(provider.clone()), None)
.await?;
SessionManager::update_session(session_id)
.provider_name(provider.get_name())
.model_config(provider.get_model_config())
@@ -1388,30 +1320,6 @@ impl Agent {
.context("Failed to persist provider config to session")
}
pub async fn load_provider(&self, provider: Arc<dyn Provider>) -> Result<()> {
let mut current_provider = self.provider.lock().await;
*current_provider = Some(provider.clone());
self.update_router_tool_selector(Some(provider.clone()), None)
.await
}
pub async fn update_router_tool_selector(
&self,
provider: Option<Arc<dyn Provider>>,
reindex_all: Option<bool>,
) -> Result<()> {
let provider = match provider {
Some(p) => p,
None => self.provider().await?,
};
// Delegate to ToolRouteManager
self.tool_route_manager
.update_router_tool_selector(provider, reindex_all, &self.extension_manager)
.await
}
/// Override the system prompt with a custom template
pub async fn override_system_prompt(&self, template: String) {
let mut prompt_manager = self.prompt_manager.lock().await;
@@ -923,7 +923,6 @@ mod tests {
let context = PlatformExtensionContext {
session_id: None,
extension_manager: None,
tool_route_manager: None,
};
let client = CodeExecutionClient::new(context).unwrap();
@@ -948,7 +947,6 @@ mod tests {
let context = PlatformExtensionContext {
session_id: None,
extension_manager: None,
tool_route_manager: None,
};
let client = CodeExecutionClient::new(context).unwrap();
-2
View File
@@ -109,8 +109,6 @@ pub struct PlatformExtensionContext {
pub session_id: Option<String>,
pub extension_manager:
Option<std::sync::Weak<crate::agents::extension_manager::ExtensionManager>>,
pub tool_route_manager:
Option<std::sync::Weak<crate::agents::tool_route_manager::ToolRouteManager>>,
}
#[derive(Debug, Clone)]
@@ -264,7 +264,6 @@ impl ExtensionManager {
context: Mutex::new(PlatformExtensionContext {
session_id: None,
extension_manager: None,
tool_route_manager: None,
}),
provider,
}
@@ -1,6 +1,5 @@
use crate::agents::extension::PlatformExtensionContext;
use crate::agents::mcp_client::{Error, McpClientTrait};
use crate::agents::tool_router_index_manager::ToolRouterIndexManager;
use crate::config::get_extension_by_name;
use anyhow::Result;
use async_trait::async_trait;
@@ -163,7 +162,6 @@ impl ExtensionManagerClient {
}
}
#[allow(clippy::too_many_lines)]
async fn manage_extensions_impl(
&self,
action: ManageExtensionAction,
@@ -182,43 +180,8 @@ impl ExtensionManagerClient {
)
})?;
let tool_route_manager = self
.context
.tool_route_manager
.as_ref()
.and_then(|weak| weak.upgrade());
// Update tool router index if router is functional
if let Some(tool_route_manager) = &tool_route_manager {
if tool_route_manager.is_router_functional().await {
let selector = tool_route_manager.get_router_tool_selector().await;
if let Some(selector) = selector {
let selector_action = if action == ManageExtensionAction::Disable {
"remove"
} else {
"add"
};
let selector = Arc::new(selector);
if let Err(e) = ToolRouterIndexManager::update_extension_tools(
&selector,
&extension_manager,
&extension_name,
selector_action,
)
.await
{
return Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to update LLM index: {}", e),
None,
));
}
}
}
}
if action == ManageExtensionAction::Disable {
let result = extension_manager
return extension_manager
.remove_extension(&extension_name)
.await
.map(|_| {
@@ -228,7 +191,6 @@ impl ExtensionManagerClient {
))]
})
.map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None));
return result;
}
let config = match get_extension_by_name(&extension_name) {
@@ -245,7 +207,7 @@ impl ExtensionManagerClient {
}
};
let result = extension_manager
extension_manager
.add_extension(config)
.await
.map(|_| {
@@ -254,40 +216,7 @@ impl ExtensionManagerClient {
extension_name
))]
})
.map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None));
// Update LLM index if operation was successful and LLM routing is functional
if result.is_ok() {
if let Some(tool_route_manager) = &tool_route_manager {
if tool_route_manager.is_router_functional().await {
let selector = tool_route_manager.get_router_tool_selector().await;
if let Some(selector) = selector {
let llm_action = if action == ManageExtensionAction::Disable {
"remove"
} else {
"add"
};
let selector = Arc::new(selector);
if let Err(e) = ToolRouterIndexManager::update_extension_tools(
&selector,
&extension_manager,
&extension_name,
llm_action,
)
.await
{
return Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to update LLM index: {}", e),
None,
));
}
}
}
}
}
result
.map_err(|e| ErrorData::new(ErrorCode::INTERNAL_ERROR, e.to_string(), None))
}
async fn handle_list_resources(
-4
View File
@@ -14,8 +14,6 @@ pub mod platform_tools;
pub mod prompt_manager;
mod reply_parts;
pub mod retry;
mod router_tool_selector;
mod router_tools;
mod schedule_tool;
pub(crate) mod skills_extension;
pub mod subagent_execution_tool;
@@ -24,8 +22,6 @@ mod subagent_task_config;
pub mod subagent_tool;
pub(crate) mod todo_extension;
mod tool_execution;
mod tool_route_manager;
mod tool_router_index_manager;
pub mod types;
pub use agent::{Agent, AgentEvent};
-13
View File
@@ -6,7 +6,6 @@ use serde_json::Value;
use std::collections::HashMap;
use crate::agents::extension::ExtensionInfo;
use crate::agents::router_tools::llm_search_tool_prompt;
use crate::hints::load_hints::{load_hint_files, AGENTS_MD_FILENAME, GOOSE_HINTS_FILENAME};
use crate::{
config::{Config, GooseMode},
@@ -33,8 +32,6 @@ impl Default for PromptManager {
#[derive(Serialize)]
struct SystemPromptContext {
extensions: Vec<ExtensionInfo>,
#[serde(skip_serializing_if = "Option::is_none")]
tool_selection_strategy: Option<String>,
current_date_time: String,
#[serde(skip_serializing_if = "Option::is_none")]
extension_tool_limits: Option<(usize, usize)>,
@@ -52,7 +49,6 @@ pub struct SystemPromptBuilder<'a, M> {
extensions_info: Vec<ExtensionInfo>,
frontend_instructions: Option<String>,
extension_tool_count: Option<(usize, usize)>,
router_enabled: bool,
subagents_enabled: bool,
hints: Option<String>,
code_execution_mode: bool,
@@ -85,11 +81,6 @@ impl<'a> SystemPromptBuilder<'a, PromptManager> {
self
}
pub fn with_router_enabled(mut self, enabled: bool) -> Self {
self.router_enabled = enabled;
self
}
pub fn with_code_execution_mode(mut self, enabled: bool) -> Self {
self.code_execution_mode = enabled;
self
@@ -158,7 +149,6 @@ impl<'a> SystemPromptBuilder<'a, PromptManager> {
let context = SystemPromptContext {
extensions: sanitized_extensions_info,
tool_selection_strategy: self.router_enabled.then(llm_search_tool_prompt),
current_date_time: self.manager.current_date_timestamp.clone(),
extension_tool_limits,
goose_mode,
@@ -247,7 +237,6 @@ impl PromptManager {
extensions_info: vec![],
frontend_instructions: None,
extension_tool_count: None,
router_enabled: false,
subagents_enabled: false,
hints: None,
code_execution_mode: false,
@@ -369,7 +358,6 @@ mod tests {
"how to use this extension",
true,
))
.with_router_enabled(true)
.build();
assert_snapshot!(system_prompt)
@@ -391,7 +379,6 @@ mod tests {
"<instructions on how to use extension B (no resources)>",
false,
))
.with_router_enabled(true)
.with_extension_and_tool_counts(MAX_EXTENSIONS + 1, MAX_TOOLS + 1)
.build();
+4 -19
View File
@@ -113,16 +113,8 @@ impl Agent {
&self,
working_dir: &std::path::Path,
) -> Result<(Vec<Tool>, Vec<Tool>, String)> {
// Get router enabled status
let router_enabled = self.tool_route_manager.is_router_enabled().await;
// Get tools from extension manager
let mut tools = self.list_tools_for_router().await;
// If router is disabled and no tools were returned, fall back to regular tools
if !router_enabled && tools.is_empty() {
tools = self.list_tools(None).await;
}
let mut tools = self.list_tools(None).await;
// Add frontend tools
let frontend_tools = self.frontend_tools.lock().await;
@@ -139,10 +131,8 @@ impl Agent {
tools.retain(|tool| tool.name.starts_with(&code_exec_prefix));
}
if !router_enabled {
// Stable tool ordering is important for multi session prompt caching.
tools.sort_by(|a, b| a.name.cmp(&b.name));
}
// 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;
@@ -159,7 +149,6 @@ impl Agent {
.with_extensions(extensions_info.into_iter())
.with_frontend_instructions(self.frontend_instructions.lock().await.clone())
.with_extension_and_tool_counts(extension_count, tool_count)
.with_router_enabled(router_enabled)
.with_code_execution_mode(code_execution_active)
.with_hints(working_dir)
.with_enable_subagents(self.subagents_enabled().await)
@@ -437,8 +426,7 @@ mod tests {
}
#[tokio::test]
async fn prepare_tools_sorts_when_router_disabled_and_includes_frontend_and_list_tools(
) -> anyhow::Result<()> {
async fn prepare_tools_sorts_and_includes_frontend_and_list_tools() -> anyhow::Result<()> {
let agent = crate::agents::Agent::new();
let session = SessionManager::create_session(
@@ -452,9 +440,6 @@ mod tests {
let provider = std::sync::Arc::new(MockProvider { model_config });
agent.update_provider(provider, &session.id).await?;
// Disable the router to trigger sorting
agent.disable_router_for_recipe().await;
// Add unsorted frontend tools
let frontend_tools = vec![
Tool::new(
@@ -1,180 +0,0 @@
use rmcp::model::{Content, ErrorCode, ErrorData};
use rmcp::model::{JsonObject, Tool};
use anyhow::Result;
use async_trait::async_trait;
use serde::Serialize;
use std::borrow::Cow;
use std::collections::HashMap;
use std::collections::VecDeque;
use std::sync::Arc;
use tokio::sync::RwLock;
use crate::conversation::message::Message;
use crate::prompt_template::render_global_file;
use crate::providers::base::Provider;
#[derive(Serialize)]
struct ToolSelectorContext {
tools: String,
query: String,
}
#[async_trait]
pub trait RouterToolSelector: Send + Sync {
async fn select_tools(&self, params: JsonObject) -> Result<Vec<Content>, ErrorData>;
async fn index_tools(&self, tools: &[Tool], extension_name: &str) -> Result<(), ErrorData>;
async fn remove_tool(&self, tool_name: &str) -> Result<(), ErrorData>;
async fn record_tool_call(&self, tool_name: &str) -> Result<(), ErrorData>;
async fn get_recent_tool_calls(&self, limit: usize) -> Result<Vec<String>, ErrorData>;
}
pub struct LLMToolSelector {
llm_provider: Arc<dyn Provider>,
tool_strings: Arc<RwLock<HashMap<String, String>>>, // extension_name -> tool_string
recent_tool_calls: Arc<RwLock<VecDeque<String>>>,
}
impl LLMToolSelector {
pub async fn new(provider: Arc<dyn Provider>) -> Result<Self> {
Ok(Self {
llm_provider: provider.clone(),
tool_strings: Arc::new(RwLock::new(HashMap::new())),
recent_tool_calls: Arc::new(RwLock::new(VecDeque::with_capacity(100))),
})
}
}
#[async_trait]
impl RouterToolSelector for LLMToolSelector {
async fn select_tools(&self, params: JsonObject) -> Result<Vec<Content>, ErrorData> {
let query = params
.get("query")
.and_then(|v| v.as_str())
.ok_or_else(|| ErrorData {
code: ErrorCode::INVALID_PARAMS,
message: Cow::from("Missing 'query' parameter"),
data: None,
})?;
let extension_name = params
.get("extension_name")
.and_then(|v| v.as_str())
.map(|s| s.to_string());
// Get relevant tool strings based on extension_name
let tool_strings = self.tool_strings.read().await;
let relevant_tools = if let Some(ext) = &extension_name {
tool_strings.get(ext).cloned()
} else {
// If no extension specified, use all tools
Some(
tool_strings
.values()
.cloned()
.collect::<Vec<String>>()
.join("\n"),
)
};
if let Some(tools) = relevant_tools {
// Use template to generate the prompt
let context = ToolSelectorContext {
tools: tools.clone(),
query: query.to_string(),
};
let user_prompt =
render_global_file("router_tool_selector.md", &context).map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to render prompt template: {}", e)),
data: None,
})?;
let user_message = Message::user().with_text(&user_prompt);
let response = self
.llm_provider
.complete("system", &[user_message], &[])
.await
.map_err(|e| ErrorData {
code: ErrorCode::INTERNAL_ERROR,
message: Cow::from(format!("Failed to search tools: {}", e)),
data: None,
})?;
// Extract just the message content from the response
let (message, _usage) = response;
let text = message.content[0].as_text().unwrap_or_default();
// Split the response into individual tool entries
let tool_entries: Vec<Content> = text
.split("\n\n")
.filter(|entry| entry.trim().starts_with("Tool:"))
.map(|entry| Content::text(entry.trim().to_string()))
.collect();
Ok(tool_entries)
} else {
Ok(vec![])
}
}
async fn index_tools(&self, tools: &[Tool], extension_name: &str) -> Result<(), ErrorData> {
let mut tool_strings = self.tool_strings.write().await;
for tool in tools {
let tool_string = format!(
"Tool: {}\nDescription: {}\nSchema: {}",
tool.name,
tool.description
.as_ref()
.map(|d| d.as_ref())
.unwrap_or_default(),
serde_json::to_string_pretty(&tool.input_schema)
.unwrap_or_else(|_| "{}".to_string())
);
// Use the provided extension_name instead of parsing from tool name
let entry = tool_strings.entry(extension_name.to_string()).or_default();
// Check if this tool already exists in the entry
if !entry.contains(&format!("Tool: {}", tool.name)) {
if !entry.is_empty() {
entry.push_str("\n\n");
}
entry.push_str(&tool_string);
}
}
Ok(())
}
async fn remove_tool(&self, tool_name: &str) -> Result<(), ErrorData> {
let mut tool_strings = self.tool_strings.write().await;
if let Some(extension_name) = tool_name.split("__").next() {
tool_strings.remove(extension_name);
}
Ok(())
}
async fn record_tool_call(&self, tool_name: &str) -> Result<(), ErrorData> {
let mut recent_calls = self.recent_tool_calls.write().await;
if recent_calls.len() >= 100 {
recent_calls.pop_front();
}
recent_calls.push_back(tool_name.to_string());
Ok(())
}
async fn get_recent_tool_calls(&self, limit: usize) -> Result<Vec<String>, ErrorData> {
let recent_calls = self.recent_tool_calls.read().await;
Ok(recent_calls.iter().rev().take(limit).cloned().collect())
}
}
// Helper function to create a boxed tool selector
pub async fn create_tool_selector(
provider: Arc<dyn Provider>,
) -> Result<Box<dyn RouterToolSelector>> {
let selector = LLMToolSelector::new(provider).await?;
Ok(Box::new(selector))
}
-65
View File
@@ -1,65 +0,0 @@
use crate::agents::extension_manager_extension::{
LIST_RESOURCES_TOOL_NAME, MANAGE_EXTENSIONS_TOOL_NAME, READ_RESOURCE_TOOL_NAME,
SEARCH_AVAILABLE_EXTENSIONS_TOOL_NAME,
};
use indoc::indoc;
use rmcp::model::{Tool, ToolAnnotations};
use rmcp::object;
pub const ROUTER_LLM_SEARCH_TOOL_NAME: &str = "router__llm_search";
pub fn llm_search_tool() -> Tool {
Tool::new(
ROUTER_LLM_SEARCH_TOOL_NAME.to_string(),
indoc! {r#"
Searches for relevant tools based on the user's messages.
Format a query to search for the most relevant tools based on the user's messages.
Pay attention to the keywords in the user's messages, especially the last message and potential tools they are asking for.
This tool should be invoked when the user's messages suggest they are asking for a tool to be run.
Use the extension_name parameter to filter tools by the appropriate extension.
For example, if the user is asking to list the files in the current directory, you filter for the "developer" extension.
Example: {"User": "list the files in the current directory", "Query": "list files in current directory", "Extension Name": "developer", "k": 5}
Extension name is not optional, it is required.
The returned result will be a list of tool names, descriptions, and schemas from which you, the agent can select the most relevant tool to invoke.
"#}
.to_string(),
object!({
"type": "object",
"required": ["query", "extension_name"],
"properties": {
"extension_name": {"type": "string", "description": "The name of the extension to filter tools by"},
"query": {"type": "string", "description": "The query to search for the most relevant tools based on the user's messages"},
"k": {"type": "integer", "description": "The number of tools to retrieve (defaults to 5)", "default": 5}
}
})
).annotate(ToolAnnotations {
title: Some("LLM search for relevant tools".to_string()),
read_only_hint: Some(true),
destructive_hint: Some(false),
idempotent_hint: Some(false),
open_world_hint: Some(false),
})
}
pub fn llm_search_tool_prompt() -> String {
format!(
r#"# LLM Tool Selection Instructions
Important: the user has opted to dynamically enable tools, so although an extension could be enabled, \
please invoke the llm search tool to actually retrieve the most relevant tools to use according to the user's messages.
For example, if the user has 3 extensions enabled, but they are asking for a tool to read a pdf file, \
you would invoke the llm_search tool to find the most relevant read pdf tool.
By dynamically enabling tools, you (goose) as the agent save context window space and allow the user to dynamically retrieve the most relevant tools.
Be sure to format a query packed with relevant keywords to search for the most relevant tools.
In addition to the extension names available to you, you also have platform extension tools available to you.
The platform extension contains the following tools:
- {}
- {}
- {}
- {}
"#,
SEARCH_AVAILABLE_EXTENSIONS_TOOL_NAME,
MANAGE_EXTENSIONS_TOOL_NAME,
READ_RESOURCE_TOOL_NAME,
LIST_RESOURCES_TOOL_NAME
)
}
@@ -26,8 +26,6 @@ new ones.
No extensions are defined. You should let the user know that they should add extensions.
# Response Guidelines
- Use Markdown formatting for all responses.
@@ -37,21 +37,6 @@ and platform__list_resources on this extension.
how to use this extension
# LLM Tool Selection Instructions
Important: the user has opted to dynamically enable tools, so although an extension could be enabled, \
please invoke the llm search tool to actually retrieve the most relevant tools to use according to the user's messages.
For example, if the user has 3 extensions enabled, but they are asking for a tool to read a pdf file, \
you would invoke the llm_search tool to find the most relevant read pdf tool.
By dynamically enabling tools, you (goose) as the agent save context window space and allow the user to dynamically retrieve the most relevant tools.
Be sure to format a query packed with relevant keywords to search for the most relevant tools.
In addition to the extension names available to you, you also have platform extension tools available to you.
The platform extension contains the following tools:
- search_available_extensions
- manage_extensions
- read_resource
- list_resources
# Response Guidelines
- Use Markdown formatting for all responses.
@@ -51,21 +51,6 @@ You should only disable extensions found from the search_available_extensions to
List all the extensions available to disable in the response.
Explain that minimizing extensions helps with the recall of the correct tools to use.
# LLM Tool Selection Instructions
Important: the user has opted to dynamically enable tools, so although an extension could be enabled, \
please invoke the llm search tool to actually retrieve the most relevant tools to use according to the user's messages.
For example, if the user has 3 extensions enabled, but they are asking for a tool to read a pdf file, \
you would invoke the llm_search tool to find the most relevant read pdf tool.
By dynamically enabling tools, you (goose) as the agent save context window space and allow the user to dynamically retrieve the most relevant tools.
Be sure to format a query packed with relevant keywords to search for the most relevant tools.
In addition to the extension names available to you, you also have platform extension tools available to you.
The platform extension contains the following tools:
- search_available_extensions
- manage_extensions
- read_resource
- list_resources
# Response Guidelines
- Use Markdown formatting for all responses.
@@ -1,185 +0,0 @@
use crate::agents::extension_manager::ExtensionManager;
use crate::agents::router_tool_selector::{create_tool_selector, RouterToolSelector};
use crate::agents::router_tools::{self};
use crate::agents::tool_execution::ToolCallResult;
use crate::agents::tool_router_index_manager::ToolRouterIndexManager;
use crate::config::Config;
use crate::conversation::message::ToolRequest;
use crate::providers::base::Provider;
use anyhow::{anyhow, Result};
use rmcp::model::{ErrorCode, ErrorData, JsonObject, Tool};
use std::sync::Arc;
use tokio::sync::Mutex;
use tracing::error;
pub struct ToolRouteManager {
router_tool_selector: Mutex<Option<Arc<Box<dyn RouterToolSelector>>>>,
router_disabled_override: Mutex<bool>,
}
impl Default for ToolRouteManager {
fn default() -> Self {
Self::new()
}
}
impl ToolRouteManager {
pub fn new() -> Self {
Self {
router_tool_selector: Mutex::new(None),
router_disabled_override: Mutex::new(false),
}
}
pub async fn disable_router_for_recipe(&self) {
*self.router_disabled_override.lock().await = true;
*self.router_tool_selector.lock().await = None;
}
pub async fn record_tool_requests(&self, requests: &[ToolRequest]) {
let selector = self.router_tool_selector.lock().await.clone();
for request in requests {
if let Ok(tool_call) = &request.tool_call {
if let Some(ref selector) = selector {
if let Err(e) = selector.record_tool_call(&tool_call.name).await {
error!("Failed to record tool call: {}", e);
}
}
}
}
}
pub async fn dispatch_route_search_tool(
&self,
arguments: JsonObject,
) -> Result<ToolCallResult, ErrorData> {
let selector = self.router_tool_selector.lock().await.clone();
match selector.as_ref() {
Some(selector) => match selector.select_tools(arguments).await {
Ok(content) => Ok(ToolCallResult::from(Ok(rmcp::model::CallToolResult {
content,
structured_content: None,
is_error: Some(false),
meta: None,
}))),
Err(e) => Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
format!("Failed to select tools: {}", e),
None,
)),
},
None => Err(ErrorData::new(
ErrorCode::INTERNAL_ERROR,
"No tool selector available".to_string(),
None,
)),
}
}
pub async fn is_router_enabled(&self) -> bool {
if *self.router_disabled_override.lock().await {
return false;
}
let config = Config::global();
if let Ok(config_value) = config.get_param::<String>("GOOSE_ENABLE_ROUTER") {
return config_value.to_lowercase() == "true";
}
// Default to false if neither is set
false
}
pub async fn update_router_tool_selector(
&self,
provider: Arc<dyn Provider>,
reindex_all: Option<bool>,
extension_manager: &ExtensionManager,
) -> Result<()> {
let enabled = self.is_router_enabled().await;
if !enabled {
return Ok(());
}
let selector = create_tool_selector(provider.clone())
.await
.map_err(|e| anyhow!("Failed to create tool selector: {}", e))?;
// Wrap selector in Arc for the index manager methods
let selector_arc = Arc::new(selector);
if reindex_all.unwrap_or(false) {
let enabled_extensions = extension_manager.list_extensions().await?;
for extension_name in enabled_extensions {
if let Err(e) = ToolRouterIndexManager::update_extension_tools(
&selector_arc,
extension_manager,
&extension_name,
"add",
)
.await
{
error!(
"Failed to index tools for extension {}: {}",
extension_name, e
);
}
}
}
// Update the selector
*self.router_tool_selector.lock().await = Some(selector_arc);
Ok(())
}
pub async fn get_router_tool_selector(&self) -> Option<Arc<Box<dyn RouterToolSelector>>> {
self.router_tool_selector.lock().await.clone()
}
/// Check if the router is actually functional (enabled in config AND initialized)
pub async fn is_router_functional(&self) -> bool {
if !self.is_router_enabled().await {
return false;
}
// Check if the selector actually exists (meaning it was successfully initialized)
self.router_tool_selector.lock().await.is_some()
}
pub async fn list_tools_for_router(&self, extension_manager: &ExtensionManager) -> Vec<Tool> {
// If router is disabled or overridden, return empty
if *self.router_disabled_override.lock().await {
return vec![];
}
let mut prefixed_tools = vec![];
// If router is enabled but not functional (no provider), just return the search tool
if !self.is_router_functional().await {
return prefixed_tools;
}
prefixed_tools.push(router_tools::llm_search_tool());
// Get recent tool calls from router tool selector
let selector = self.router_tool_selector.lock().await.clone();
if let Some(selector) = selector {
if let Ok(recent_calls) = selector.get_recent_tool_calls(20).await {
// Add recent tool calls to the list, avoiding duplicates
for tool_name in recent_calls {
// Find the tool in the extension manager's tools
if let Ok(extension_tools) = extension_manager.get_prefixed_tools(None).await {
if let Some(tool) = extension_tools.iter().find(|t| t.name == tool_name) {
// Only add if not already in prefixed_tools
if !prefixed_tools.iter().any(|t| t.name == tool.name) {
prefixed_tools.push(tool.clone());
}
}
}
}
}
}
prefixed_tools
}
}
@@ -1,76 +0,0 @@
use anyhow::{anyhow, Result};
use std::sync::Arc;
use tracing;
use crate::agents::extension_manager::ExtensionManager;
use crate::agents::router_tool_selector::RouterToolSelector;
/// Manages tool indexing operations for the router when LLM routing is enabled
pub struct ToolRouterIndexManager;
impl ToolRouterIndexManager {
/// Updates the LLM index for tools when extensions are added or removed
pub async fn update_extension_tools(
selector: &Arc<Box<dyn RouterToolSelector>>,
extension_manager: &ExtensionManager,
extension_name: &str,
action: &str,
) -> Result<()> {
match action {
"add" => {
// Get tools for specific extension
let tools = extension_manager
.get_prefixed_tools(Some(extension_name.to_string()))
.await?;
if !tools.is_empty() {
// Index all tools at once
selector
.index_tools(&tools, extension_name)
.await
.map_err(|e| {
anyhow!(
"Failed to index tools for extension {}: {}",
extension_name,
e
)
})?;
tracing::info!(
"Indexed {} tools for extension {}",
tools.len(),
extension_name
);
}
}
"remove" => {
// Remove all tools for this extension
let tools = extension_manager
.get_prefixed_tools(Some(extension_name.to_string()))
.await?;
for tool in &tools {
selector.remove_tool(&tool.name).await.map_err(|e| {
anyhow!(
"Failed to remove tool {} for extension {}: {}",
tool.name,
extension_name,
e
)
})?;
}
tracing::info!(
"Removed {} tools for extension {}",
tools.len(),
extension_name
);
}
_ => {
return Err(anyhow!("Invalid action: {}", action));
}
}
Ok(())
}
}
-1
View File
@@ -87,7 +87,6 @@ impl AgentManager {
.set_context(PlatformExtensionContext {
session_id: Some(session_id.clone()),
extension_manager: Some(Arc::downgrade(&agent.extension_manager)),
tool_route_manager: Some(Arc::downgrade(&agent.tool_route_manager)),
})
.await;
if let Some(provider) = &*self.default_provider.read().await {
-5
View File
@@ -363,11 +363,6 @@ async fn send_session_event(installation: &InstallationData) -> Result<(), Strin
if let Ok(max_turns) = config.get_param::<i64>("GOOSE_MAX_TURNS") {
event.insert_prop("setting_max_turns", max_turns).ok();
}
if let Ok(router_enabled) = config.get_param::<bool>("GOOSE_ENABLE_ROUTER") {
event
.insert_prop("setting_router_enabled", router_enabled)
.ok();
}
if let Ok(lead_model) = config.get_param::<String>("GOOSE_LEAD_MODEL") {
event.insert_prop("setting_lead_model", lead_model).ok();
@@ -1,11 +0,0 @@
You are a tool selection assistant. Your task is to find the most relevant tools based on the user's query.
Given the following tools:
{{ tools }}
Find the most relevant tools for the query: {{ query }}
Return the tools in this exact format for each tool:
Tool: <tool_name>
Description: <tool_description>
Schema: <tool_schema>
-2
View File
@@ -58,8 +58,6 @@ Explain that minimizing extensions helps with the recall of the correct tools to
{% endwith %}
{% endif %}
{{tool_selection_strategy}}
# Response Guidelines
- Use Markdown formatting for all responses.
-1
View File
@@ -475,7 +475,6 @@ mod tests {
.set_context(PlatformExtensionContext {
session_id: Some("test_session".to_string()),
extension_manager: Some(Arc::downgrade(&agent.extension_manager)),
tool_route_manager: Some(Arc::downgrade(&agent.tool_route_manager)),
})
.await;
@@ -226,7 +226,6 @@ These variables control how goose handles [tool execution](/docs/guides/goose-pe
| Variable | Purpose | Values | Default |
|----------|---------|---------|---------|
| `GOOSE_MODE` | Controls how goose handles tool execution | "auto", "approve", "chat", "smart_approve" | "smart_approve" |
| `GOOSE_ENABLE_ROUTER` | Enables [intelligent tool selection strategy](/docs/guides/managing-tools/tool-router) | "true", "false" | "false" |
| `GOOSE_TOOLSHIM` | Enables/disables tool call interpretation | "1", "true" (case insensitive) to enable | false |
| `GOOSE_TOOLSHIM_OLLAMA_MODEL` | Specifies the model for [tool call interpretation](/docs/experimental/ollama) | Model name (e.g. llama3.2, qwen2.5) | System default |
| `GOOSE_CLI_MIN_PRIORITY` | Controls verbosity of [tool output](/docs/guides/managing-tools/adjust-tool-output) | Float between 0.0 and 1.0 | 0.0 |
@@ -237,9 +236,6 @@ These variables control how goose handles [tool execution](/docs/guides/goose-pe
**Examples**
```bash
# Enable intelligent tool selection
export GOOSE_ENABLE_ROUTER=true
# Enable tool interpretation
export GOOSE_TOOLSHIM=true
export GOOSE_TOOLSHIM_OLLAMA_MODEL=llama3.2
@@ -20,11 +20,6 @@ import styles from '@site/src/components/Card/styles.module.css';
description="Configure fine-grained permissions to control which tools goose can use and when, ensuring secure and controlled automation."
link="/docs/guides/managing-tools/tool-permissions"
/>
<Card
title="Tool Selection Strategy"
description="Optimize tool selection with dynamic routing that loads only the tools you need, reducing context overhead and improving performance."
link="/docs/guides/managing-tools/tool-router"
/>
<Card
title="Adjust Tool Output"
description="Customize how tool interactions are displayed, from detailed verbose output to clean concise summaries."
@@ -1,163 +0,0 @@
---
sidebar_position: 3
title: Tool Selection Strategy
sidebar_label: Tool Selection Strategy
description: Configure smart tool selection to load only relevant tools, improving performance with multiple extensions
---
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import { PanelLeft } from 'lucide-react';
:::warning Experimental Feature
Tool Selection Strategy is an experimental feature and currently only tested with Claude models. Behavior and configuration may change in future releases.
:::
When you enable an [extension](/docs/getting-started/using-extensions), you gain access to all of its tools. For example, the Google Drive extension provides tools for reading documents, updating permissions, managing comments, and more. By default, goose loads all tools into context when interacting with the LLM.
Enabling multiple extensions gives you access to a wider range of tools, but loading a lot of tools into context can be inefficient and confusing for the LLM. It's like having every tool in your workshop spread out on your bench when you only need one or two.
To manage this more efficiently, you can enable a tool selection strategy. Instead of loading all tools for every interaction, it loads only the tools needed for your current task. This ensures that only the functionality you need is loaded into context, so you can keep more of your favorite extensions enabled. This provides:
- Reduced token consumption
- Improved LLM performance
- Better context management
- More accurate and efficient tool selection
## Tool Selection Options
| Option | Speed | Best For | How It Works |
|--------|-------|----------|--------------|
| **Disabled** | Fastest | Few extensions, simple setups | Loads all tools from enabled extensions |
| **Enabled** | Slower | Many extensions, complex queries | Uses LLM intelligence to select relevant tools |
:::tip
You can also use [tool permissions](/docs/guides/managing-tools/tool-permissions) to limit tool use.
:::
### Disabled (Default)
When tool selection strategy is disabled, goose loads all tools from enabled extensions into context. This is the traditional behavior and works well if you only have a few extensions enabled.
**Best for:**
- Simple setups with few extensions
- When you want all tools available at all times
- Maximum tool availability without selection logic
### Enabled (LLM-based Strategy)
When enabled, goose uses LLM intelligence to analyze your query and select only the most relevant tools from your enabled extensions. This reduces token consumption and improves tool selection accuracy when you have many extensions enabled.
**Best for:**
- Complex or ambiguous queries that require understanding context
- Setups with many extensions enabled
- When you want more accurate tool selection and reduced token usage
**Example:**
- Prompt: "help me analyze the contents of my document"
- Result: Intelligently selects document reading and analysis tools while ignoring unrelated tools like calendar or email extensions
## Configuration
<Tabs groupId="interface">
<TabItem value="ui" label="goose Desktop" default>
1. Click the <PanelLeft className="inline" size={16} /> button in the top-left to open the sidebar
2. Click the `Settings` button on the sidebar
3. Click the `Chat` tab
4. Under `Tool Selection Strategy`, choose your preferred option:
- `Disabled` - Use the default tool selection strategy
- `Enabled` - Use LLM-based intelligence to select tools
</TabItem>
<TabItem value="cli" label="goose CLI">
1. Run the configuration command:
```sh
goose configure
```
2. Select `goose settings`:
```sh
┌ goose-configure
│
◆ What would you like to configure?
│ ○ Configure Providers
│ ○ Add Extension
│ ○ Toggle Extensions
│ ○ Remove Extension
// highlight-start
│ ● goose settings (Set the goose mode, Tool Output, Tool Permissions, Experiment, goose recipe github repo and more)
// highlight-end
└
```
3. Select `Router Tool Selection Strategy`:
```sh
┌ goose-configure
│
◇ What would you like to configure?
│ goose settings
│
◆ What setting would you like to configure?
│ ○ goose mode
// highlight-start
│ ● Router Tool Selection Strategy (Experimental: configure a strategy for auto selecting tools to use)
// highlight-end
│ ○ Tool Permission
│ ○ Tool Output
│ ○ Toggle Experiment
│ ○ goose recipe github repo
└
```
4. Choose whether to enable smart tool routing:
```sh
┌ goose-configure
│
◇ What would you like to configure?
│ goose settings
│
◇ What setting would you like to configure?
│ Router Tool Selection Strategy
│
// highlight-start
◆ Would you like to enable smart tool routing?
│ ● Enable Router (Use LLM-based intelligence to select tools)
│ ○ Disable Router
// highlight-end
└
```
This example output shows that the router was enabled:
```
┌ goose-configure
│
◇ What would you like to configure?
│ goose settings
│
◇ What setting would you like to configure?
│ Router Tool Selection Strategy
│
◇ Would you like to enable smart tool routing?
│ Enable Router
│
└ Router enabled - using LLM-based intelligence for tool selection
```
When the router is enabled, goose CLI displays a message indicating when the `llm_search` strategy is in use.
</TabItem>
</Tabs>
## Environment Variable Configuration
You can also configure tool selection using environment variables or in your [configuration file](/docs/guides/config-files):
```bash
# Enable LLM-based tool selection
export GOOSE_ENABLE_ROUTER=true
# Disable (use default behavior)
export GOOSE_ENABLE_ROUTER=false
```
Or in your `config.yaml` file:
```yaml
GOOSE_ENABLE_ROUTER: 'true'
```
-50
View File
@@ -418,45 +418,6 @@
}
}
},
"/agent/update_router_tool_selector": {
"post": {
"tags": [
"super::routes::agent"
],
"operationId": "update_router_tool_selector",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UpdateRouterToolSelectorRequest"
}
}
},
"required": true
},
"responses": {
"200": {
"description": "Tool selection strategy updated successfully",
"content": {
"text/plain": {
"schema": {
"type": "string"
}
}
}
},
"401": {
"description": "Unauthorized - invalid secret key"
},
"424": {
"description": "Agent not initialized"
},
"500": {
"description": "Internal server error"
}
}
}
},
"/config": {
"get": {
"tags": [
@@ -5902,17 +5863,6 @@
}
}
},
"UpdateRouterToolSelectorRequest": {
"type": "object",
"required": [
"session_id"
],
"properties": {
"session_id": {
"type": "string"
}
}
},
"UpdateScheduleRequest": {
"type": "object",
"required": [
+1 -10
View File
@@ -2,7 +2,7 @@
import type { Client, Options as Options2, TDataShape } from './client';
import { client } from './client.gen';
import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, CallToolData, CallToolErrors, CallToolResponses, CheckProviderData, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectProviderData, DetectProviderErrors, DetectProviderResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, EditMessageData, EditMessageErrors, EditMessageResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetPricingData, GetPricingResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SendTelemetryEventData, SendTelemetryEventResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StartTunnelData, StartTunnelErrors, StartTunnelResponses, StatusData, StatusResponses, StopTunnelData, StopTunnelErrors, StopTunnelResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateRouterToolSelectorData, UpdateRouterToolSelectorErrors, UpdateRouterToolSelectorResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen';
import type { AddExtensionData, AddExtensionErrors, AddExtensionResponses, AgentAddExtensionData, AgentAddExtensionErrors, AgentAddExtensionResponses, AgentRemoveExtensionData, AgentRemoveExtensionErrors, AgentRemoveExtensionResponses, BackupConfigData, BackupConfigErrors, BackupConfigResponses, CallToolData, CallToolErrors, CallToolResponses, CheckProviderData, ConfirmToolActionData, ConfirmToolActionErrors, ConfirmToolActionResponses, CreateCustomProviderData, CreateCustomProviderErrors, CreateCustomProviderResponses, CreateRecipeData, CreateRecipeErrors, CreateRecipeResponses, CreateScheduleData, CreateScheduleErrors, CreateScheduleResponses, DecodeRecipeData, DecodeRecipeErrors, DecodeRecipeResponses, DeleteRecipeData, DeleteRecipeErrors, DeleteRecipeResponses, DeleteScheduleData, DeleteScheduleErrors, DeleteScheduleResponses, DeleteSessionData, DeleteSessionErrors, DeleteSessionResponses, DetectProviderData, DetectProviderErrors, DetectProviderResponses, DiagnosticsData, DiagnosticsErrors, DiagnosticsResponses, EditMessageData, EditMessageErrors, EditMessageResponses, EncodeRecipeData, EncodeRecipeErrors, EncodeRecipeResponses, ExportSessionData, ExportSessionErrors, ExportSessionResponses, GetCustomProviderData, GetCustomProviderErrors, GetCustomProviderResponses, GetExtensionsData, GetExtensionsErrors, GetExtensionsResponses, GetPricingData, GetPricingResponses, GetProviderModelsData, GetProviderModelsErrors, GetProviderModelsResponses, GetSessionData, GetSessionErrors, GetSessionInsightsData, GetSessionInsightsErrors, GetSessionInsightsResponses, GetSessionResponses, GetSlashCommandsData, GetSlashCommandsResponses, GetToolsData, GetToolsErrors, GetToolsResponses, GetTunnelStatusData, GetTunnelStatusResponses, ImportSessionData, ImportSessionErrors, ImportSessionResponses, InitConfigData, InitConfigErrors, InitConfigResponses, InspectRunningJobData, InspectRunningJobErrors, InspectRunningJobResponses, KillRunningJobData, KillRunningJobResponses, ListRecipesData, ListRecipesErrors, ListRecipesResponses, ListSchedulesData, ListSchedulesErrors, ListSchedulesResponses, ListSessionsData, ListSessionsErrors, ListSessionsResponses, McpUiProxyData, McpUiProxyErrors, McpUiProxyResponses, ParseRecipeData, ParseRecipeErrors, ParseRecipeResponses, PauseScheduleData, PauseScheduleErrors, PauseScheduleResponses, ProvidersData, ProvidersResponses, ReadAllConfigData, ReadAllConfigResponses, ReadConfigData, ReadConfigErrors, ReadConfigResponses, ReadResourceData, ReadResourceErrors, ReadResourceResponses, RecoverConfigData, RecoverConfigErrors, RecoverConfigResponses, RemoveConfigData, RemoveConfigErrors, RemoveConfigResponses, RemoveCustomProviderData, RemoveCustomProviderErrors, RemoveCustomProviderResponses, RemoveExtensionData, RemoveExtensionErrors, RemoveExtensionResponses, ReplyData, ReplyErrors, ReplyResponses, ResumeAgentData, ResumeAgentErrors, ResumeAgentResponses, RunNowHandlerData, RunNowHandlerErrors, RunNowHandlerResponses, SaveRecipeData, SaveRecipeErrors, SaveRecipeResponses, ScanRecipeData, ScanRecipeResponses, ScheduleRecipeData, ScheduleRecipeErrors, ScheduleRecipeResponses, SendTelemetryEventData, SendTelemetryEventResponses, SessionsHandlerData, SessionsHandlerErrors, SessionsHandlerResponses, SetConfigProviderData, SetRecipeSlashCommandData, SetRecipeSlashCommandErrors, SetRecipeSlashCommandResponses, StartAgentData, StartAgentErrors, StartAgentResponses, StartOpenrouterSetupData, StartOpenrouterSetupResponses, StartTetrateSetupData, StartTetrateSetupResponses, StartTunnelData, StartTunnelErrors, StartTunnelResponses, StatusData, StatusResponses, StopTunnelData, StopTunnelErrors, StopTunnelResponses, UnpauseScheduleData, UnpauseScheduleErrors, UnpauseScheduleResponses, UpdateAgentProviderData, UpdateAgentProviderErrors, UpdateAgentProviderResponses, UpdateCustomProviderData, UpdateCustomProviderErrors, UpdateCustomProviderResponses, UpdateFromSessionData, UpdateFromSessionErrors, UpdateFromSessionResponses, UpdateScheduleData, UpdateScheduleErrors, UpdateScheduleResponses, UpdateSessionNameData, UpdateSessionNameErrors, UpdateSessionNameResponses, UpdateSessionUserRecipeValuesData, UpdateSessionUserRecipeValuesErrors, UpdateSessionUserRecipeValuesResponses, UpsertConfigData, UpsertConfigErrors, UpsertConfigResponses, UpsertPermissionsData, UpsertPermissionsErrors, UpsertPermissionsResponses, ValidateConfigData, ValidateConfigErrors, ValidateConfigResponses } from './types.gen';
export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
/**
@@ -101,15 +101,6 @@ export const updateAgentProvider = <ThrowOnError extends boolean = false>(option
}
});
export const updateRouterToolSelector = <ThrowOnError extends boolean = false>(options: Options<UpdateRouterToolSelectorData, ThrowOnError>) => (options.client ?? client).post<UpdateRouterToolSelectorResponses, UpdateRouterToolSelectorErrors, ThrowOnError>({
url: '/agent/update_router_tool_selector',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers
}
});
export const readAllConfig = <ThrowOnError extends boolean = false>(options?: Options<ReadAllConfigData, ThrowOnError>) => (options?.client ?? client).get<ReadAllConfigResponses, unknown, ThrowOnError>({ url: '/config', ...options });
export const backupConfig = <ThrowOnError extends boolean = false>(options?: Options<BackupConfigData, ThrowOnError>) => (options?.client ?? client).post<BackupConfigResponses, BackupConfigErrors, ThrowOnError>({ url: '/config/backup', ...options });
-35
View File
@@ -1108,10 +1108,6 @@ export type UpdateProviderRequest = {
session_id: string;
};
export type UpdateRouterToolSelectorRequest = {
session_id: string;
};
export type UpdateScheduleRequest = {
cron: string;
};
@@ -1465,37 +1461,6 @@ export type UpdateAgentProviderResponses = {
200: unknown;
};
export type UpdateRouterToolSelectorData = {
body: UpdateRouterToolSelectorRequest;
path?: never;
query?: never;
url: '/agent/update_router_tool_selector';
};
export type UpdateRouterToolSelectorErrors = {
/**
* Unauthorized - invalid secret key
*/
401: unknown;
/**
* Agent not initialized
*/
424: unknown;
/**
* Internal server error
*/
500: unknown;
};
export type UpdateRouterToolSelectorResponses = {
/**
* Tool selection strategy updated successfully
*/
200: string;
};
export type UpdateRouterToolSelectorResponse = UpdateRouterToolSelectorResponses[keyof UpdateRouterToolSelectorResponses];
export type ReadAllConfigData = {
body?: never;
path?: never;
@@ -1,5 +1,4 @@
import { ModeSection } from '../mode/ModeSection';
import { ToolSelectionStrategySection } from '../tool_selection_strategy/ToolSelectionStrategySection';
import DictationSection from '../dictation/DictationSection';
import { SecurityToggle } from '../security/SecurityToggle';
import { ResponseStylesSection } from '../response_styles/ResponseStylesSection';
@@ -46,19 +45,6 @@ export default function ChatSettingsSection() {
<GoosehintsSection />
</CardContent>
</Card>
<Card className="pb-2 rounded-lg">
<CardHeader className="pb-0">
<CardTitle className="">Tool Selection Strategy (preview)</CardTitle>
<CardDescription>
Experimental: configure how Goose selects tools for your requests, useful when there are
many tools. Only tested with Claude models currently.
</CardDescription>
</CardHeader>
<CardContent className="px-2">
<ToolSelectionStrategySection />
</CardContent>
</Card>
</div>
);
}
@@ -1,145 +0,0 @@
import { useEffect, useState, useCallback } from 'react';
import { useConfig } from '../../ConfigContext';
import { getApiUrl } from '../../../config';
interface ToolSelectionStrategy {
key: boolean;
label: string;
description: string;
}
export const all_tool_selection_strategies: ToolSelectionStrategy[] = [
{
key: false,
label: 'Disabled',
description: 'Use the default tool selection strategy',
},
{
key: true,
label: 'Enabled',
description:
'Use LLM-based intelligence to select the most relevant tools based on the user query context.',
},
];
export const ToolSelectionStrategySection = () => {
const [routerEnabled, setRouterEnabled] = useState(false);
const [_error, setError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false);
const { read, upsert } = useConfig();
const handleStrategyChange = async (enableRouter: boolean) => {
if (isLoading) return; // Prevent multiple simultaneous requests
setError(null); // Clear any previous errors
setIsLoading(true);
try {
// First update the configuration
try {
await upsert('GOOSE_ENABLE_ROUTER', enableRouter.toString(), false);
} catch (error) {
console.error('Error updating configuration:', error);
setError(`Failed to update configuration: ${error}`);
setIsLoading(false);
return;
}
// Then update the backend
try {
const response = await fetch(getApiUrl('/agent/update_router_tool_selector'), {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Secret-Key': await window.electron.getSecretKey(),
},
body: JSON.stringify({
session_id: '', // TODO(jack) add the session id, or remove from this request payload
}),
});
if (!response.ok) {
const errorData = await response
.json()
.catch(() => ({ error: 'Unknown error from backend' }));
throw new Error(errorData.error || 'Unknown error from backend');
}
// Parse the success response
const data = await response
.json()
.catch(() => ({ message: 'Tool selection strategy updated successfully' }));
if (data.error) {
throw new Error(data.error);
}
} catch (error) {
console.error('Error updating backend:', error);
setError(`Failed to update backend: ${error}`);
setIsLoading(false);
return;
}
// If both succeeded, update the UI
setRouterEnabled(enableRouter);
} catch (error) {
console.error('Error updating tool selection strategy:', error);
setError(`Failed to update tool selection strategy: ${error}`);
} finally {
setIsLoading(false);
}
};
const fetchCurrentStrategy = useCallback(async () => {
try {
const strategy = (await read('GOOSE_ENABLE_ROUTER', false)) as string;
if (strategy) {
setRouterEnabled(strategy === 'true');
}
} catch (error) {
console.error('Error fetching current router setting:', error);
setError(`Failed to fetch current router setting: ${error}`);
}
}, [read]);
useEffect(() => {
fetchCurrentStrategy();
}, [fetchCurrentStrategy]);
return (
<div className="space-y-1">
{all_tool_selection_strategies.map((strategy) => (
<div className="group hover:cursor-pointer text-sm" key={strategy.key.toString()}>
<div
className={`flex items-center justify-between text-text-default py-2 px-2 ${routerEnabled === strategy.key ? 'bg-background-muted' : 'bg-background-default hover:bg-background-muted'} rounded-lg transition-all`}
onClick={() => handleStrategyChange(strategy.key)}
>
<div className="flex">
<div>
<h3 className="text-text-default">{strategy.label}</h3>
<p className="text-xs text-text-muted mt-[2px]">{strategy.description}</p>
</div>
</div>
<div className="relative flex items-center gap-2">
<input
type="radio"
name="tool-selection-strategy"
value={strategy.key.toString()}
checked={routerEnabled === strategy.key}
onChange={() => handleStrategyChange(strategy.key)}
disabled={isLoading}
className="peer sr-only"
/>
<div
className="h-4 w-4 rounded-full border border-border-default
peer-checked:border-[6px] peer-checked:border-black dark:peer-checked:border-white
peer-checked:bg-white dark:peer-checked:bg-black
transition-all duration-200 ease-in-out group-hover:border-border-default"
></div>
</div>
</div>
</div>
))}
</div>
);
};