use std::collections::HashMap; use std::future::Future; use std::pin::Pin; use std::sync::Arc; use anyhow::{anyhow, Result}; use futures::stream::BoxStream; use futures::{FutureExt, Stream, TryStreamExt}; use futures_util::stream; use futures_util::stream::StreamExt; use mcp_core::protocol::JsonRpcMessage; use crate::config::{Config, ExtensionConfigManager, PermissionManager}; use crate::message::Message; use crate::permission::permission_judge::check_tool_permissions; use crate::permission::PermissionConfirmation; use crate::providers::base::Provider; use crate::providers::errors::ProviderError; use crate::recipe::{Author, Recipe, Settings}; use crate::scheduler_trait::SchedulerTrait; use crate::tool_monitor::{ToolCall, ToolMonitor}; use regex::Regex; use serde_json::Value; use tokio::sync::{mpsc, Mutex}; use tracing::{debug, error, instrument}; use crate::agents::extension::{ExtensionConfig, ExtensionError, ExtensionResult, ToolInfo}; use crate::agents::extension_manager::{get_parameter_names, ExtensionManager}; use crate::agents::platform_tools::{ PLATFORM_LIST_RESOURCES_TOOL_NAME, PLATFORM_MANAGE_EXTENSIONS_TOOL_NAME, PLATFORM_MANAGE_SCHEDULE_TOOL_NAME, PLATFORM_READ_RESOURCE_TOOL_NAME, PLATFORM_SEARCH_AVAILABLE_EXTENSIONS_TOOL_NAME, }; use crate::agents::prompt_manager::PromptManager; use crate::agents::router_tool_selector::{ create_tool_selector, RouterToolSelectionStrategy, RouterToolSelector, }; use crate::agents::router_tools::{ROUTER_LLM_SEARCH_TOOL_NAME, ROUTER_VECTOR_SEARCH_TOOL_NAME}; use crate::agents::tool_router_index_manager::ToolRouterIndexManager; use crate::agents::tool_vectordb::generate_table_id; use crate::agents::types::SessionConfig; use crate::agents::types::{FrontendTool, ToolResultReceiver}; use mcp_core::{ prompt::Prompt, protocol::GetPromptResult, tool::Tool, Content, ToolError, ToolResult, }; use super::platform_tools; use super::router_tools; use super::tool_execution::{ToolCallResult, CHAT_MODE_TOOL_SKIPPED_RESPONSE, DECLINED_RESPONSE}; /// The main goose Agent pub struct Agent { pub(super) provider: Mutex>>, pub(super) extension_manager: Mutex, pub(super) frontend_tools: Mutex>, pub(super) frontend_instructions: Mutex>, pub(super) prompt_manager: Mutex, pub(super) confirmation_tx: mpsc::Sender<(String, PermissionConfirmation)>, pub(super) confirmation_rx: Mutex>, pub(super) tool_result_tx: mpsc::Sender<(String, ToolResult>)>, pub(super) tool_result_rx: ToolResultReceiver, pub(super) tool_monitor: Mutex>, pub(super) router_tool_selector: Mutex>>>, pub(super) scheduler_service: Mutex>>, } #[derive(Clone, Debug)] pub enum AgentEvent { Message(Message), McpNotification((String, JsonRpcMessage)), ModelChange { model: String, mode: String }, } impl Agent { pub fn new() -> Self { // Create channels with buffer size 32 (adjust if needed) let (confirm_tx, confirm_rx) = mpsc::channel(32); let (tool_tx, tool_rx) = mpsc::channel(32); Self { provider: Mutex::new(None), extension_manager: Mutex::new(ExtensionManager::new()), frontend_tools: Mutex::new(HashMap::new()), frontend_instructions: Mutex::new(None), prompt_manager: Mutex::new(PromptManager::new()), confirmation_tx: confirm_tx, confirmation_rx: Mutex::new(confirm_rx), tool_result_tx: tool_tx, tool_result_rx: Arc::new(Mutex::new(tool_rx)), tool_monitor: Mutex::new(None), router_tool_selector: Mutex::new(None), scheduler_service: Mutex::new(None), } } pub async fn configure_tool_monitor(&self, max_repetitions: Option) { let mut tool_monitor = self.tool_monitor.lock().await; *tool_monitor = Some(ToolMonitor::new(max_repetitions)); } pub async fn get_tool_stats(&self) -> Option> { let tool_monitor = self.tool_monitor.lock().await; tool_monitor.as_ref().map(|monitor| monitor.get_stats()) } pub async fn reset_tool_monitor(&self) { if let Some(monitor) = self.tool_monitor.lock().await.as_mut() { monitor.reset(); } } /// Set the scheduler service for this agent pub async fn set_scheduler(&self, scheduler: Arc) { let mut scheduler_service = self.scheduler_service.lock().await; *scheduler_service = Some(scheduler); } } impl Default for Agent { fn default() -> Self { Self::new() } } pub enum ToolStreamItem { Message(JsonRpcMessage), Result(T), } pub type ToolStream = Pin>>> + Send>>; // tool_stream combines a stream of JsonRpcMessages with a future representing the // final result of the tool call. MCP notifications are not request-scoped, but // this lets us capture all notifications emitted during the tool call for // simpler consumption pub fn tool_stream(rx: S, done: F) -> ToolStream where S: Stream + Send + Unpin + 'static, F: Future>> + Send + 'static, { Box::pin(async_stream::stream! { tokio::pin!(done); let mut rx = rx; loop { tokio::select! { Some(msg) = rx.next() => { yield ToolStreamItem::Message(msg); } r = &mut done => { yield ToolStreamItem::Result(r); break; } } } }) } impl Agent { /// Get a reference count clone to the provider pub async fn provider(&self) -> Result, anyhow::Error> { match &*self.provider.lock().await { Some(provider) => Ok(Arc::clone(provider)), None => Err(anyhow!("Provider not set")), } } /// Check if a tool is a frontend tool pub async fn is_frontend_tool(&self, name: &str) -> bool { self.frontend_tools.lock().await.contains_key(name) } /// Get a reference to a frontend tool pub async fn get_frontend_tool(&self, name: &str) -> Option { self.frontend_tools.lock().await.get(name).cloned() } /// Get all tools from all clients with proper prefixing pub async fn get_prefixed_tools(&self) -> ExtensionResult> { let mut tools = self .extension_manager .lock() .await .get_prefixed_tools(None) .await?; // Add frontend tools directly - they don't need prefixing since they're already uniquely named let frontend_tools = self.frontend_tools.lock().await; for frontend_tool in frontend_tools.values() { tools.push(frontend_tool.tool.clone()); } Ok(tools) } /// Dispatch a single tool call to the appropriate client #[instrument(skip(self, tool_call, request_id), fields(input, output))] pub async fn dispatch_tool_call( &self, tool_call: mcp_core::tool::ToolCall, request_id: String, ) -> (String, Result) { // Check if this tool call should be allowed based on repetition monitoring if let Some(monitor) = self.tool_monitor.lock().await.as_mut() { let tool_call_info = ToolCall::new(tool_call.name.clone(), tool_call.arguments.clone()); if !monitor.check_tool_call(tool_call_info) { return ( request_id, Err(ToolError::ExecutionError( "Tool call rejected: exceeded maximum allowed repetitions".to_string(), )), ); } } if tool_call.name == PLATFORM_MANAGE_SCHEDULE_TOOL_NAME { let result = self .handle_schedule_management(tool_call.arguments, request_id.clone()) .await; return (request_id, Ok(ToolCallResult::from(result))); } if tool_call.name == PLATFORM_MANAGE_EXTENSIONS_TOOL_NAME { let extension_name = tool_call .arguments .get("extension_name") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(); let action = tool_call .arguments .get("action") .and_then(|v| v.as_str()) .unwrap_or("") .to_string(); let (request_id, result) = self .manage_extensions(action, extension_name, request_id) .await; return (request_id, Ok(ToolCallResult::from(result))); } let extension_manager = self.extension_manager.lock().await; let result: ToolCallResult = if tool_call.name == PLATFORM_READ_RESOURCE_TOOL_NAME { // Check if the tool is read_resource and handle it separately ToolCallResult::from( extension_manager .read_resource(tool_call.arguments.clone()) .await, ) } else if tool_call.name == PLATFORM_LIST_RESOURCES_TOOL_NAME { ToolCallResult::from( extension_manager .list_resources(tool_call.arguments.clone()) .await, ) } else if tool_call.name == PLATFORM_SEARCH_AVAILABLE_EXTENSIONS_TOOL_NAME { ToolCallResult::from(extension_manager.search_available_extensions().await) } else if self.is_frontend_tool(&tool_call.name).await { // For frontend tools, return an error indicating we need frontend execution ToolCallResult::from(Err(ToolError::ExecutionError( "Frontend tool execution required".to_string(), ))) } else if tool_call.name == ROUTER_VECTOR_SEARCH_TOOL_NAME || tool_call.name == ROUTER_LLM_SEARCH_TOOL_NAME { let selector = self.router_tool_selector.lock().await.clone(); let selected_tools = match selector.as_ref() { Some(selector) => match selector.select_tools(tool_call.arguments.clone()).await { Ok(tools) => tools, Err(e) => { return ( request_id, Err(ToolError::ExecutionError(format!( "Failed to select tools: {}", e ))), ) } }, None => { return ( request_id, Err(ToolError::ExecutionError( "No tool selector available".to_string(), )), ) } }; ToolCallResult::from(Ok(selected_tools)) } else { // Clone the result to ensure no references to extension_manager are returned let result = extension_manager .dispatch_tool_call(tool_call.clone()) .await; match result { Ok(call_result) => call_result, Err(e) => ToolCallResult::from(Err(ToolError::ExecutionError(e.to_string()))), } }; ( request_id, Ok(ToolCallResult { notification_stream: result.notification_stream, result: Box::new( result .result .map(super::large_response_handler::process_tool_response), ), }), ) } pub(super) async fn manage_extensions( &self, action: String, extension_name: String, request_id: String, ) -> (String, Result, ToolError>) { let mut extension_manager = self.extension_manager.lock().await; let selector = self.router_tool_selector.lock().await.clone(); if ToolRouterIndexManager::is_tool_router_enabled(&selector) { if let Some(selector) = selector { let selector_action = if action == "disable" { "remove" } else { "add" }; let extension_manager = self.extension_manager.lock().await; let selector = Arc::new(selector); if let Err(e) = ToolRouterIndexManager::update_extension_tools( &selector, &extension_manager, &extension_name, selector_action, ) .await { return ( request_id, Err(ToolError::ExecutionError(format!( "Failed to update vector index: {}", e ))), ); } } } if action == "disable" { let result = extension_manager .remove_extension(&extension_name) .await .map(|_| { vec![Content::text(format!( "The extension '{}' has been disabled successfully", extension_name ))] }) .map_err(|e| ToolError::ExecutionError(e.to_string())); return (request_id, result); } let config = match ExtensionConfigManager::get_config_by_name(&extension_name) { Ok(Some(config)) => config, Ok(None) => { return ( request_id, Err(ToolError::ExecutionError(format!( "Extension '{}' not found. Please check the extension name and try again.", extension_name ))), ) } Err(e) => { return ( request_id, Err(ToolError::ExecutionError(format!( "Failed to get extension config: {}", e ))), ) } }; let result = extension_manager .add_extension(config) .await .map(|_| { vec![Content::text(format!( "The extension '{}' has been installed successfully", extension_name ))] }) .map_err(|e| ToolError::ExecutionError(e.to_string())); (request_id, result) } pub async fn add_extension(&self, extension: ExtensionConfig) -> ExtensionResult<()> { match &extension { ExtensionConfig::Frontend { name: _, tools, instructions, bundled: _, } => { // For frontend tools, just store them in the frontend_tools map let mut frontend_tools = self.frontend_tools.lock().await; for tool in tools { let frontend_tool = FrontendTool { name: tool.name.clone(), tool: tool.clone(), }; frontend_tools.insert(tool.name.clone(), frontend_tool); } // Store instructions if provided, using "frontend" as the key let mut frontend_instructions = self.frontend_instructions.lock().await; if let Some(instructions) = instructions { *frontend_instructions = Some(instructions.clone()); } else { // Default frontend instructions if none provided *frontend_instructions = Some( "The following tools are provided directly by the frontend and will be executed by the frontend when called.".to_string(), ); } } _ => { let mut extension_manager = self.extension_manager.lock().await; extension_manager.add_extension(extension.clone()).await?; } } // If vector tool selection is enabled, index the tools let selector = self.router_tool_selector.lock().await.clone(); if ToolRouterIndexManager::is_tool_router_enabled(&selector) { if let Some(selector) = selector { let extension_manager = self.extension_manager.lock().await; let selector = Arc::new(selector); if let Err(e) = ToolRouterIndexManager::update_extension_tools( &selector, &extension_manager, &extension.name(), "add", ) .await { return Err(ExtensionError::SetupError(format!( "Failed to index tools for extension {}: {}", extension.name(), e ))); } } } Ok(()) } pub async fn list_tools(&self, extension_name: Option) -> Vec { let extension_manager = self.extension_manager.lock().await; let mut prefixed_tools = extension_manager .get_prefixed_tools(extension_name.clone()) .await .unwrap_or_default(); if extension_name.is_none() || extension_name.as_deref() == Some("platform") { // Add platform tools prefixed_tools.push(platform_tools::search_available_extensions_tool()); prefixed_tools.push(platform_tools::manage_extensions_tool()); prefixed_tools.push(platform_tools::manage_schedule_tool()); // Add resource tools if supported if extension_manager.supports_resources() { prefixed_tools.push(platform_tools::read_resource_tool()); prefixed_tools.push(platform_tools::list_resources_tool()); } } prefixed_tools } pub async fn list_tools_for_router( &self, strategy: Option, ) -> Vec { let mut prefixed_tools = vec![]; match strategy { Some(RouterToolSelectionStrategy::Vector) => { prefixed_tools.push(router_tools::vector_search_tool()); } Some(RouterToolSelectionStrategy::Llm) => { prefixed_tools.push(router_tools::llm_search_tool()); } None => {} } // Get recent tool calls from router tool selector if available 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 { let extension_manager = self.extension_manager.lock().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 } pub async fn remove_extension(&self, name: &str) -> Result<()> { // If vector tool selection is enabled, remove tools from the index let selector = self.router_tool_selector.lock().await.clone(); if ToolRouterIndexManager::is_tool_router_enabled(&selector) { if let Some(selector) = selector { let extension_manager = self.extension_manager.lock().await; ToolRouterIndexManager::update_extension_tools( &selector, &extension_manager, name, "remove", ) .await?; } } let mut extension_manager = self.extension_manager.lock().await; extension_manager.remove_extension(name).await?; Ok(()) } pub async fn list_extensions(&self) -> Vec { let extension_manager = self.extension_manager.lock().await; extension_manager .list_extensions() .await .expect("Failed to list extensions") } /// Handle a confirmation response for a tool request pub async fn handle_confirmation( &self, request_id: String, confirmation: PermissionConfirmation, ) { if let Err(e) = self.confirmation_tx.send((request_id, confirmation)).await { error!("Failed to send confirmation: {}", e); } } #[instrument(skip(self, messages, session), fields(user_message))] pub async fn reply( &self, messages: &[Message], session: Option, ) -> anyhow::Result>> { let mut messages = messages.to_vec(); let reply_span = tracing::Span::current(); // Load settings from config let config = Config::global(); // Setup tools and prompt let (mut tools, mut toolshim_tools, mut system_prompt) = self.prepare_tools_and_prompt().await?; let goose_mode = config.get_param("GOOSE_MODE").unwrap_or("auto".to_string()); let (tools_with_readonly_annotation, tools_without_annotation) = Self::categorize_tools_by_annotation(&tools); if let Some(content) = messages .last() .and_then(|msg| msg.content.first()) .and_then(|c| c.as_text()) { debug!("user_message" = &content); } Ok(Box::pin(async_stream::try_stream! { let _ = reply_span.enter(); loop { match Self::generate_response_from_provider( self.provider().await?, &system_prompt, &messages, &tools, &toolshim_tools, ).await { Ok((response, usage)) => { // Emit model change event if provider is lead-worker let provider = self.provider().await?; if let Some(lead_worker) = provider.as_lead_worker() { // The actual model used is in the usage let active_model = usage.model.clone(); let (lead_model, worker_model) = lead_worker.get_model_info(); let mode = if active_model == lead_model { "lead" } else if active_model == worker_model { "worker" } else { "unknown" }; yield AgentEvent::ModelChange { model: active_model, mode: mode.to_string(), }; } // record usage for the session in the session file if let Some(session_config) = session.clone() { Self::update_session_metrics(session_config, &usage, messages.len()).await?; } // categorize the type of requests we need to handle let (frontend_requests, remaining_requests, filtered_response) = self.categorize_tool_requests(&response).await; // Record tool calls in the router selector let selector = self.router_tool_selector.lock().await.clone(); if let Some(selector) = selector { // Record frontend tool calls for request in &frontend_requests { if let Ok(tool_call) = &request.tool_call { if let Err(e) = selector.record_tool_call(&tool_call.name).await { tracing::error!("Failed to record frontend tool call: {}", e); } } } // Record remaining tool calls for request in &remaining_requests { if let Ok(tool_call) = &request.tool_call { if let Err(e) = selector.record_tool_call(&tool_call.name).await { tracing::error!("Failed to record tool call: {}", e); } } } } // Yield the assistant's response with frontend tool requests filtered out yield AgentEvent::Message(filtered_response.clone()); tokio::task::yield_now().await; let num_tool_requests = frontend_requests.len() + remaining_requests.len(); if num_tool_requests == 0 { break; } // Process tool requests depending on frontend tools and then goose_mode let message_tool_response = Arc::new(Mutex::new(Message::user())); // First handle any frontend tool requests let mut frontend_tool_stream = self.handle_frontend_tool_requests( &frontend_requests, message_tool_response.clone() ); // we have a stream of frontend tools to handle, inside the stream // execution is yeield back to this reply loop, and is of the same Message // type, so we can yield that back up to be handled while let Some(msg) = frontend_tool_stream.try_next().await? { yield AgentEvent::Message(msg); } // Clone goose_mode once before the match to avoid move issues let mode = goose_mode.clone(); if mode.as_str() == "chat" { // Skip all tool calls in chat mode for request in remaining_requests { let mut response = message_tool_response.lock().await; *response = response.clone().with_tool_response( request.id.clone(), Ok(vec![Content::text(CHAT_MODE_TOOL_SKIPPED_RESPONSE)]), ); } } else { // At this point, we have handled the frontend tool requests and know goose_mode != "chat" // What remains is handling the remaining tool requests (enable extension, // regular tool calls) in goose_mode == ["auto", "approve" or "smart_approve"] let mut permission_manager = PermissionManager::default(); let (permission_check_result, enable_extension_request_ids) = check_tool_permissions( &remaining_requests, &mode, tools_with_readonly_annotation.clone(), tools_without_annotation.clone(), &mut permission_manager, self.provider().await?).await; // Handle pre-approved and read-only tools in parallel let mut tool_futures: Vec<(String, ToolStream)> = Vec::new(); // Skip the confirmation for approved tools for request in &permission_check_result.approved { if let Ok(tool_call) = request.tool_call.clone() { let (req_id, tool_result) = self.dispatch_tool_call(tool_call, request.id.clone()).await; tool_futures.push((req_id, match tool_result { Ok(result) => tool_stream( result.notification_stream.unwrap_or_else(|| Box::new(stream::empty())), result.result, ), Err(e) => tool_stream( Box::new(stream::empty()), futures::future::ready(Err(e)), ), })); } } for request in &permission_check_result.denied { let mut response = message_tool_response.lock().await; *response = response.clone().with_tool_response( request.id.clone(), Ok(vec![Content::text(DECLINED_RESPONSE)]), ); } // We need interior mutability in handle_approval_tool_requests let tool_futures_arc = Arc::new(Mutex::new(tool_futures)); // Process tools requiring approval (enable extension, regular tool calls) let mut tool_approval_stream = self.handle_approval_tool_requests( &permission_check_result.needs_approval, tool_futures_arc.clone(), &mut permission_manager, message_tool_response.clone() ); // We have a stream of tool_approval_requests to handle // Execution is yielded back to this reply loop, and is of the same Message // type, so we can yield the Message back up to be handled and grab any // confirmations or denials while let Some(msg) = tool_approval_stream.try_next().await? { yield AgentEvent::Message(msg); } tool_futures = { // Lock the mutex asynchronously let mut futures_lock = tool_futures_arc.lock().await; // Drain the vector and collect into a new Vec futures_lock.drain(..).collect::>() }; let with_id = tool_futures .into_iter() .map(|(request_id, stream)| { stream.map(move |item| (request_id.clone(), item)) }) .collect::>(); let mut combined = stream::select_all(with_id); let mut all_install_successful = true; while let Some((request_id, item)) = combined.next().await { match item { ToolStreamItem::Result(output) => { if enable_extension_request_ids.contains(&request_id) && output.is_err(){ all_install_successful = false; } let mut response = message_tool_response.lock().await; *response = response.clone().with_tool_response(request_id, output); }, ToolStreamItem::Message(msg) => { yield AgentEvent::McpNotification((request_id, msg)) } } } // Update system prompt and tools if installations were successful if all_install_successful { (tools, toolshim_tools, system_prompt) = self.prepare_tools_and_prompt().await?; } } let final_message_tool_resp = message_tool_response.lock().await.clone(); yield AgentEvent::Message(final_message_tool_resp.clone()); messages.push(response); messages.push(final_message_tool_resp); }, Err(ProviderError::ContextLengthExceeded(_)) => { // At this point, the last message should be a user message // because call to provider led to context length exceeded error // Immediately yield a special message and break yield AgentEvent::Message(Message::assistant().with_context_length_exceeded( "The context length of the model has been exceeded. Please start a new session and try again.", )); break; }, Err(e) => { // Create an error message & terminate the stream error!("Error: {}", e); yield AgentEvent::Message(Message::assistant().with_text(format!("Ran into this error: {e}.\n\nPlease retry if you think this is a transient or recoverable error."))); break; } } // Yield control back to the scheduler to prevent blocking tokio::task::yield_now().await; } })) } /// Extend the system prompt with one line of additional instruction pub async fn extend_system_prompt(&self, instruction: String) { let mut prompt_manager = self.prompt_manager.lock().await; prompt_manager.add_system_prompt_extra(instruction); } /// Update the provider used by this agent pub async fn update_provider(&self, provider: Arc) -> Result<()> { *self.provider.lock().await = Some(provider.clone()); self.update_router_tool_selector(provider).await?; Ok(()) } async fn update_router_tool_selector(&self, provider: Arc) -> Result<()> { let config = Config::global(); let router_tool_selection_strategy = config .get_param("GOOSE_ROUTER_TOOL_SELECTION_STRATEGY") .unwrap_or_else(|_| "default".to_string()); let strategy = match router_tool_selection_strategy.to_lowercase().as_str() { "vector" => Some(RouterToolSelectionStrategy::Vector), "llm" => Some(RouterToolSelectionStrategy::Llm), _ => None, }; let selector = match strategy { Some(RouterToolSelectionStrategy::Vector) => { let table_name = generate_table_id(); let selector = create_tool_selector(strategy, provider, Some(table_name)) .await .map_err(|e| anyhow!("Failed to create tool selector: {}", e))?; Arc::new(selector) } Some(RouterToolSelectionStrategy::Llm) => { let selector = create_tool_selector(strategy, provider, None) .await .map_err(|e| anyhow!("Failed to create tool selector: {}", e))?; Arc::new(selector) } None => return Ok(()), }; let extension_manager = self.extension_manager.lock().await; ToolRouterIndexManager::index_platform_tools(&selector, &extension_manager).await?; *self.router_tool_selector.lock().await = Some(selector.clone()); Ok(()) } /// 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; prompt_manager.set_system_prompt_override(template); } pub async fn list_extension_prompts(&self) -> HashMap> { let extension_manager = self.extension_manager.lock().await; extension_manager .list_prompts() .await .expect("Failed to list prompts") } pub async fn get_prompt(&self, name: &str, arguments: Value) -> Result { let extension_manager = self.extension_manager.lock().await; // First find which extension has this prompt let prompts = extension_manager .list_prompts() .await .map_err(|e| anyhow!("Failed to list prompts: {}", e))?; if let Some(extension) = prompts .iter() .find(|(_, prompt_list)| prompt_list.iter().any(|p| p.name == name)) .map(|(extension, _)| extension) { return extension_manager .get_prompt(extension, name, arguments) .await .map_err(|e| anyhow!("Failed to get prompt: {}", e)); } Err(anyhow!("Prompt '{}' not found", name)) } pub async fn get_plan_prompt(&self) -> anyhow::Result { let extension_manager = self.extension_manager.lock().await; let tools = extension_manager.get_prefixed_tools(None).await?; let tools_info = tools .into_iter() .map(|tool| { ToolInfo::new( &tool.name, &tool.description, get_parameter_names(&tool), None, ) }) .collect(); let plan_prompt = extension_manager.get_planning_prompt(tools_info).await; Ok(plan_prompt) } pub async fn handle_tool_result(&self, id: String, result: ToolResult>) { if let Err(e) = self.tool_result_tx.send((id, result)).await { tracing::error!("Failed to send tool result: {}", e); } } pub async fn create_recipe(&self, mut messages: Vec) -> Result { let extension_manager = self.extension_manager.lock().await; let extensions_info = extension_manager.get_extensions_info().await; // Get model name from provider let provider = self.provider().await?; let model_config = provider.get_model_config(); let model_name = &model_config.model_name; let prompt_manager = self.prompt_manager.lock().await; let system_prompt = prompt_manager.build_system_prompt( extensions_info, self.frontend_instructions.lock().await.clone(), extension_manager.suggest_disable_extensions_prompt().await, Some(model_name), None, ); let recipe_prompt = prompt_manager.get_recipe_prompt().await; let tools = extension_manager.get_prefixed_tools(None).await?; messages.push(Message::user().with_text(recipe_prompt)); let (result, _usage) = self .provider .lock() .await .as_ref() .unwrap() .complete(&system_prompt, &messages, &tools) .await?; let content = result.as_concat_text(); // the response may be contained in ```json ```, strip that before parsing json let re = Regex::new(r"(?s)```[^\n]*\n(.*?)\n```").unwrap(); let clean_content = re .captures(&content) .and_then(|caps| caps.get(1).map(|m| m.as_str())) .unwrap_or(&content) .trim() .to_string(); // try to parse json response from the LLM let (instructions, activities) = if let Ok(json_content) = serde_json::from_str::(&clean_content) { let instructions = json_content .get("instructions") .ok_or_else(|| anyhow!("Missing 'instructions' in json response"))? .as_str() .ok_or_else(|| anyhow!("instructions' is not a string"))? .to_string(); let activities = json_content .get("activities") .ok_or_else(|| anyhow!("Missing 'activities' in json response"))? .as_array() .ok_or_else(|| anyhow!("'activities' is not an array'"))? .iter() .map(|act| { act.as_str() .map(|s| s.to_string()) .ok_or(anyhow!("'activities' array element is not a string")) }) .collect::>()?; (instructions, activities) } else { // If we can't get valid JSON, try string parsing // Use split_once to get the content after "Instructions:". let after_instructions = content .split_once("instructions:") .map(|(_, rest)| rest) .unwrap_or(&content); // Split once more to separate instructions from activities. let (instructions_part, activities_text) = after_instructions .split_once("activities:") .unwrap_or((after_instructions, "")); let instructions = instructions_part .trim_end_matches(|c: char| c.is_whitespace() || c == '#') .trim() .to_string(); let activities_text = activities_text.trim(); // Regex to remove bullet markers or numbers with an optional dot. let bullet_re = Regex::new(r"^[•\-\*\d]+\.?\s*").expect("Invalid regex"); // Process each line in the activities section. let activities: Vec = activities_text .lines() .map(|line| bullet_re.replace(line, "").to_string()) .map(|s| s.trim().to_string()) .filter(|line| !line.is_empty()) .collect(); (instructions, activities) }; let extensions = ExtensionConfigManager::get_all().unwrap_or_default(); let extension_configs: Vec<_> = extensions .iter() .filter(|e| e.enabled) .map(|e| e.config.clone()) .collect(); let author = Author { contact: std::env::var("USER") .or_else(|_| std::env::var("USERNAME")) .ok(), metadata: None, }; // Ideally we'd get the name of the provider we are using from the provider itself // but it doesn't know and the plumbing looks complicated. let config = Config::global(); let provider_name: String = config .get_param("GOOSE_PROVIDER") .expect("No provider configured. Run 'goose configure' first"); let settings = Settings { goose_provider: Some(provider_name.clone()), goose_model: Some(model_name.clone()), temperature: Some(model_config.temperature.unwrap_or(0.0)), }; let recipe = Recipe::builder() .title("Custom recipe from chat") .description("a custom recipe instance from this chat session") .instructions(instructions) .activities(activities) .extensions(extension_configs) .settings(settings) .author(author) .build() .expect("valid recipe"); Ok(recipe) } }