diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs index 32b580488..a2ec12cc5 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -11,7 +11,6 @@ use anyhow::Result; use etcetera::choose_app_strategy; use goose::agents::extension::{Envs, ExtensionConfig}; use goose::agents::Agent; -use goose::config::Config; use goose::message::{Message, MessageContent}; use mcp_core::handler::ToolError; use rand::{distributions::Alphanumeric, Rng}; @@ -122,18 +121,15 @@ impl Session { } } } - let config = Config::global(); output::display_greeting(); loop { - let goose_mode = config.get("GOOSE_MODE").unwrap_or("auto".to_string()); match input::get_input(&mut editor)? { input::InputResult::Message(content) => { self.messages.push(Message::user().with_text(&content)); storage::persist_messages(&self.session_file, &self.messages)?; output::show_thinking(); - self.process_agent_response(&mut editor, Some(goose_mode.clone())) - .await?; + self.process_agent_response(&mut editor).await?; output::hide_thinking(); } input::InputResult::Exit => break, @@ -189,25 +185,19 @@ impl Session { } pub async fn headless_start(&mut self, initial_message: String) -> Result<()> { - // Load settings from config - let config = Config::global(); - let goose_mode = config.get("GOOSE_MODE").unwrap_or("auto".to_string()); - self.messages .push(Message::user().with_text(&initial_message)); storage::persist_messages(&self.session_file, &self.messages)?; let mut editor = Editor::<(), rustyline::history::DefaultHistory>::new()?; - self.process_agent_response(&mut editor, Some(goose_mode.clone())) - .await?; + self.process_agent_response(&mut editor).await?; Ok(()) } async fn process_agent_response( &mut self, editor: &mut Editor<(), rustyline::history::DefaultHistory>, - goose_mode: Option, ) -> Result<()> { - let mut stream = self.agent.reply(&self.messages, goose_mode).await?; + let mut stream = self.agent.reply(&self.messages).await?; use futures::StreamExt; loop { @@ -222,11 +212,16 @@ impl Session { // Format the confirmation prompt let prompt = format!( - "Goose would like to call the tool: {}\nWith arguments: {}\nAllow? (y/n): ", - confirmation.tool_name, - serde_json::to_string_pretty(&confirmation.arguments).unwrap_or_default() + "Goose would like to call the above tool. Allow? (y/n): ", + ); + + let confirmation_request = Message::user().with_tool_confirmation_request( + confirmation.id.clone(), + confirmation.tool_name.clone(), + confirmation.arguments.clone(), + Some(prompt.clone()) ); - output::render_message(&Message::assistant().with_text(&prompt)); + output::render_message(&confirmation_request); // Get confirmation from user let confirmed = match input::get_input(editor)? { @@ -235,11 +230,6 @@ impl Session { } _ => false, }; - let confirmation_request = Message::user().with_tool_confirmation_request( - confirmation.id.clone(), - confirmation.tool_name.clone(), - confirmation.arguments.clone(), - ); self.agent.handle_confirmation(confirmation.id.clone(), confirmed).await; diff --git a/crates/goose-cli/src/session/output.rs b/crates/goose-cli/src/session/output.rs index 88896da63..feaebe049 100644 --- a/crates/goose-cli/src/session/output.rs +++ b/crates/goose-cli/src/session/output.rs @@ -1,7 +1,7 @@ use bat::WrappingMode; use console::style; use goose::config::Config; -use goose::message::{Message, MessageContent, ToolRequest, ToolResponse}; +use goose::message::{Message, MessageContent, ToolConfirmationRequest, ToolRequest, ToolResponse}; use mcp_core::tool::ToolCall; use serde_json::Value; use std::cell::RefCell; @@ -94,7 +94,9 @@ pub fn render_message(message: &Message) { MessageContent::Text(text) => print_markdown(&text.text, theme), MessageContent::ToolRequest(req) => render_tool_request(req, theme), MessageContent::ToolResponse(resp) => render_tool_response(resp, theme), - MessageContent::ToolConfirmationRequest(_) => {} + MessageContent::ToolConfirmationRequest(req) => { + render_tool_confirmation_request(req, theme) + } MessageContent::Image(image) => { println!("Image: [data: {}, type: {}]", image.data, image.mime_type); } @@ -148,6 +150,17 @@ fn render_tool_response(resp: &ToolResponse, theme: Theme) { } } +fn render_tool_confirmation_request(req: &ToolConfirmationRequest, theme: Theme) { + match &req.prompt { + Some(prompt) => { + let colored_prompt = + prompt.replace("Allow? (y/n)", &format!("{}", style("Allow? (y/n)").cyan())); + println!("{}", colored_prompt); + } + None => print_markdown(&"No prompt provided".to_string(), theme), + } +} + pub fn render_error(message: &str) { println!("\n {} {}\n", style("error:").red().bold(), message); } diff --git a/crates/goose-server/src/routes/reply.rs b/crates/goose-server/src/routes/reply.rs index ec321928e..03166c173 100644 --- a/crates/goose-server/src/routes/reply.rs +++ b/crates/goose-server/src/routes/reply.rs @@ -248,14 +248,13 @@ async fn stream_message( } } MessageContent::ToolConfirmationRequest(_) => { - // TODO + // skip tool confirmation requests } MessageContent::Image(_) => { - // TODO + // skip images } MessageContent::ToolResponse(_) => { - // Tool responses should only come from the user - continue; + // skip tool responses } } } @@ -310,7 +309,7 @@ async fn handler( } }; - let mut stream = match agent.reply(&messages, Some("auto".to_string())).await { + let mut stream = match agent.reply(&messages).await { Ok(stream) => stream, Err(e) => { tracing::error!("Failed to start reply stream: {:?}", e); @@ -397,7 +396,7 @@ async fn ask_handler( // Get response from agent let mut response_text = String::new(); - let mut stream = match agent.reply(&messages, Some("auto".to_string())).await { + let mut stream = match agent.reply(&messages).await { Ok(stream) => stream, Err(e) => { tracing::error!("Failed to start reply stream: {:?}", e); diff --git a/crates/goose/src/agents/agent.rs b/crates/goose/src/agents/agent.rs index d5169055c..469418b20 100644 --- a/crates/goose/src/agents/agent.rs +++ b/crates/goose/src/agents/agent.rs @@ -11,11 +11,7 @@ use crate::providers::base::ProviderUsage; #[async_trait] pub trait Agent: Send + Sync { /// Create a stream that yields each message as it's generated by the agent - async fn reply( - &self, - messages: &[Message], - goose_mode: Option, - ) -> Result>>; + async fn reply(&self, messages: &[Message]) -> Result>>; /// Add a new MCP client to the agent async fn add_extension(&mut self, config: ExtensionConfig) -> ExtensionResult<()>; diff --git a/crates/goose/src/agents/reference.rs b/crates/goose/src/agents/reference.rs index 7546a332d..bda3acce2 100644 --- a/crates/goose/src/agents/reference.rs +++ b/crates/goose/src/agents/reference.rs @@ -69,7 +69,6 @@ impl Agent for ReferenceAgent { async fn reply( &self, messages: &[Message], - _goose_mode: Option, ) -> anyhow::Result>> { let mut messages = messages.to_vec(); let reply_span = tracing::Span::current(); diff --git a/crates/goose/src/agents/truncate.rs b/crates/goose/src/agents/truncate.rs index d58bee22b..86663d2df 100644 --- a/crates/goose/src/agents/truncate.rs +++ b/crates/goose/src/agents/truncate.rs @@ -9,6 +9,7 @@ use tracing::{debug, error, instrument, warn}; use super::Agent; use crate::agents::capabilities::Capabilities; use crate::agents::extension::{ExtensionConfig, ExtensionResult}; +use crate::config::Config; use crate::message::{Message, ToolRequest}; use crate::providers::base::Provider; use crate::providers::base::ProviderUsage; @@ -140,7 +141,6 @@ impl Agent for TruncateAgent { async fn reply( &self, messages: &[Message], - goose_mode: Option, ) -> anyhow::Result>> { let mut messages = messages.to_vec(); let reply_span = tracing::Span::current(); @@ -148,6 +148,10 @@ impl Agent for TruncateAgent { let mut tools = capabilities.get_prefixed_tools().await?; let mut truncation_attempt: usize = 0; + // Load settings from config + let config = Config::global(); + let goose_mode = config.get("GOOSE_MODE").unwrap_or("auto".to_string()); + // we add in the 2 resource tools if any extensions support resources // TODO: make sure there is no collision with another extension's tool name let read_resource_tool = Tool::new( @@ -233,10 +237,10 @@ impl Agent for TruncateAgent { break; } - // Process each tool request sequentially, asking for confirmation + // Process tool requests depending on goose_mode let mut message_tool_response = Message::user(); // Clone goose_mode once before the match to avoid move issues - let mode = goose_mode.clone().unwrap_or_else(|| "auto".to_string()); + let mode = goose_mode.clone(); match mode.as_str() { "approve" => { // Process each tool request sequentially with confirmation @@ -246,6 +250,7 @@ impl Agent for TruncateAgent { request.id.clone(), tool_call.name.clone(), tool_call.arguments.clone(), + Some("Goose would like to call the tool: {}\nAllow? (y/n): ".to_string()), ); yield confirmation; @@ -277,7 +282,15 @@ impl Agent for TruncateAgent { for request in &tool_requests { message_tool_response = message_tool_response.with_tool_response( request.id.clone(), - Ok(vec![Content::text("Tool call skipped in Goose chat mode")]), + Ok(vec![Content::text( + "The following tool call was skipped in Goose chat mode. \ + In chat mode, you cannot run tool calls, instead, you can \ + only provide a detailed plan to the user. Provide an \ + explanation of the proposed tool call as if it were a plan. \ + Only if the user asks, provide a short explanation to the \ + user that they could consider running the tool above on \ + their own or with a different goose mode." + )]), ); } }, diff --git a/crates/goose/src/message.rs b/crates/goose/src/message.rs index e49a7c5ef..3e10e9d69 100644 --- a/crates/goose/src/message.rs +++ b/crates/goose/src/message.rs @@ -31,6 +31,7 @@ pub struct ToolConfirmationRequest { pub id: String, pub tool_name: String, pub arguments: Value, + pub prompt: Option, } #[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)] @@ -77,11 +78,13 @@ impl MessageContent { id: S, tool_name: String, arguments: Value, + prompt: Option, ) -> Self { MessageContent::ToolConfirmationRequest(ToolConfirmationRequest { id: id.into(), tool_name, arguments, + prompt, }) } pub fn as_tool_request(&self) -> Option<&ToolRequest> { @@ -212,9 +215,10 @@ impl Message { id: S, tool_name: String, arguments: Value, + prompt: Option, ) -> Self { self.with_content(MessageContent::tool_confirmation_request( - id, tool_name, arguments, + id, tool_name, arguments, prompt, )) } diff --git a/crates/goose/src/providers/formats/anthropic.rs b/crates/goose/src/providers/formats/anthropic.rs index 3679c1437..4eadf4bcd 100644 --- a/crates/goose/src/providers/formats/anthropic.rs +++ b/crates/goose/src/providers/formats/anthropic.rs @@ -57,14 +57,8 @@ pub fn format_messages(messages: &[Message]) -> Vec { })); } } - MessageContent::ToolConfirmationRequest(tool_confirmation_request) => { - content.push(json!({ - "type": "text", - "text": format!("Do you want to run tool '{}' with id '{}' and input '{}'?", - tool_confirmation_request.tool_name, - tool_confirmation_request.id, - tool_confirmation_request.arguments), - })); + MessageContent::ToolConfirmationRequest(_tool_confirmation_request) => { + // Skip tool confirmation requests } MessageContent::Image(_) => continue, // Anthropic doesn't support image content yet } diff --git a/crates/goose/src/providers/formats/bedrock.rs b/crates/goose/src/providers/formats/bedrock.rs index a47de0736..dcedf31ba 100644 --- a/crates/goose/src/providers/formats/bedrock.rs +++ b/crates/goose/src/providers/formats/bedrock.rs @@ -28,13 +28,8 @@ pub fn to_bedrock_message(message: &Message) -> Result { pub fn to_bedrock_message_content(content: &MessageContent) -> Result { Ok(match content { MessageContent::Text(text) => bedrock::ContentBlock::Text(text.text.to_string()), - MessageContent::ToolConfirmationRequest(tool_confirmation_request) => { - bedrock::ContentBlock::Text(format!( - "Do you want to run tool '{}' with id '{}' and input '{}'?", - tool_confirmation_request.tool_name, - tool_confirmation_request.id, - tool_confirmation_request.arguments - )) + MessageContent::ToolConfirmationRequest(_tool_confirmation_request) => { + bedrock::ContentBlock::Text("".to_string()) } MessageContent::Image(_) => { bail!("Image content is not supported by Bedrock provider yet") diff --git a/crates/goose/src/providers/formats/openai.rs b/crates/goose/src/providers/formats/openai.rs index 48ba4b154..f78304163 100644 --- a/crates/goose/src/providers/formats/openai.rs +++ b/crates/goose/src/providers/formats/openai.rs @@ -136,13 +136,8 @@ pub fn format_messages(messages: &[Message], image_format: &ImageFormat) -> Vec< } } } - MessageContent::ToolConfirmationRequest(tool_confirmation_request) => { - converted["content"] = json!(format!( - "Do you want to run tool '{}' with id '{}' and input '{}'?", - tool_confirmation_request.tool_name, - tool_confirmation_request.id, - tool_confirmation_request.arguments - )); + MessageContent::ToolConfirmationRequest(_) => { + // Skip tool confirmation requests } MessageContent::Image(image) => { // Handle direct image content diff --git a/crates/goose/tests/truncate_agent.rs b/crates/goose/tests/truncate_agent.rs index 3571b1bfb..4225797f9 100644 --- a/crates/goose/tests/truncate_agent.rs +++ b/crates/goose/tests/truncate_agent.rs @@ -120,7 +120,7 @@ async fn run_truncate_test( ), ]; - let reply_stream = agent.reply(&messages, Some("auto".to_string())).await?; + let reply_stream = agent.reply(&messages).await?; tokio::pin!(reply_stream); let mut responses = Vec::new();