From e615ad1a8e8c1b62321f7efc5a9a600396729ccc Mon Sep 17 00:00:00 2001 From: Bradley Axen Date: Wed, 26 Feb 2025 17:33:02 +0100 Subject: [PATCH] feat: interactive after run (#1377) --- crates/goose-cli/src/main.rs | 18 ++++++- crates/goose-cli/src/session/mod.rs | 75 +++++++++++--------------- crates/goose-cli/src/session/output.rs | 19 ++----- 3 files changed, 51 insertions(+), 61 deletions(-) diff --git a/crates/goose-cli/src/main.rs b/crates/goose-cli/src/main.rs index 7c95b02f0f..c438caf937 100644 --- a/crates/goose-cli/src/main.rs +++ b/crates/goose-cli/src/main.rs @@ -106,6 +106,14 @@ enum Command { )] input_text: Option, + /// Continue in interactive mode after processing input + #[arg( + short = 's', + long = "interactive", + help = "Continue in interactive mode after processing initial input" + )] + interactive: bool, + /// Name for this run session #[arg( short, @@ -182,12 +190,13 @@ async fn main() -> Result<()> { }) => { let mut session = build_session(name, resume, extension, builtin).await; setup_logging(session.session_file().file_stem().and_then(|s| s.to_str()))?; - let _ = session.start().await; + let _ = session.interactive(None).await; return Ok(()); } Some(Command::Run { instructions, input_text, + interactive, name, resume, extension, @@ -213,7 +222,12 @@ async fn main() -> Result<()> { }; let mut session = build_session(name, resume, extension, builtin).await; setup_logging(session.session_file().file_stem().and_then(|s| s.to_str()))?; - let _ = session.headless_start(contents.clone()).await; + + if interactive { + session.interactive(Some(contents)).await?; + } else { + session.headless(contents).await?; + } return Ok(()); } Some(Command::Agents(cmd)) => { diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs index dc2c797b3f..575517dfef 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -14,7 +14,6 @@ use goose::agents::Agent; use goose::message::{Message, MessageContent}; use mcp_core::handler::ToolError; use rand::{distributions::Alphanumeric, Rng}; -use rustyline::Editor; use std::path::PathBuf; use tokio; @@ -104,8 +103,22 @@ impl Session { Ok(()) } - pub async fn start(&mut self) -> Result<()> { - let mut editor = Editor::<(), rustyline::history::DefaultHistory>::new()?; + /// Process a single message and get the response + async fn process_message(&mut self, message: String) -> Result<()> { + self.messages.push(Message::user().with_text(&message)); + storage::persist_messages(&self.session_file, &self.messages)?; + self.process_agent_response(false).await?; + Ok(()) + } + + /// Start an interactive session, optionally with an initial message + pub async fn interactive(&mut self, message: Option) -> Result<()> { + // Process initial message if provided + if let Some(msg) = message { + self.process_message(msg).await?; + } + + let mut editor = rustyline::Editor::<(), rustyline::history::DefaultHistory>::new()?; // Load history from messages for msg in self @@ -129,7 +142,7 @@ impl Session { storage::persist_messages(&self.session_file, &self.messages)?; output::show_thinking(); - self.process_agent_response(&mut editor).await?; + self.process_agent_response(true).await?; output::hide_thinking(); } input::InputResult::Exit => break, @@ -184,19 +197,12 @@ impl Session { Ok(()) } - pub async fn headless_start(&mut self, initial_message: String) -> Result<()> { - 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).await?; - Ok(()) + /// Process a single message and exit + pub async fn headless(&mut self, message: String) -> Result<()> { + self.process_message(message).await } - async fn process_agent_response( - &mut self, - editor: &mut Editor<(), rustyline::history::DefaultHistory>, - ) -> Result<()> { + async fn process_agent_response(&mut self, interactive: bool) -> Result<()> { let mut stream = self.agent.reply(&self.messages).await?; use futures::StreamExt; @@ -204,45 +210,26 @@ impl Session { tokio::select! { result = stream.next() => { match result { - Some(Ok(mut message)) => { - - // Handle tool confirmation requests before rendering + Some(Ok(message)) => { + // If it's a confirmation request, get approval but otherwise do not render/persist if let Some(MessageContent::ToolConfirmationRequest(confirmation)) = message.content.first() { output::hide_thinking(); // Format the confirmation prompt - let prompt = "Goose would like to call the above tool. Allow? (y/n):".to_string(); - - let confirmation_request = Message::user().with_tool_confirmation_request( - confirmation.id.clone(), - confirmation.tool_name.clone(), - confirmation.arguments.clone(), - Some(prompt) - ); - output::render_message(&confirmation_request); + let prompt = "Goose would like to call the above tool, do you approve?".to_string(); // Get confirmation from user - let confirmed = match input::get_input(editor)? { - input::InputResult::Message(content) => { - content.trim().to_lowercase().starts_with('y') - } - _ => false, - }; - + let confirmed = cliclack::confirm(prompt).initial_value(true).interact()?; self.agent.handle_confirmation(confirmation.id.clone(), confirmed).await; - - message = confirmation_request; } - - // Only push the message if it's not a tool confirmation request - if !message.content.iter().any(|content| matches!(content, MessageContent::ToolConfirmationRequest(_))) { + // otherwise we have a model/tool to render + else { self.messages.push(message.clone()); + storage::persist_messages(&self.session_file, &self.messages)?; + if interactive {output::hide_thinking()}; + output::render_message(&message); + if interactive {output::show_thinking()}; } - - storage::persist_messages(&self.session_file, &self.messages)?; - output::hide_thinking(); - output::render_message(&message); - output::show_thinking(); } Some(Err(e)) => { eprintln!("Error: {}", e); diff --git a/crates/goose-cli/src/session/output.rs b/crates/goose-cli/src/session/output.rs index e190d30ccb..06d383430e 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, ToolConfirmationRequest, ToolRequest, ToolResponse}; +use goose::message::{Message, MessageContent, ToolRequest, ToolResponse}; use mcp_core::tool::ToolCall; use serde_json::Value; use std::cell::RefCell; @@ -94,12 +94,12 @@ 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(req) => { - render_tool_confirmation_request(req, theme) - } MessageContent::Image(image) => { println!("Image: [data: {}, type: {}]", image.data, image.mime_type); } + _ => { + println!("Message type could not be rendered"); + } } } println!(); @@ -150,17 +150,6 @@ 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", theme), - } -} - pub fn render_error(message: &str) { println!("\n {} {}\n", style("error:").red().bold(), message); }