diff --git a/crates/goose/src/agents/extension.rs b/crates/goose/src/agents/extension.rs index 8e04692490..347c93f73f 100644 --- a/crates/goose/src/agents/extension.rs +++ b/crates/goose/src/agents/extension.rs @@ -1,6 +1,7 @@ use crate::agents::chatrecall_extension; use crate::agents::code_execution_extension; use crate::agents::extension_manager_extension; +use crate::agents::oracle_extension; use crate::agents::skills_extension; use crate::agents::todo_extension; use std::collections::HashMap; @@ -100,6 +101,17 @@ pub static PLATFORM_EXTENSIONS: Lazy }, ); + map.insert( + oracle_extension::EXTENSION_NAME, + PlatformExtensionDef { + name: oracle_extension::EXTENSION_NAME, + description: + "A powerful reasoning tool that provides a second opinion for complex problems", + default_enabled: false, + client_factory: |ctx| Box::new(oracle_extension::OracleClient::new(ctx).unwrap()), + }, + ); + map }, ); @@ -109,6 +121,7 @@ pub struct PlatformExtensionContext { pub extension_manager: Option>, pub session_manager: std::sync::Arc, + pub provider: Option, } #[derive(Debug, Clone)] diff --git a/crates/goose/src/agents/extension_manager.rs b/crates/goose/src/agents/extension_manager.rs index 27cc65060e..feee52e368 100644 --- a/crates/goose/src/agents/extension_manager.rs +++ b/crates/goose/src/agents/extension_manager.rs @@ -449,6 +449,7 @@ impl ExtensionManager { context: PlatformExtensionContext { extension_manager: None, session_manager, + provider: None, }, provider, tools_cache: Mutex::new(None), @@ -576,6 +577,7 @@ impl ExtensionManager { })?; let mut context = self.context.clone(); context.extension_manager = Some(Arc::downgrade(self)); + context.provider = Some(self.provider.clone()); (def.client_factory)(context) } ExtensionConfig::InlinePython { diff --git a/crates/goose/src/agents/mod.rs b/crates/goose/src/agents/mod.rs index c6bf9706ee..d31cc637a9 100644 --- a/crates/goose/src/agents/mod.rs +++ b/crates/goose/src/agents/mod.rs @@ -10,6 +10,7 @@ pub mod final_output_tool; mod large_response_handler; pub mod mcp_client; pub mod moim; +pub(crate) mod oracle_extension; pub mod platform_tools; pub mod prompt_manager; mod reply_parts; diff --git a/crates/goose/src/agents/oracle_extension.rs b/crates/goose/src/agents/oracle_extension.rs new file mode 100644 index 0000000000..9570c66bfb --- /dev/null +++ b/crates/goose/src/agents/oracle_extension.rs @@ -0,0 +1,246 @@ +use crate::agents::extension::PlatformExtensionContext; +use crate::agents::mcp_client::{Error, McpClientTrait, McpMeta}; +use crate::providers::base::Provider; +use anyhow::Result; +use async_trait::async_trait; +use indoc::indoc; +use rmcp::model::{ + CallToolResult, Content, Implementation, InitializeResult, JsonObject, ListToolsResult, + ProtocolVersion, ServerCapabilities, Tool, ToolAnnotations, ToolsCapability, +}; +use schemars::{schema_for, JsonSchema}; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use tokio::sync::OnceCell; +use tokio_util::sync::CancellationToken; + +pub static EXTENSION_NAME: &str = "oracle"; + +const DEFAULT_ORACLE_PROVIDER: &str = "openai"; +const DEFAULT_ORACLE_MODEL: &str = "gpt-5.2"; + +#[derive(Debug, Serialize, Deserialize, JsonSchema)] +struct ThinkParams { + /// The problem, question, or situation to think deeply about + problem: String, + /// Optional context or background information relevant to the problem + #[serde(default)] + context: Option, +} + +pub struct OracleClient { + info: InitializeResult, + provider: OnceCell>, +} + +impl OracleClient { + pub fn new(_context: PlatformExtensionContext) -> Result { + let info = InitializeResult { + protocol_version: ProtocolVersion::V_2025_03_26, + capabilities: ServerCapabilities { + tasks: None, + tools: Some(ToolsCapability { + list_changed: Some(false), + }), + resources: None, + prompts: None, + completions: None, + experimental: None, + logging: None, + }, + server_info: Implementation { + name: EXTENSION_NAME.to_string(), + title: Some("Oracle".to_string()), + version: "1.0.0".to_string(), + icons: None, + website_url: None, + }, + instructions: Some( + indoc! {r#" + The Oracle is a powerful reasoning tool that provides a "second opinion" + for complex problems. It uses a different model to give + you a fresh perspective. + + *critical* when starting a session or a new task always consult the oracle to think and come up with a plan + + Uses: + - Architectural decisions and code review + - Planning multi-step implementations + - Getting a fresh perspective on stuck problems + "#} + .to_string(), + ), + }; + + Ok(Self { + info, + provider: OnceCell::new(), + }) + } + + async fn get_or_init_provider(&self) -> Result<&Arc, String> { + self.provider + .get_or_try_init(|| async { + let provider_name = std::env::var("GOOSE_ORACLE_PROVIDER") + .unwrap_or_else(|_| DEFAULT_ORACLE_PROVIDER.to_string()); + let model_name = std::env::var("GOOSE_ORACLE_MODEL") + .unwrap_or_else(|_| DEFAULT_ORACLE_MODEL.to_string()); + + tracing::info!( + "Initializing Oracle with provider: {}, model: {}", + provider_name, + model_name + ); + + crate::providers::create_with_named_model(&provider_name, &model_name) + .await + .map_err(|e| { + anyhow::anyhow!( + "Failed to create Oracle provider ({}:{}): {}. \ + Make sure the provider is configured (e.g., OPENAI_API_KEY is set).", + provider_name, + model_name, + e + ) + }) + }) + .await + .map_err(|e| e.to_string()) + } + + async fn handle_think(&self, arguments: Option) -> Result, String> { + let args = arguments.as_ref().ok_or("Missing arguments")?; + + let problem = args + .get("problem") + .and_then(|v| v.as_str()) + .ok_or("Missing required parameter: problem")?; + + let context = args + .get("context") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + + let provider = self.get_or_init_provider().await?; + + let system_prompt = indoc! {r#" + You are the Oracle - a deep reasoning assistant that helps analyze complex problems. + + Your role is to: + 1. Carefully analyze the problem presented + 2. Consider multiple angles and potential solutions + 3. Identify edge cases, risks, and potential issues + 4. Provide clear, actionable recommendations + 5. Think step by step through complex logic + + Be thorough but concise. Focus on insights that might be missed on first glance. + If you see potential bugs, architectural issues, or better approaches, highlight them. + "#}; + + let user_message = if let Some(ctx) = context { + format!( + "## Problem\n{}\n\n## Context\n{}\n\nPlease analyze this thoroughly and provide your insights.", + problem, ctx + ) + } else { + format!( + "## Problem\n{}\n\nPlease analyze this thoroughly and provide your insights.", + problem + ) + }; + + let messages = vec![crate::conversation::message::Message::user().with_text(&user_message)]; + + let (response, _usage) = provider + .complete(system_prompt, &messages, &[]) + .await + .map_err(|e| format!("Oracle failed to think: {}", e))?; + + let response_text = response + .content + .iter() + .filter_map(|c| { + if let crate::conversation::message::MessageContent::Text(t) = c { + Some(t.text.clone()) + } else { + None + } + }) + .collect::>() + .join("\n"); + + Ok(vec![Content::text(response_text)]) + } + + fn get_tools() -> Vec { + let schema = schema_for!(ThinkParams); + let schema_value = + serde_json::to_value(schema).expect("Failed to serialize ThinkParams schema"); + + vec![Tool::new( + "think".to_string(), + indoc! {r#" + Ask the Oracle to think deeply about a problem. + + Use this tool when you need: + - A second opinion on complex code or architecture + - To plan a multi-step implementation + - A fresh perspective when you're stuck + + The Oracle uses a powerful reasoning model to provide + thorough analysis from a different perspective than the main agent. + + *critical* always use the the think tool first in a session or new task to think and plan + "#} + .to_string(), + schema_value.as_object().unwrap().clone(), + ) + .annotate(ToolAnnotations { + title: Some("Think".to_string()), + read_only_hint: Some(true), + destructive_hint: Some(false), + idempotent_hint: Some(true), + open_world_hint: Some(false), + })] + } +} + +#[async_trait] +impl McpClientTrait for OracleClient { + async fn list_tools( + &self, + _next_cursor: Option, + _cancellation_token: CancellationToken, + ) -> Result { + Ok(ListToolsResult { + tools: Self::get_tools(), + next_cursor: None, + meta: None, + }) + } + + async fn call_tool( + &self, + name: &str, + arguments: Option, + _meta: McpMeta, + _cancellation_token: CancellationToken, + ) -> Result { + let content = match name { + "think" => self.handle_think(arguments).await, + _ => Err(format!("Unknown tool: {}", name)), + }; + + match content { + Ok(content) => Ok(CallToolResult::success(content)), + Err(error) => Ok(CallToolResult::error(vec![Content::text(format!( + "Error: {}", + error + ))])), + } + } + + fn get_info(&self) -> Option<&InitializeResult> { + Some(&self.info) + } +}