From a800ca7d7fb60284442e17e97dbe208aee9f418e Mon Sep 17 00:00:00 2001 From: Douwe Osinga Date: Fri, 13 Feb 2026 21:32:00 +0100 Subject: [PATCH] Move platform extensions into their own folder (#7210) Co-authored-by: Douwe Osinga --- crates/goose/src/agents/agent.rs | 2 +- crates/goose/src/agents/extension.rs | 166 +---------------- crates/goose/src/agents/mod.rs | 8 +- .../apps.rs} | 2 +- .../chatrecall.rs} | 5 - .../code_execution.rs} | 0 .../ext_manager.rs} | 24 +-- .../src/agents/platform_extensions/mod.rs | 171 ++++++++++++++++++ .../summon.rs} | 0 .../todo.rs} | 0 .../tom.rs} | 0 crates/goose/src/agents/reply_parts.rs | 4 +- .../src/permission/permission_inspector.rs | 2 +- .../goose/src/permission/permission_judge.rs | 2 +- crates/goose/tests/agent.rs | 2 +- 15 files changed, 189 insertions(+), 199 deletions(-) rename crates/goose/src/agents/{apps_extension.rs => platform_extensions/apps.rs} (99%) rename crates/goose/src/agents/{chatrecall_extension.rs => platform_extensions/chatrecall.rs} (97%) rename crates/goose/src/agents/{code_execution_extension.rs => platform_extensions/code_execution.rs} (100%) rename crates/goose/src/agents/{extension_manager_extension.rs => platform_extensions/ext_manager.rs} (95%) create mode 100644 crates/goose/src/agents/platform_extensions/mod.rs rename crates/goose/src/agents/{summon_extension.rs => platform_extensions/summon.rs} (100%) rename crates/goose/src/agents/{todo_extension.rs => platform_extensions/todo.rs} (100%) rename crates/goose/src/agents/{tom_extension.rs => platform_extensions/tom.rs} (100%) diff --git a/crates/goose/src/agents/agent.rs b/crates/goose/src/agents/agent.rs index a862f83027..3cfedeb39f 100644 --- a/crates/goose/src/agents/agent.rs +++ b/crates/goose/src/agents/agent.rs @@ -15,8 +15,8 @@ use super::tool_execution::{ToolCallResult, CHAT_MODE_TOOL_SKIPPED_RESPONSE, DEC use crate::action_required_manager::ActionRequiredManager; 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_extensions::MANAGE_EXTENSIONS_TOOL_NAME_COMPLETE; use crate::agents::platform_tools::PLATFORM_MANAGE_SCHEDULE_TOOL_NAME; use crate::agents::prompt_manager::PromptManager; use crate::agents::retry::{RetryManager, RetryResult}; diff --git a/crates/goose/src/agents/extension.rs b/crates/goose/src/agents/extension.rs index ccd80324c3..5e460ba4b3 100644 --- a/crates/goose/src/agents/extension.rs +++ b/crates/goose/src/agents/extension.rs @@ -1,18 +1,9 @@ -use crate::agents::apps_extension; -use crate::agents::chatrecall_extension; -use crate::agents::code_execution_extension; -use crate::agents::extension_manager_extension; -use crate::agents::summon_extension; -use crate::agents::todo_extension; -use crate::agents::tom_extension; use std::collections::HashMap; -use crate::agents::mcp_client::McpClientTrait; use crate::config; use crate::config::extensions::name_to_key; use crate::config::permission::PermissionLevel; use crate::config::Config; -use once_cell::sync::Lazy; use rmcp::model::Tool; use rmcp::service::ClientInitializeError; use rmcp::ServiceError as ClientError; @@ -22,6 +13,10 @@ use thiserror::Error; use tracing::warn; use utoipa::ToSchema; +pub use crate::agents::platform_extensions::{ + PlatformExtensionContext, PlatformExtensionDef, PLATFORM_EXTENSIONS, +}; + #[derive(Error, Debug)] #[error("process quit before initialization: stderr = {stderr}")] pub struct ProcessExit { @@ -42,159 +37,6 @@ impl ProcessExit { } } -pub static PLATFORM_EXTENSIONS: Lazy> = Lazy::new( - || { - let mut map = HashMap::new(); - - map.insert( - todo_extension::EXTENSION_NAME, - PlatformExtensionDef { - name: todo_extension::EXTENSION_NAME, - display_name: "Todo", - description: - "Enable a todo list for goose so it can keep track of what it is doing", - default_enabled: true, - unprefixed_tools: false, - client_factory: |ctx| Box::new(todo_extension::TodoClient::new(ctx).unwrap()), - }, - ); - - map.insert( - apps_extension::EXTENSION_NAME, - PlatformExtensionDef { - name: apps_extension::EXTENSION_NAME, - display_name: "Apps", - description: - "Create and manage custom Goose apps through chat. Apps are HTML/CSS/JavaScript and run in sandboxed windows.", - default_enabled: true, - unprefixed_tools: false, - client_factory: |ctx| Box::new(apps_extension::AppsManagerClient::new(ctx).unwrap()), - }, - ); - - map.insert( - chatrecall_extension::EXTENSION_NAME, - PlatformExtensionDef { - name: chatrecall_extension::EXTENSION_NAME, - display_name: "Chat Recall", - description: - "Search past conversations and load session summaries for contextual memory", - default_enabled: false, - unprefixed_tools: false, - client_factory: |ctx| { - Box::new(chatrecall_extension::ChatRecallClient::new(ctx).unwrap()) - }, - }, - ); - - map.insert( - "extensionmanager", - PlatformExtensionDef { - name: extension_manager_extension::EXTENSION_NAME, - display_name: "Extension Manager", - description: - "Enable extension management tools for discovering, enabling, and disabling extensions", - default_enabled: true, - unprefixed_tools: false, - client_factory: |ctx| Box::new(extension_manager_extension::ExtensionManagerClient::new(ctx).unwrap()), - }, - ); - - map.insert( - summon_extension::EXTENSION_NAME, - PlatformExtensionDef { - name: summon_extension::EXTENSION_NAME, - display_name: "Summon", - description: "Load knowledge and delegate tasks to subagents", - default_enabled: true, - unprefixed_tools: true, - client_factory: |ctx| Box::new(summon_extension::SummonClient::new(ctx).unwrap()), - }, - ); - - map.insert( - code_execution_extension::EXTENSION_NAME, - PlatformExtensionDef { - name: code_execution_extension::EXTENSION_NAME, - display_name: "Code Mode", - description: - "Goose will make extension calls through code execution, saving tokens", - default_enabled: false, - unprefixed_tools: true, - client_factory: |ctx| { - Box::new(code_execution_extension::CodeExecutionClient::new(ctx).unwrap()) - }, - }, - ); - - map.insert( - tom_extension::EXTENSION_NAME, - PlatformExtensionDef { - name: tom_extension::EXTENSION_NAME, - display_name: "Top Of Mind", - description: - "Inject custom context into every turn via GOOSE_MOIM_MESSAGE_TEXT and GOOSE_MOIM_MESSAGE_FILE environment variables", - default_enabled: true, - unprefixed_tools: false, - client_factory: |ctx| Box::new(tom_extension::TomClient::new(ctx).unwrap()), - }, - ); - - map - }, -); - -#[derive(Clone)] -pub struct PlatformExtensionContext { - pub extension_manager: - Option>, - pub session_manager: std::sync::Arc, -} - -impl PlatformExtensionContext { - pub fn result_with_platform_notification( - &self, - mut result: rmcp::model::CallToolResult, - extension_name: impl Into, - event_type: impl Into, - mut additional_params: serde_json::Map, - ) -> rmcp::model::CallToolResult { - additional_params.insert("extension".to_string(), extension_name.into().into()); - additional_params.insert("event_type".to_string(), event_type.into().into()); - - let meta_value = serde_json::json!({ - "platform_notification": { - "method": "platform_event", - "params": additional_params - } - }); - - if let Some(ref mut meta) = result.meta { - if let Some(obj) = meta_value.as_object() { - for (k, v) in obj { - meta.0.insert(k.clone(), v.clone()); - } - } - } else { - result.meta = Some(rmcp::model::Meta(meta_value.as_object().unwrap().clone())); - } - - result - } -} - -/// Definition for a platform extension that runs in-process with direct agent access. -#[derive(Debug, Clone)] -pub struct PlatformExtensionDef { - pub name: &'static str, - pub display_name: &'static str, - pub description: &'static str, - pub default_enabled: bool, - /// If true, tools are exposed without extension prefix for intuitive first-class use. - pub unprefixed_tools: bool, - pub client_factory: fn(PlatformExtensionContext) -> Box, -} - /// Errors from Extension operation #[derive(Error, Debug)] pub enum ExtensionError { diff --git a/crates/goose/src/agents/mod.rs b/crates/goose/src/agents/mod.rs index 07cd370ce7..268bfe2903 100644 --- a/crates/goose/src/agents/mod.rs +++ b/crates/goose/src/agents/mod.rs @@ -1,18 +1,15 @@ mod agent; -pub(crate) mod apps_extension; pub(crate) mod builtin_skills; -pub(crate) mod chatrecall_extension; -pub(crate) mod code_execution_extension; pub mod container; pub mod execute_commands; pub mod extension; pub mod extension_malware_check; pub mod extension_manager; -pub mod extension_manager_extension; pub mod final_output_tool; mod large_response_handler; pub mod mcp_client; pub mod moim; +pub mod platform_extensions; pub mod platform_tools; pub mod prompt_manager; mod reply_parts; @@ -21,9 +18,6 @@ mod schedule_tool; pub mod subagent_execution_tool; pub(crate) mod subagent_handler; pub(crate) mod subagent_task_config; -pub(crate) mod summon_extension; -pub(crate) mod todo_extension; -pub(crate) mod tom_extension; mod tool_execution; pub mod types; diff --git a/crates/goose/src/agents/apps_extension.rs b/crates/goose/src/agents/platform_extensions/apps.rs similarity index 99% rename from crates/goose/src/agents/apps_extension.rs rename to crates/goose/src/agents/platform_extensions/apps.rs index a06599e385..e68c3ec876 100644 --- a/crates/goose/src/agents/apps_extension.rs +++ b/crates/goose/src/agents/platform_extensions/apps.rs @@ -150,7 +150,7 @@ impl AppsManagerClient { fn ensure_default_apps(&self) -> Result<(), String> { // TODO(Douwe): we have the same check in cache, consider unifying that - const CLOCK_HTML: &str = include_str!("../goose_apps/clock.html"); + const CLOCK_HTML: &str = include_str!("../../goose_apps/clock.html"); // Check if clock app exists let clock_path = self.apps_dir.join("clock.html"); diff --git a/crates/goose/src/agents/chatrecall_extension.rs b/crates/goose/src/agents/platform_extensions/chatrecall.rs similarity index 97% rename from crates/goose/src/agents/chatrecall_extension.rs rename to crates/goose/src/agents/platform_extensions/chatrecall.rs index 1842a89d53..017b36478a 100644 --- a/crates/goose/src/agents/chatrecall_extension.rs +++ b/crates/goose/src/agents/platform_extensions/chatrecall.rs @@ -13,7 +13,6 @@ use tokio_util::sync::CancellationToken; pub static EXTENSION_NAME: &str = "chatrecall"; -/// Parameters for the chatrecall tool #[derive(Debug, Serialize, Deserialize, JsonSchema)] struct ChatRecallParams { /// Search keywords. Use multiple related terms/synonyms (e.g., 'database postgres sql'). Mutually exclusive with session_id. @@ -120,7 +119,6 @@ impl ChatRecallClient { total ); - // Show first 3 messages let first_count = std::cmp::min(3, total); output.push_str("--- First Few Messages ---\n\n"); for (idx, msg) in msgs.iter().take(first_count).enumerate() { @@ -134,7 +132,6 @@ impl ChatRecallClient { output.push('\n'); } - // Show last 3 messages (if different from first) if total > first_count { output.push_str("--- Last Few Messages ---\n\n"); let last_count = std::cmp::min(3, total); @@ -186,7 +183,6 @@ impl ChatRecallClient { .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok()) .map(|dt| dt.with_timezone(&chrono::Utc)); - // Exclude current session from results to avoid self-referential loops let exclude_session_id = Some(current_session_id.to_string()); match self @@ -248,7 +244,6 @@ impl ChatRecallClient { } fn get_tools() -> Vec { - // Generate JSON schema from the ChatRecallParams struct let schema = schema_for!(ChatRecallParams); let schema_value = serde_json::to_value(schema).expect("Failed to serialize ChatRecallParams schema"); diff --git a/crates/goose/src/agents/code_execution_extension.rs b/crates/goose/src/agents/platform_extensions/code_execution.rs similarity index 100% rename from crates/goose/src/agents/code_execution_extension.rs rename to crates/goose/src/agents/platform_extensions/code_execution.rs diff --git a/crates/goose/src/agents/extension_manager_extension.rs b/crates/goose/src/agents/platform_extensions/ext_manager.rs similarity index 95% rename from crates/goose/src/agents/extension_manager_extension.rs rename to crates/goose/src/agents/platform_extensions/ext_manager.rs index 635275d87c..d6b317e20b 100644 --- a/crates/goose/src/agents/extension_manager_extension.rs +++ b/crates/goose/src/agents/platform_extensions/ext_manager.rs @@ -16,10 +16,8 @@ use serde_json::Value; use std::sync::Arc; use tokio::sync::mpsc; use tokio_util::sync::CancellationToken; -use tracing::error; pub static EXTENSION_NAME: &str = "Extension Manager"; -// pub static DISPLAY_NAME: &str = "Extension Manager"; #[derive(Debug, thiserror::Error)] pub enum ExtensionManagerToolError { @@ -32,9 +30,6 @@ pub enum ExtensionManagerToolError { #[error("Missing required parameter: {param_name}")] MissingParameter { param_name: String }, - #[error("Invalid action: {action}. Must be 'enable' or 'disable'")] - InvalidAction { action: String }, - #[error("Extension operation failed: {message}")] OperationFailed { message: String }, @@ -336,7 +331,6 @@ impl ExtensionManagerClient { }), ]; - // Only add resource tools if extension manager supports resources if let Some(weak_ref) = &self.context.extension_manager { if let Some(extension_manager) = weak_ref.upgrade() { if extension_manager.supports_resources().await { @@ -454,18 +448,12 @@ impl McpClientTrait for ExtensionManagerClient { match result { Ok(content) => Ok(CallToolResult::success(content)), - Err(error) => { - // Log the error for debugging - error!("Extension manager tool '{}' failed: {}", name, error); - - // Return proper error result with is_error flag set - Ok(CallToolResult { - content: vec![Content::text(error.to_string())], - is_error: Some(true), // ✅ Properly mark as error - structured_content: None, - meta: None, - }) - } + Err(error) => Ok(CallToolResult { + content: vec![Content::text(error.to_string())], + is_error: Some(true), + structured_content: None, + meta: None, + }), } } diff --git a/crates/goose/src/agents/platform_extensions/mod.rs b/crates/goose/src/agents/platform_extensions/mod.rs new file mode 100644 index 0000000000..6a176b4f16 --- /dev/null +++ b/crates/goose/src/agents/platform_extensions/mod.rs @@ -0,0 +1,171 @@ +pub mod apps; +pub mod chatrecall; +pub mod code_execution; +pub mod ext_manager; +pub mod summon; +pub mod todo; +pub mod tom; + +use std::collections::HashMap; + +use crate::agents::mcp_client::McpClientTrait; +use once_cell::sync::Lazy; + +pub use ext_manager::MANAGE_EXTENSIONS_TOOL_NAME_COMPLETE; + +// These are used by integration tests in crates/goose/tests/ +#[allow(unused_imports)] +pub use ext_manager::MANAGE_EXTENSIONS_TOOL_NAME; +#[allow(unused_imports)] +pub use ext_manager::SEARCH_AVAILABLE_EXTENSIONS_TOOL_NAME; + +pub static PLATFORM_EXTENSIONS: Lazy> = Lazy::new( + || { + let mut map = HashMap::new(); + + map.insert( + todo::EXTENSION_NAME, + PlatformExtensionDef { + name: todo::EXTENSION_NAME, + display_name: "Todo", + description: + "Enable a todo list for goose so it can keep track of what it is doing", + default_enabled: true, + unprefixed_tools: false, + client_factory: |ctx| Box::new(todo::TodoClient::new(ctx).unwrap()), + }, + ); + + map.insert( + apps::EXTENSION_NAME, + PlatformExtensionDef { + name: apps::EXTENSION_NAME, + display_name: "Apps", + description: + "Create and manage custom Goose apps through chat. Apps are HTML/CSS/JavaScript and run in sandboxed windows.", + default_enabled: true, + unprefixed_tools: false, + client_factory: |ctx| Box::new(apps::AppsManagerClient::new(ctx).unwrap()), + }, + ); + + map.insert( + chatrecall::EXTENSION_NAME, + PlatformExtensionDef { + name: chatrecall::EXTENSION_NAME, + display_name: "Chat Recall", + description: + "Search past conversations and load session summaries for contextual memory", + default_enabled: false, + unprefixed_tools: false, + client_factory: |ctx| Box::new(chatrecall::ChatRecallClient::new(ctx).unwrap()), + }, + ); + + map.insert( + "extensionmanager", + PlatformExtensionDef { + name: ext_manager::EXTENSION_NAME, + display_name: "Extension Manager", + description: + "Enable extension management tools for discovering, enabling, and disabling extensions", + default_enabled: true, + unprefixed_tools: false, + client_factory: |ctx| Box::new(ext_manager::ExtensionManagerClient::new(ctx).unwrap()), + }, + ); + + map.insert( + summon::EXTENSION_NAME, + PlatformExtensionDef { + name: summon::EXTENSION_NAME, + display_name: "Summon", + description: "Load knowledge and delegate tasks to subagents", + default_enabled: true, + unprefixed_tools: true, + client_factory: |ctx| Box::new(summon::SummonClient::new(ctx).unwrap()), + }, + ); + + map.insert( + code_execution::EXTENSION_NAME, + PlatformExtensionDef { + name: code_execution::EXTENSION_NAME, + display_name: "Code Mode", + description: + "Goose will make extension calls through code execution, saving tokens", + default_enabled: false, + unprefixed_tools: true, + client_factory: |ctx| { + Box::new(code_execution::CodeExecutionClient::new(ctx).unwrap()) + }, + }, + ); + + map.insert( + tom::EXTENSION_NAME, + PlatformExtensionDef { + name: tom::EXTENSION_NAME, + display_name: "Top Of Mind", + description: + "Inject custom context into every turn via GOOSE_MOIM_MESSAGE_TEXT and GOOSE_MOIM_MESSAGE_FILE environment variables", + default_enabled: true, + unprefixed_tools: false, + client_factory: |ctx| Box::new(tom::TomClient::new(ctx).unwrap()), + }, + ); + + map + }, +); + +#[derive(Clone)] +pub struct PlatformExtensionContext { + pub extension_manager: + Option>, + pub session_manager: std::sync::Arc, +} + +impl PlatformExtensionContext { + pub fn result_with_platform_notification( + &self, + mut result: rmcp::model::CallToolResult, + extension_name: impl Into, + event_type: impl Into, + mut additional_params: serde_json::Map, + ) -> rmcp::model::CallToolResult { + additional_params.insert("extension".to_string(), extension_name.into().into()); + additional_params.insert("event_type".to_string(), event_type.into().into()); + + let meta_value = serde_json::json!({ + "platform_notification": { + "method": "platform_event", + "params": additional_params + } + }); + + if let Some(ref mut meta) = result.meta { + if let Some(obj) = meta_value.as_object() { + for (k, v) in obj { + meta.0.insert(k.clone(), v.clone()); + } + } + } else { + result.meta = Some(rmcp::model::Meta(meta_value.as_object().unwrap().clone())); + } + + result + } +} + +/// Definition for a platform extension that runs in-process with direct agent access. +#[derive(Debug, Clone)] +pub struct PlatformExtensionDef { + pub name: &'static str, + pub display_name: &'static str, + pub description: &'static str, + pub default_enabled: bool, + /// If true, tools are exposed without extension prefix for intuitive first-class use. + pub unprefixed_tools: bool, + pub client_factory: fn(PlatformExtensionContext) -> Box, +} diff --git a/crates/goose/src/agents/summon_extension.rs b/crates/goose/src/agents/platform_extensions/summon.rs similarity index 100% rename from crates/goose/src/agents/summon_extension.rs rename to crates/goose/src/agents/platform_extensions/summon.rs diff --git a/crates/goose/src/agents/todo_extension.rs b/crates/goose/src/agents/platform_extensions/todo.rs similarity index 100% rename from crates/goose/src/agents/todo_extension.rs rename to crates/goose/src/agents/platform_extensions/todo.rs diff --git a/crates/goose/src/agents/tom_extension.rs b/crates/goose/src/agents/platform_extensions/tom.rs similarity index 100% rename from crates/goose/src/agents/tom_extension.rs rename to crates/goose/src/agents/platform_extensions/tom.rs diff --git a/crates/goose/src/agents/reply_parts.rs b/crates/goose/src/agents/reply_parts.rs index 765f019e5b..54e6d81ae8 100644 --- a/crates/goose/src/agents/reply_parts.rs +++ b/crates/goose/src/agents/reply_parts.rs @@ -8,7 +8,7 @@ use serde_json::{json, Value}; use tracing::debug; use super::super::agents::Agent; -use crate::agents::code_execution_extension::EXTENSION_NAME as CODE_EXECUTION_EXTENSION; +use crate::agents::platform_extensions::code_execution; use crate::conversation::message::{Message, MessageContent, ToolRequest}; use crate::conversation::Conversation; use crate::providers::base::{stream_from_single_message, MessageStream, Provider, ProviderUsage}; @@ -146,7 +146,7 @@ impl Agent { let code_execution_active = self .extension_manager - .is_extension_enabled(CODE_EXECUTION_EXTENSION) + .is_extension_enabled(code_execution::EXTENSION_NAME) .await; if code_execution_active { tools.retain(|tool| { diff --git a/crates/goose/src/permission/permission_inspector.rs b/crates/goose/src/permission/permission_inspector.rs index a04d81768a..8510d6fc33 100644 --- a/crates/goose/src/permission/permission_inspector.rs +++ b/crates/goose/src/permission/permission_inspector.rs @@ -1,4 +1,4 @@ -use crate::agents::extension_manager_extension::MANAGE_EXTENSIONS_TOOL_NAME_COMPLETE; +use crate::agents::platform_extensions::MANAGE_EXTENSIONS_TOOL_NAME_COMPLETE; use crate::config::permission::PermissionLevel; use crate::config::{GooseMode, PermissionManager}; use crate::conversation::message::{Message, ToolRequest}; diff --git a/crates/goose/src/permission/permission_judge.rs b/crates/goose/src/permission/permission_judge.rs index 99e415e829..d6f848bdf7 100644 --- a/crates/goose/src/permission/permission_judge.rs +++ b/crates/goose/src/permission/permission_judge.rs @@ -1,4 +1,4 @@ -use crate::agents::extension_manager_extension::MANAGE_EXTENSIONS_TOOL_NAME_COMPLETE; +use crate::agents::platform_extensions::MANAGE_EXTENSIONS_TOOL_NAME_COMPLETE; use crate::config::permission::PermissionLevel; use crate::config::PermissionManager; use crate::conversation::message::{Message, MessageContent, ToolRequest}; diff --git a/crates/goose/tests/agent.rs b/crates/goose/tests/agent.rs index 8db987786c..a6817a3cc5 100644 --- a/crates/goose/tests/agent.rs +++ b/crates/goose/tests/agent.rs @@ -510,7 +510,7 @@ mod tests { mod extension_manager_tests { use super::*; use goose::agents::extension::ExtensionConfig; - use goose::agents::extension_manager_extension::{ + use goose::agents::platform_extensions::{ MANAGE_EXTENSIONS_TOOL_NAME, SEARCH_AVAILABLE_EXTENSIONS_TOOL_NAME, }; use goose::agents::AgentConfig;