Move platform extensions into their own folder (#7210)

Co-authored-by: Douwe Osinga <douwe@squareup.com>
This commit is contained in:
Douwe Osinga
2026-02-13 21:32:00 +01:00
committed by GitHub
parent 58d3431bf5
commit a800ca7d7f
15 changed files with 189 additions and 199 deletions
+1 -1
View File
@@ -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};
+4 -162
View File
@@ -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<HashMap<&'static str, PlatformExtensionDef>> = 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<std::sync::Weak<crate::agents::extension_manager::ExtensionManager>>,
pub session_manager: std::sync::Arc<crate::session::SessionManager>,
}
impl PlatformExtensionContext {
pub fn result_with_platform_notification(
&self,
mut result: rmcp::model::CallToolResult,
extension_name: impl Into<String>,
event_type: impl Into<String>,
mut additional_params: serde_json::Map<String, serde_json::Value>,
) -> 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<dyn McpClientTrait>,
}
/// Errors from Extension operation
#[derive(Error, Debug)]
pub enum ExtensionError {
+1 -7
View File
@@ -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;
@@ -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");
@@ -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<Tool> {
// 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");
@@ -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,
}),
}
}
@@ -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<HashMap<&'static str, PlatformExtensionDef>> = 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<std::sync::Weak<crate::agents::extension_manager::ExtensionManager>>,
pub session_manager: std::sync::Arc<crate::session::SessionManager>,
}
impl PlatformExtensionContext {
pub fn result_with_platform_notification(
&self,
mut result: rmcp::model::CallToolResult,
extension_name: impl Into<String>,
event_type: impl Into<String>,
mut additional_params: serde_json::Map<String, serde_json::Value>,
) -> 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<dyn McpClientTrait>,
}
+2 -2
View File
@@ -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| {
@@ -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};
@@ -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};
+1 -1
View File
@@ -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;