From 2ffd55b4beb66a82ed072714903a13104e9462ae Mon Sep 17 00:00:00 2001 From: Salman Mohammed Date: Mon, 10 Mar 2025 11:24:20 -0400 Subject: [PATCH 01/15] stash goose changes - sessions/20250307_122909.jsonl --- crates/goose-cli/src/session/input.rs | 60 ++++++++++++++++++++++- crates/goose-cli/src/session/mod.rs | 31 ++++++++++++ crates/goose-cli/src/session/output.rs | 68 ++++++++++++++++++++++++++ 3 files changed, 157 insertions(+), 2 deletions(-) diff --git a/crates/goose-cli/src/session/input.rs b/crates/goose-cli/src/session/input.rs index 795516250bc5..867540046c28 100644 --- a/crates/goose-cli/src/session/input.rs +++ b/crates/goose-cli/src/session/input.rs @@ -15,6 +15,10 @@ pub enum InputResult { ListPrompts(Option), PromptCommand(PromptCommandOptions), GooseMode(String), + // New modes for exploration and planning + ExploreMode, // ExploreMode uses read-only tools + PlanMode(String), // String contains the plan instructions + ActMode, // Execute the current plan } #[derive(Debug)] @@ -72,6 +76,9 @@ fn handle_slash_command(input: &str) -> Option { const CMD_EXTENSION: &str = "/extension "; const CMD_BUILTIN: &str = "/builtin "; const CMD_MODE: &str = "/mode "; + const CMD_EXPLORE: &str = "/explore"; + const CMD_PLAN: &str = "/plan "; + const CMD_ACT: &str = "/act"; match input { "/exit" | "/quit" => Some(InputResult::Exit), @@ -111,6 +118,19 @@ fn handle_slash_command(input: &str) -> Option { s if s.starts_with(CMD_MODE) => { Some(InputResult::GooseMode(s[CMD_MODE.len()..].to_string())) } + s if s.starts_with(CMD_EXPLORE) => Some(InputResult::ExploreMode), + s if s.starts_with(CMD_PLAN) => { + // Extract plan instructions + let instructions = s[CMD_PLAN.len()..].trim().to_string(); + if instructions.is_empty() { + // Error case - empty plan instructions + eprintln!("Error: Plan instructions cannot be empty"); + Some(InputResult::Retry) + } else { + Some(InputResult::PlanMode(instructions)) + } + } + s if s == CMD_ACT => Some(InputResult::ActMode), _ => None, } } @@ -175,9 +195,12 @@ fn print_help() { /t - Toggle Light/Dark/Ansi theme /extension - Add a stdio extension (format: ENV1=val1 command args...) /builtin - Add builtin extensions by name (comma-separated) -/prompts [--extension ] - List all available prompts, optionally filtered by extension +/prompts [--extension ] - List all available prompts, optionally filtered by extension /prompt [--info] [key=value...] - Get prompt info or execute a prompt -/mode - Set the goose mode to use ('auto', 'approve', 'chat') +/mode - Set the goose mode to use ('auto', 'approve', 'chat') +/explore - Start an exploratory chat session, with read-only* tools (*read-only is determined by an LLM call) +/plan - Create a plan based on instructions (requires approval) +/act - Execute the current approved plan /? or /help - Display this help message Navigation: @@ -370,4 +393,37 @@ mod tests { panic!("Expected PromptCommand"); } } + + // Test new mode commands + #[test] + fn test_explore_mode() { + // Test explore mode + if let Some(InputResult::ExploreMode) = handle_slash_command("/explore") { + // This is expected + } else { + panic!("Expected ExploreMode"); + } + } + + #[test] + fn test_plan_mode() { + // Test plan mode with instructions + if let Some(InputResult::PlanMode(instructions)) = + handle_slash_command("/plan create a hello world app") + { + assert_eq!(instructions, "create a hello world app"); + } else { + panic!("Expected PlanMode"); + } + } + + #[test] + fn test_act_mode() { + // Test act mode + if let Some(InputResult::ActMode) = handle_slash_command("/act") { + // This is expected + } else { + panic!("Expected ActMode"); + } + } } diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs index f4bcb9f43c4d..59c0b4d849a2 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -348,6 +348,37 @@ impl Session { println!("Goose mode set to '{}'", mode); continue; } + input::InputResult::ExploreMode => { + save_history(&mut editor); + + // Render the explore mode entry message + output::render_enter_explore_mode(); + + // Mock response for now + output::render_mock_explore_response(); + continue; + } + input::InputResult::PlanMode(instructions) => { + save_history(&mut editor); + + // Render the plan mode entry message + output::render_enter_plan_mode(&instructions); + + // Mock response for now + output::render_mock_plan_response(&instructions); + output::render_plan_options(); + continue; + } + input::InputResult::ActMode => { + save_history(&mut editor); + + // Render the act mode entry message + output::render_enter_act_mode(); + + // Mock response for now + output::render_mock_act_response(); + continue; + } input::InputResult::PromptCommand(opts) => { save_history(&mut editor); diff --git a/crates/goose-cli/src/session/output.rs b/crates/goose-cli/src/session/output.rs index 4a7e82a062a1..7278de60d0a1 100644 --- a/crates/goose-cli/src/session/output.rs +++ b/crates/goose-cli/src/session/output.rs @@ -492,6 +492,74 @@ pub fn display_greeting() { println!("\nGoose is running! Enter your instructions, or try asking what goose can do.\n"); } +// New mode rendering functions +pub fn render_enter_explore_mode() { + println!( + "\n{} {}\n", + style("Entering explore mode (with read-only tools).") + .green() + .bold(), + style("To switch to a plan, you can run /plan ") + .green() + .dim() + ); +} + +pub fn render_enter_plan_mode(instructions: &str) { + println!( + "\n{} {}\n", + style("Creating plan:").green().bold(), + style("Review the plan and choose an option: act, retry , or back") + .green() + .dim() + ); + println!("{}", style(instructions).yellow()); + println!(); +} + +pub fn render_plan_options() { + println!( + "\n{}\n{}\n{}\n", + style("Options:").dim(), + style(" act - Execute the plan").dim(), + style(" retry - Create a new plan").dim(), + ); +} + +pub fn render_enter_act_mode() { + println!( + "\n{} {}\n", + style("Executing plan.").green().bold(), + style("The agent will now execute the approved plan.") + .green() + .dim() + ); +} + +pub fn render_mock_explore_response() { + println!("\n{}\n", style("Mock explore response.").cyan().bold(),); +} + +pub fn render_mock_plan_response(instructions: &str) { + println!( + "\n{}\n\n{}\n\n{}\n", + style("You are in plan mode.").cyan().bold(), + style(format!("Plan instructions: {}", instructions)).cyan(), + style("Choose: act, retry , or back") + .cyan() + .dim() + ); +} + +pub fn render_mock_act_response() { + println!( + "\n{}\n", + style("You are in act mode. Executing the approved plan.") + .cyan() + .bold(), + ); +} + #[cfg(test)] mod tests { use super::*; From 858c1f33299ebe0f3e0cc450bfa7147b8d94ba04 Mon Sep 17 00:00:00 2001 From: Salman Mohammed Date: Mon, 10 Mar 2025 14:47:22 -0400 Subject: [PATCH 02/15] copy over agent.plan() method from sm/plan-slash-command --- crates/goose/src/agents/agent.rs | 3 ++ crates/goose/src/agents/capabilities.rs | 18 +++++++- crates/goose/src/agents/extension.rs | 18 ++++++++ crates/goose/src/agents/reference.rs | 5 +++ crates/goose/src/agents/summarize.rs | 5 +++ crates/goose/src/agents/truncate.rs | 35 ++++++++++++++- crates/goose/src/prompts/plan.md | 60 +++++++++---------------- 7 files changed, 103 insertions(+), 41 deletions(-) diff --git a/crates/goose/src/agents/agent.rs b/crates/goose/src/agents/agent.rs index d724fb4a3a85..a906add4de55 100644 --- a/crates/goose/src/agents/agent.rs +++ b/crates/goose/src/agents/agent.rs @@ -50,6 +50,9 @@ pub trait Agent: Send + Sync { /// Get the total usage of the agent async fn usage(&self) -> Vec; + /// Create a response message from the planner model + async fn plan(&self, plan_messages: &[Message]) -> Result; + /// Add custom text to be included in the system prompt async fn extend_system_prompt(&mut self, extension: String); diff --git a/crates/goose/src/agents/capabilities.rs b/crates/goose/src/agents/capabilities.rs index 2b61438c725d..61a498b0ae42 100644 --- a/crates/goose/src/agents/capabilities.rs +++ b/crates/goose/src/agents/capabilities.rs @@ -10,7 +10,7 @@ use std::time::Duration; use tokio::sync::Mutex; use tracing::{debug, instrument}; -use super::extension::{ExtensionConfig, ExtensionError, ExtensionInfo, ExtensionResult}; +use super::extension::{ExtensionConfig, ExtensionError, ExtensionInfo, ExtensionResult, ToolInfo}; use crate::config::Config; use crate::prompt_template; use crate::providers::base::{Provider, ProviderUsage}; @@ -84,6 +84,14 @@ fn normalize(input: String) -> String { result.to_lowercase() } +pub fn get_parameter_names(tool: &Tool) -> Vec { + tool.input_schema + .get("properties") + .and_then(|props| props.as_object()) + .map(|props| props.keys().cloned().collect()) + .unwrap_or_default() +} + impl Capabilities { /// Create a new Capabilities with the specified provider pub fn new(provider: Box) -> Self { @@ -322,6 +330,14 @@ impl Capabilities { Ok(result) } + /// Get the extension prompt including client instructions + pub async fn get_planning_prompt(&self, tools_info: Vec) -> String { + let mut context: HashMap<&str, Value> = HashMap::new(); + context.insert("tools", serde_json::to_value(tools_info).unwrap()); + + prompt_template::render_global_file("plan.md", &context).expect("Prompt should render") + } + /// Get the extension prompt including client instructions pub async fn get_system_prompt(&self) -> String { let mut context: HashMap<&str, Value> = HashMap::new(); diff --git a/crates/goose/src/agents/extension.rs b/crates/goose/src/agents/extension.rs index 6aeafea0348c..14ea2369850a 100644 --- a/crates/goose/src/agents/extension.rs +++ b/crates/goose/src/agents/extension.rs @@ -171,3 +171,21 @@ impl ExtensionInfo { } } } + +/// Information about the tool used for building prompts +#[derive(Clone, Debug, Serialize)] +pub struct ToolInfo { + name: String, + description: String, + parameters: Vec, +} + +impl ToolInfo { + pub fn new(name: &str, description: &str, parameters: Vec) -> Self { + Self { + name: name.to_string(), + description: description.to_string(), + parameters, + } + } +} diff --git a/crates/goose/src/agents/reference.rs b/crates/goose/src/agents/reference.rs index 089202de1c65..4508098612f0 100644 --- a/crates/goose/src/agents/reference.rs +++ b/crates/goose/src/agents/reference.rs @@ -71,6 +71,11 @@ impl Agent for ReferenceAgent { // TODO implement } + /// Create a response message from the planner model + async fn plan(&self, _plan_messages: &[Message]) -> anyhow::Result { + todo!() + } + #[instrument(skip(self, messages, session), fields(user_message))] async fn reply( &self, diff --git a/crates/goose/src/agents/summarize.rs b/crates/goose/src/agents/summarize.rs index a2f93902f559..a69b8818d4ba 100644 --- a/crates/goose/src/agents/summarize.rs +++ b/crates/goose/src/agents/summarize.rs @@ -163,6 +163,11 @@ impl Agent for SummarizeAgent { } } + /// Create a response message from the planner model + async fn plan(&self, _plan_messages: &[Message]) -> anyhow::Result { + todo!() + } + #[instrument(skip(self, messages, session), fields(user_message))] async fn reply( &self, diff --git a/crates/goose/src/agents/truncate.rs b/crates/goose/src/agents/truncate.rs index c0cbd3ae4318..5c18a76680ae 100644 --- a/crates/goose/src/agents/truncate.rs +++ b/crates/goose/src/agents/truncate.rs @@ -10,12 +10,14 @@ use tracing::{debug, error, instrument, warn}; use super::agent::SessionConfig; use super::detect_read_only_tools; +use super::extension::ToolInfo; use super::Agent; -use crate::agents::capabilities::Capabilities; +use crate::agents::capabilities::{get_parameter_names, Capabilities}; use crate::agents::extension::{ExtensionConfig, ExtensionResult}; use crate::config::Config; use crate::config::ExperimentManager; use crate::message::{Message, ToolRequest}; +use crate::model::ModelConfig; use crate::providers::base::Provider; use crate::providers::base::ProviderUsage; use crate::providers::errors::ProviderError; @@ -146,6 +148,37 @@ impl Agent for TruncateAgent { } } + /// Create a response message from the planner model + async fn plan(&self, plan_messages: &[Message]) -> anyhow::Result { + let mut capabilities = self.capabilities.lock().await; + let tools = capabilities.get_prefixed_tools().await?; + let tools_info = tools + .into_iter() + .map(|tool| ToolInfo::new(&tool.name, &tool.description, get_parameter_names(&tool))) + .collect(); + + let plan_prompt = capabilities.get_planning_prompt(tools_info).await; + + // TODO: hacky to create a new provider for the planner each time plan is called + let planner_provider = std::env::var("PLANNER_PROVIDER").unwrap_or("openai".to_string()); + let mut planner_model = std::env::var("PLANNER_MODEL"); + if planner_model.is_err() && planner_provider == "openai" { + planner_model = Ok("o1-high".to_string()); + } else if planner_model.is_err() && planner_provider == "anthropic" { + planner_model = Ok("claude-3-7-sonnet-latest".to_string()); + } else { + return Err(anyhow::anyhow!("PLANNER_MODEL is not set for the provider")); + } + let planner_model = planner_model.unwrap(); + + let model = ModelConfig::new(planner_model); + let provider = crate::providers::create(&planner_provider, model)?; + + let (response, _usage) = provider.complete(&plan_prompt, plan_messages, &[]).await?; + + Ok(response) + } + #[instrument(skip(self, messages, session), fields(user_message))] async fn reply( &self, diff --git a/crates/goose/src/prompts/plan.md b/crates/goose/src/prompts/plan.md index c268d9fd9f47..1216a1dd8171 100644 --- a/crates/goose/src/prompts/plan.md +++ b/crates/goose/src/prompts/plan.md @@ -1,41 +1,23 @@ -You prepare plans for an agent system. You will recieve the current system -status as well as in an incoming request from the human. Your plan will be used by an AI agent, -who is taking actions on behalf of the human. - -The agent currently has access to the following tools +You are a specialized "planner" AI. Your job is to review the user's instruction and produce a detailed, actionable plan for accomplishing that instruction. +Your plan will executed by another "executor" AI agent, who has access to these tools: +{% if (tools is defined) and tools %} {% for tool in tools %} -{{tool.name}}: {{tool.description}}{% endfor %} - -If the request is simple, such as a greeting or a request for information or advice, the plan can simply be: -"reply to the user". - -However for anything more complex, reflect on the available tools and describe a step by step -solution that the agent can follow using their tools. - -Your plan needs to use the following format, but can have any number of tasks. - -```json -[ - {"description": "the first task here"}, - {"description": "the second task here"}, -] -``` - -# Examples - -These examples show the format you should follow. *Do not reply with any other text, just the json plan* - -```json -[ - {"description": "reply to the user"}, -] -``` - -```json -[ - {"description": "create a directory 'demo'"}, - {"description": "write a file at 'demo/fibonacci.py' with a function fibonacci implementation"}, - {"description": "run python demo/fibonacci.py"}, -] -``` +**{{tool.name}}** +Description: {{tool.description}} +Parameters: {{tool.parameters}} + +{% endfor %} +{% else %} +No tools are defined. +{% endif %} + +Instructions: +1. Consider the problem holistically. Determine whether you have enough information to create a full plan. + a. If the request or solution is unclear in any way, prepare clarifying questions. + b. If the available tools are insufficient to complete the request, describe the gap and either suggest next steps or ask for guidance. + c. When possible, batch your questions for the user, so it’s easier for them to provide all missing details at once. +2. Turn the high-level request into a concrete, step-by-step plan suitable for execution by a separate AI agent. + a. Where appropriate, outline control flow (e.g., conditions or branching decisions) that might be needed to handle different scenarios. + b. If steps depend on outputs from prior steps, clearly indicate how the data will be passed from one step to another (e.g., “Use the ‘image_url’ from Step 2 as input to Step 3”). + c. Include short explanatory notes about control flow, dependencies, or placeholders if it helps to execute the plan. From bff1e706a0cfd5a7e759308c8e07f8f0fd1491db Mon Sep 17 00:00:00 2001 From: Salman Mohammed Date: Mon, 10 Mar 2025 15:15:36 -0400 Subject: [PATCH 03/15] simplify --- crates/goose-cli/src/session/input.rs | 20 +++---- crates/goose-cli/src/session/mod.rs | 75 ++++++++++++++++++-------- crates/goose-cli/src/session/output.rs | 33 ++---------- 3 files changed, 65 insertions(+), 63 deletions(-) diff --git a/crates/goose-cli/src/session/input.rs b/crates/goose-cli/src/session/input.rs index 867540046c28..27633dcf4ff2 100644 --- a/crates/goose-cli/src/session/input.rs +++ b/crates/goose-cli/src/session/input.rs @@ -15,10 +15,10 @@ pub enum InputResult { ListPrompts(Option), PromptCommand(PromptCommandOptions), GooseMode(String), - // New modes for exploration and planning - ExploreMode, // ExploreMode uses read-only tools - PlanMode(String), // String contains the plan instructions - ActMode, // Execute the current plan + // New mode for explore-plan-act (represents a separate mode) + Explore, // Explore uses read-only tools + Plan(String), // String contains the plan instructions + Act, // Execute the current plan } #[derive(Debug)] @@ -118,7 +118,7 @@ fn handle_slash_command(input: &str) -> Option { s if s.starts_with(CMD_MODE) => { Some(InputResult::GooseMode(s[CMD_MODE.len()..].to_string())) } - s if s.starts_with(CMD_EXPLORE) => Some(InputResult::ExploreMode), + s if s.starts_with(CMD_EXPLORE) => Some(InputResult::Explore), s if s.starts_with(CMD_PLAN) => { // Extract plan instructions let instructions = s[CMD_PLAN.len()..].trim().to_string(); @@ -127,10 +127,10 @@ fn handle_slash_command(input: &str) -> Option { eprintln!("Error: Plan instructions cannot be empty"); Some(InputResult::Retry) } else { - Some(InputResult::PlanMode(instructions)) + Some(InputResult::Plan(instructions)) } } - s if s == CMD_ACT => Some(InputResult::ActMode), + s if s == CMD_ACT => Some(InputResult::Act), _ => None, } } @@ -398,7 +398,7 @@ mod tests { #[test] fn test_explore_mode() { // Test explore mode - if let Some(InputResult::ExploreMode) = handle_slash_command("/explore") { + if let Some(InputResult::Explore) = handle_slash_command("/explore") { // This is expected } else { panic!("Expected ExploreMode"); @@ -408,7 +408,7 @@ mod tests { #[test] fn test_plan_mode() { // Test plan mode with instructions - if let Some(InputResult::PlanMode(instructions)) = + if let Some(InputResult::Plan(instructions)) = handle_slash_command("/plan create a hello world app") { assert_eq!(instructions, "create a hello world app"); @@ -420,7 +420,7 @@ mod tests { #[test] fn test_act_mode() { // Test act mode - if let Some(InputResult::ActMode) = handle_slash_command("/act") { + if let Some(InputResult::Act) = handle_slash_command("/act") { // This is expected } else { panic!("Expected ActMode"); diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs index 59c0b4d849a2..7044ae1aaed6 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -37,6 +37,8 @@ pub struct Session { // Cache for completion data - using std::sync for thread safety without async completion_cache: Arc>, debug: bool, // New field for debug mode + in_explore_plan_mode: bool, + plan_messages: Vec, } // Cache structure for completion data @@ -72,6 +74,8 @@ impl Session { session_file, completion_cache: Arc::new(std::sync::RwLock::new(CompletionCache::new())), debug, + in_explore_plan_mode: false, + plan_messages: Vec::new(), } } @@ -265,20 +269,31 @@ impl Session { loop { match input::get_input(&mut editor)? { input::InputResult::Message(content) => { - save_history(&mut editor); + if !self.in_explore_plan_mode { + save_history(&mut editor); - self.messages.push(Message::user().with_text(&content)); + self.messages.push(Message::user().with_text(&content)); - // Get the provider from the agent for description generation - let provider = self.agent.provider().await; + // Get the provider from the agent for description generation + let provider = self.agent.provider().await; - // Persist messages with provider for automatic description generation - session::persist_messages(&self.session_file, &self.messages, Some(provider)) + // Persist messages with provider for automatic description generation + session::persist_messages( + &self.session_file, + &self.messages, + Some(provider), + ) .await?; - output::show_thinking(); - self.process_agent_response(true).await?; - output::hide_thinking(); + output::show_thinking(); + self.process_agent_response(true).await?; + output::hide_thinking(); + } else { + self.plan_messages.push(Message::user().with_text(&content)); + output::show_thinking(); + self.process_planner_response().await?; + output::hide_thinking(); + } } input::InputResult::Exit => break, input::InputResult::AddExtension(cmd) => { @@ -348,35 +363,42 @@ impl Session { println!("Goose mode set to '{}'", mode); continue; } - input::InputResult::ExploreMode => { - save_history(&mut editor); + input::InputResult::Explore => { + if self.in_explore_plan_mode { + println!("You're already in explore-plan-act mode."); + continue; + } - // Render the explore mode entry message - output::render_enter_explore_mode(); + self.in_explore_plan_mode = true; + output::render_enter_explore_plan_act_mode(); - // Mock response for now - output::render_mock_explore_response(); continue; } - input::InputResult::PlanMode(instructions) => { - save_history(&mut editor); + input::InputResult::Plan(instructions) => { + if !self.in_explore_plan_mode { + println!("You need to run /explore first to enter explore-plan-act mode."); + continue; + } // Render the plan mode entry message output::render_enter_plan_mode(&instructions); - // Mock response for now - output::render_mock_plan_response(&instructions); output::render_plan_options(); continue; } - input::InputResult::ActMode => { - save_history(&mut editor); + input::InputResult::Act => { + if !self.in_explore_plan_mode { + println!("You need to run /explore first to enter explore-plan-act mode."); + continue; + } + + self.in_explore_plan_mode = false; // Render the act mode entry message output::render_enter_act_mode(); - // Mock response for now - output::render_mock_act_response(); + // Clear the plan messages + self.plan_messages.clear(); continue; } input::InputResult::PromptCommand(opts) => { @@ -536,6 +558,13 @@ impl Session { Ok(()) } + async fn process_planner_response(&mut self) -> Result<()> { + let response_message = self.agent.plan(&self.plan_messages).await?; + self.plan_messages.push(response_message.clone()); + output::render_message(&response_message, self.debug); + Ok(()) + } + async fn handle_interrupted_messages(&mut self, interrupt: bool) -> Result<()> { // First, get any tool requests from the last message if it exists let tool_requests = self diff --git a/crates/goose-cli/src/session/output.rs b/crates/goose-cli/src/session/output.rs index 7278de60d0a1..a179daec3f76 100644 --- a/crates/goose-cli/src/session/output.rs +++ b/crates/goose-cli/src/session/output.rs @@ -492,14 +492,11 @@ pub fn display_greeting() { println!("\nGoose is running! Enter your instructions, or try asking what goose can do.\n"); } -// New mode rendering functions -pub fn render_enter_explore_mode() { +pub fn render_enter_explore_plan_act_mode() { println!( "\n{} {}\n", - style("Entering explore mode (with read-only tools).") - .green() - .bold(), - style("To switch to a plan, you can run /plan ") + style("Entering explore-plan-act mode.").green().bold(), + style("You can chat and use read-only tools. Type /plan to create a detailed plan.") .green() .dim() ); @@ -536,30 +533,6 @@ pub fn render_enter_act_mode() { ); } -pub fn render_mock_explore_response() { - println!("\n{}\n", style("Mock explore response.").cyan().bold(),); -} - -pub fn render_mock_plan_response(instructions: &str) { - println!( - "\n{}\n\n{}\n\n{}\n", - style("You are in plan mode.").cyan().bold(), - style(format!("Plan instructions: {}", instructions)).cyan(), - style("Choose: act, retry , or back") - .cyan() - .dim() - ); -} - -pub fn render_mock_act_response() { - println!( - "\n{}\n", - style("You are in act mode. Executing the approved plan.") - .cyan() - .bold(), - ); -} - #[cfg(test)] mod tests { use super::*; From 61235ac7c4551826509c982555b4e49197027472 Mon Sep 17 00:00:00 2001 From: Salman Mohammed Date: Tue, 11 Mar 2025 09:32:06 -0400 Subject: [PATCH 04/15] just have a plan mode, that gets plan prompt and runs reasoner provider --- crates/goose-cli/src/session/input.rs | 54 ++--------- crates/goose-cli/src/session/mod.rs | 119 ++++++++++++------------- crates/goose-cli/src/session/output.rs | 41 +-------- crates/goose/src/agents/agent.rs | 6 +- crates/goose/src/agents/reference.rs | 9 +- crates/goose/src/agents/summarize.rs | 9 +- crates/goose/src/agents/truncate.rs | 45 +++------- 7 files changed, 89 insertions(+), 194 deletions(-) diff --git a/crates/goose-cli/src/session/input.rs b/crates/goose-cli/src/session/input.rs index 27633dcf4ff2..0b1dbe04b757 100644 --- a/crates/goose-cli/src/session/input.rs +++ b/crates/goose-cli/src/session/input.rs @@ -15,10 +15,7 @@ pub enum InputResult { ListPrompts(Option), PromptCommand(PromptCommandOptions), GooseMode(String), - // New mode for explore-plan-act (represents a separate mode) - Explore, // Explore uses read-only tools - Plan(String), // String contains the plan instructions - Act, // Execute the current plan + Plan(String), // String contains the planner model name } #[derive(Debug)] @@ -76,9 +73,7 @@ fn handle_slash_command(input: &str) -> Option { const CMD_EXTENSION: &str = "/extension "; const CMD_BUILTIN: &str = "/builtin "; const CMD_MODE: &str = "/mode "; - const CMD_EXPLORE: &str = "/explore"; - const CMD_PLAN: &str = "/plan "; - const CMD_ACT: &str = "/act"; + const CMD_PLAN: &str = "/plan"; match input { "/exit" | "/quit" => Some(InputResult::Exit), @@ -118,19 +113,11 @@ fn handle_slash_command(input: &str) -> Option { s if s.starts_with(CMD_MODE) => { Some(InputResult::GooseMode(s[CMD_MODE.len()..].to_string())) } - s if s.starts_with(CMD_EXPLORE) => Some(InputResult::Explore), s if s.starts_with(CMD_PLAN) => { - // Extract plan instructions - let instructions = s[CMD_PLAN.len()..].trim().to_string(); - if instructions.is_empty() { - // Error case - empty plan instructions - eprintln!("Error: Plan instructions cannot be empty"); - Some(InputResult::Retry) - } else { - Some(InputResult::Plan(instructions)) - } + // Extract planner model name + let model = s[CMD_PLAN.len()..].trim().to_string(); + Some(InputResult::Plan(model)) } - s if s == CMD_ACT => Some(InputResult::Act), _ => None, } } @@ -198,9 +185,7 @@ fn print_help() { /prompts [--extension ] - List all available prompts, optionally filtered by extension /prompt [--info] [key=value...] - Get prompt info or execute a prompt /mode - Set the goose mode to use ('auto', 'approve', 'chat') -/explore - Start an exploratory chat session, with read-only* tools (*read-only is determined by an LLM call) -/plan - Create a plan based on instructions (requires approval) -/act - Execute the current approved plan +/plan - Create a plan based on the current messages. Model options: o1, o3-mini-high, o3-mini, claude-3-7 /? or /help - Display this help message Navigation: @@ -394,36 +379,13 @@ mod tests { } } - // Test new mode commands - #[test] - fn test_explore_mode() { - // Test explore mode - if let Some(InputResult::Explore) = handle_slash_command("/explore") { - // This is expected - } else { - panic!("Expected ExploreMode"); - } - } - #[test] fn test_plan_mode() { // Test plan mode with instructions - if let Some(InputResult::Plan(instructions)) = - handle_slash_command("/plan create a hello world app") - { - assert_eq!(instructions, "create a hello world app"); + if let Some(InputResult::Plan(model)) = handle_slash_command("/plan claude-3-7") { + assert_eq!(model, "claude-3-7"); } else { panic!("Expected PlanMode"); } } - - #[test] - fn test_act_mode() { - // Test act mode - if let Some(InputResult::Act) = handle_slash_command("/act") { - // This is expected - } else { - panic!("Expected ActMode"); - } - } } diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs index 7044ae1aaed6..abb105c32784 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -37,8 +37,6 @@ pub struct Session { // Cache for completion data - using std::sync for thread safety without async completion_cache: Arc>, debug: bool, // New field for debug mode - in_explore_plan_mode: bool, - plan_messages: Vec, } // Cache structure for completion data @@ -74,8 +72,6 @@ impl Session { session_file, completion_cache: Arc::new(std::sync::RwLock::new(CompletionCache::new())), debug, - in_explore_plan_mode: false, - plan_messages: Vec::new(), } } @@ -269,31 +265,20 @@ impl Session { loop { match input::get_input(&mut editor)? { input::InputResult::Message(content) => { - if !self.in_explore_plan_mode { - save_history(&mut editor); + save_history(&mut editor); - self.messages.push(Message::user().with_text(&content)); + self.messages.push(Message::user().with_text(&content)); - // Get the provider from the agent for description generation - let provider = self.agent.provider().await; + // Get the provider from the agent for description generation + let provider = self.agent.provider().await; - // Persist messages with provider for automatic description generation - session::persist_messages( - &self.session_file, - &self.messages, - Some(provider), - ) + // Persist messages with provider for automatic description generation + session::persist_messages(&self.session_file, &self.messages, Some(provider)) .await?; - output::show_thinking(); - self.process_agent_response(true).await?; - output::hide_thinking(); - } else { - self.plan_messages.push(Message::user().with_text(&content)); - output::show_thinking(); - self.process_planner_response().await?; - output::hide_thinking(); - } + output::show_thinking(); + self.process_agent_response(true).await?; + output::hide_thinking(); } input::InputResult::Exit => break, input::InputResult::AddExtension(cmd) => { @@ -363,43 +348,56 @@ impl Session { println!("Goose mode set to '{}'", mode); continue; } - input::InputResult::Explore => { - if self.in_explore_plan_mode { - println!("You're already in explore-plan-act mode."); - continue; - } - - self.in_explore_plan_mode = true; - output::render_enter_explore_plan_act_mode(); - - continue; - } - input::InputResult::Plan(instructions) => { - if !self.in_explore_plan_mode { - println!("You need to run /explore first to enter explore-plan-act mode."); + input::InputResult::Plan(model) => { + // Copy the messages before the plan + // Run the plan -> prompting a reasoner model to create a plan + let reasoner_provider: String; + let reasoner_model: String; + if model.is_empty() { + reasoner_provider = "openai".to_string(); + reasoner_model = "o1-high".to_string(); + } else if model.starts_with("o1") || model.starts_with("o3-mini") { + reasoner_provider = "openai".to_string(); + reasoner_model = model; + } else if model.starts_with("claude") { + reasoner_provider = "anthropic".to_string(); + reasoner_model = "claude-3-7-sonnet-latest".to_string(); + // set env var "ANTHROPIC_THINKING_ENABLED" to "true" + std::env::set_var("ANTHROPIC_THINKING_ENABLED", "true"); + } else { + println!("Invalid planner model: {}", model); continue; } - // Render the plan mode entry message - output::render_enter_plan_mode(&instructions); - - output::render_plan_options(); - continue; - } - input::InputResult::Act => { - if !self.in_explore_plan_mode { - println!("You need to run /explore first to enter explore-plan-act mode."); - continue; + use goose::model::ModelConfig; + use goose::providers::create; + + // TODO: hacky to create a new provider for the planner each time plan is called + let model_config = ModelConfig::new(reasoner_model.to_string()); + let reasoner = create(reasoner_provider.as_str(), model_config)?; + + let plan_messages = self.messages.clone(); + let plan_prompt = self.agent.get_plan_prompt().await?; + let (plan_response, _usage) = + reasoner.complete(&plan_prompt, &plan_messages, &[]).await?; + + // Render the plan & ask if user wants to act on it + output::render_message(&plan_response, self.debug); + let confirmed = + cliclack::confirm("Do you want to clear history & act on this plan?") + .initial_value(true) + .interact()?; + if confirmed { + // clear the messages before the plan + self.messages.clear(); + output::display_session_history_cleared(); + // add the plan response + self.messages.push(plan_response); + // act on the plan + self.process_agent_response(true).await?; + } else { + self.messages.push(plan_response); } - - self.in_explore_plan_mode = false; - - // Render the act mode entry message - output::render_enter_act_mode(); - - // Clear the plan messages - self.plan_messages.clear(); - continue; } input::InputResult::PromptCommand(opts) => { save_history(&mut editor); @@ -558,13 +556,6 @@ impl Session { Ok(()) } - async fn process_planner_response(&mut self) -> Result<()> { - let response_message = self.agent.plan(&self.plan_messages).await?; - self.plan_messages.push(response_message.clone()); - output::render_message(&response_message, self.debug); - Ok(()) - } - async fn handle_interrupted_messages(&mut self, interrupt: bool) -> Result<()> { // First, get any tool requests from the last message if it exists let tool_requests = self diff --git a/crates/goose-cli/src/session/output.rs b/crates/goose-cli/src/session/output.rs index a179daec3f76..983db5bd934e 100644 --- a/crates/goose-cli/src/session/output.rs +++ b/crates/goose-cli/src/session/output.rs @@ -492,45 +492,8 @@ pub fn display_greeting() { println!("\nGoose is running! Enter your instructions, or try asking what goose can do.\n"); } -pub fn render_enter_explore_plan_act_mode() { - println!( - "\n{} {}\n", - style("Entering explore-plan-act mode.").green().bold(), - style("You can chat and use read-only tools. Type /plan to create a detailed plan.") - .green() - .dim() - ); -} - -pub fn render_enter_plan_mode(instructions: &str) { - println!( - "\n{} {}\n", - style("Creating plan:").green().bold(), - style("Review the plan and choose an option: act, retry , or back") - .green() - .dim() - ); - println!("{}", style(instructions).yellow()); - println!(); -} - -pub fn render_plan_options() { - println!( - "\n{}\n{}\n{}\n", - style("Options:").dim(), - style(" act - Execute the plan").dim(), - style(" retry - Create a new plan").dim(), - ); -} - -pub fn render_enter_act_mode() { - println!( - "\n{} {}\n", - style("Executing plan.").green().bold(), - style("The agent will now execute the approved plan.") - .green() - .dim() - ); +pub fn display_session_history_cleared() { + println!("\n{}\n", style("Session history cleared.").dim().cyan(),); } #[cfg(test)] diff --git a/crates/goose/src/agents/agent.rs b/crates/goose/src/agents/agent.rs index a906add4de55..9d2940702b39 100644 --- a/crates/goose/src/agents/agent.rs +++ b/crates/goose/src/agents/agent.rs @@ -50,9 +50,6 @@ pub trait Agent: Send + Sync { /// Get the total usage of the agent async fn usage(&self) -> Vec; - /// Create a response message from the planner model - async fn plan(&self, plan_messages: &[Message]) -> Result; - /// Add custom text to be included in the system prompt async fn extend_system_prompt(&mut self, extension: String); @@ -69,6 +66,9 @@ pub trait Agent: Send + Sync { /// Returns the prompt text that would be used as user input async fn get_prompt(&self, name: &str, arguments: Value) -> Result; + /// Get the plan prompt, which will be used with the planner (reasoner) model + async fn get_plan_prompt(&self) -> anyhow::Result; + /// Get a reference to the provider used by this agent async fn provider(&self) -> Arc>; } diff --git a/crates/goose/src/agents/reference.rs b/crates/goose/src/agents/reference.rs index 4508098612f0..8e9a047b7d62 100644 --- a/crates/goose/src/agents/reference.rs +++ b/crates/goose/src/agents/reference.rs @@ -71,11 +71,6 @@ impl Agent for ReferenceAgent { // TODO implement } - /// Create a response message from the planner model - async fn plan(&self, _plan_messages: &[Message]) -> anyhow::Result { - todo!() - } - #[instrument(skip(self, messages, session), fields(user_message))] async fn reply( &self, @@ -255,6 +250,10 @@ impl Agent for ReferenceAgent { Err(anyhow!("Prompt '{}' not found", name)) } + async fn get_plan_prompt(&self) -> anyhow::Result { + todo!() + } + async fn provider(&self) -> Arc> { let capabilities = self.capabilities.lock().await; capabilities.provider() diff --git a/crates/goose/src/agents/summarize.rs b/crates/goose/src/agents/summarize.rs index a69b8818d4ba..6fff07897e74 100644 --- a/crates/goose/src/agents/summarize.rs +++ b/crates/goose/src/agents/summarize.rs @@ -163,11 +163,6 @@ impl Agent for SummarizeAgent { } } - /// Create a response message from the planner model - async fn plan(&self, _plan_messages: &[Message]) -> anyhow::Result { - todo!() - } - #[instrument(skip(self, messages, session), fields(user_message))] async fn reply( &self, @@ -475,6 +470,10 @@ impl Agent for SummarizeAgent { Err(anyhow!("Prompt '{}' not found", name)) } + async fn get_plan_prompt(&self) -> anyhow::Result { + todo!() + } + async fn provider(&self) -> Arc> { let capabilities = self.capabilities.lock().await; capabilities.provider() diff --git a/crates/goose/src/agents/truncate.rs b/crates/goose/src/agents/truncate.rs index 5c18a76680ae..8d4cc4baa10e 100644 --- a/crates/goose/src/agents/truncate.rs +++ b/crates/goose/src/agents/truncate.rs @@ -17,7 +17,6 @@ use crate::agents::extension::{ExtensionConfig, ExtensionResult}; use crate::config::Config; use crate::config::ExperimentManager; use crate::message::{Message, ToolRequest}; -use crate::model::ModelConfig; use crate::providers::base::Provider; use crate::providers::base::ProviderUsage; use crate::providers::errors::ProviderError; @@ -148,37 +147,6 @@ impl Agent for TruncateAgent { } } - /// Create a response message from the planner model - async fn plan(&self, plan_messages: &[Message]) -> anyhow::Result { - let mut capabilities = self.capabilities.lock().await; - let tools = capabilities.get_prefixed_tools().await?; - let tools_info = tools - .into_iter() - .map(|tool| ToolInfo::new(&tool.name, &tool.description, get_parameter_names(&tool))) - .collect(); - - let plan_prompt = capabilities.get_planning_prompt(tools_info).await; - - // TODO: hacky to create a new provider for the planner each time plan is called - let planner_provider = std::env::var("PLANNER_PROVIDER").unwrap_or("openai".to_string()); - let mut planner_model = std::env::var("PLANNER_MODEL"); - if planner_model.is_err() && planner_provider == "openai" { - planner_model = Ok("o1-high".to_string()); - } else if planner_model.is_err() && planner_provider == "anthropic" { - planner_model = Ok("claude-3-7-sonnet-latest".to_string()); - } else { - return Err(anyhow::anyhow!("PLANNER_MODEL is not set for the provider")); - } - let planner_model = planner_model.unwrap(); - - let model = ModelConfig::new(planner_model); - let provider = crate::providers::create(&planner_provider, model)?; - - let (response, _usage) = provider.complete(&plan_prompt, plan_messages, &[]).await?; - - Ok(response) - } - #[instrument(skip(self, messages, session), fields(user_message))] async fn reply( &self, @@ -487,6 +455,19 @@ impl Agent for TruncateAgent { Err(anyhow!("Prompt '{}' not found", name)) } + async fn get_plan_prompt(&self) -> anyhow::Result { + let mut capabilities = self.capabilities.lock().await; + let tools = capabilities.get_prefixed_tools().await?; + let tools_info = tools + .into_iter() + .map(|tool| ToolInfo::new(&tool.name, &tool.description, get_parameter_names(&tool))) + .collect(); + + let plan_prompt = capabilities.get_planning_prompt(tools_info).await; + + Ok(plan_prompt) + } + async fn provider(&self) -> Arc> { let capabilities = self.capabilities.lock().await; capabilities.provider() From 066e63edc163210bb3a974ff1822fc10be96ad21 Mon Sep 17 00:00:00 2001 From: Salman Mohammed Date: Tue, 11 Mar 2025 10:02:14 -0400 Subject: [PATCH 05/15] cleared session's first msg needs to be user role, print out plan --- crates/goose-cli/src/session/mod.rs | 12 ++++++++++-- crates/goose/src/prompts/plan.md | 7 ++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs index abb105c32784..e5617ed38834 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -378,6 +378,7 @@ impl Session { let plan_messages = self.messages.clone(); let plan_prompt = self.agent.get_plan_prompt().await?; + println!("Plan Prompt: {}\n", plan_prompt); // TODO: remove let (plan_response, _usage) = reasoner.complete(&plan_prompt, &plan_messages, &[]).await?; @@ -387,15 +388,22 @@ impl Session { cliclack::confirm("Do you want to clear history & act on this plan?") .initial_value(true) .interact()?; + if confirmed { // clear the messages before the plan self.messages.clear(); output::display_session_history_cleared(); - // add the plan response - self.messages.push(plan_response); + // add the plan response as a user message + let plan_message = + Message::user().with_text(plan_response.as_concat_text()); + self.messages.push(plan_message); // act on the plan + output::show_thinking(); self.process_agent_response(true).await?; + output::hide_thinking(); } else { + // add the plan response (assistant message) & carry the conversation forward + // in the next round, the user might wanna slightly modify the plan self.messages.push(plan_response); } } diff --git a/crates/goose/src/prompts/plan.md b/crates/goose/src/prompts/plan.md index 1216a1dd8171..0eeb1369a987 100644 --- a/crates/goose/src/prompts/plan.md +++ b/crates/goose/src/prompts/plan.md @@ -1,4 +1,4 @@ -You are a specialized "planner" AI. Your job is to review the user's instruction and produce a detailed, actionable plan for accomplishing that instruction. +You are a specialized "planner" AI. Your job is to review the user's instruction and produce a detailed, actionable plan for accomplishing that instruction. Your plan will executed by another "executor" AI agent, who has access to these tools: {% if (tools is defined) and tools %} @@ -13,11 +13,12 @@ No tools are defined. {% endif %} Instructions: -1. Consider the problem holistically. Determine whether you have enough information to create a full plan. +1. Consider the problem holistically. Determine whether you have enough information to create a full plan. a. If the request or solution is unclear in any way, prepare clarifying questions. - b. If the available tools are insufficient to complete the request, describe the gap and either suggest next steps or ask for guidance. + b. If the available tools are insufficient to complete the request, describe the gap and either suggest next steps or ask for guidance. c. When possible, batch your questions for the user, so it’s easier for them to provide all missing details at once. 2. Turn the high-level request into a concrete, step-by-step plan suitable for execution by a separate AI agent. a. Where appropriate, outline control flow (e.g., conditions or branching decisions) that might be needed to handle different scenarios. b. If steps depend on outputs from prior steps, clearly indicate how the data will be passed from one step to another (e.g., “Use the ‘image_url’ from Step 2 as input to Step 3”). c. Include short explanatory notes about control flow, dependencies, or placeholders if it helps to execute the plan. +3. When outputting the plan, write it as an action plan for the "executor" AI agent. You should mention start by providing the higher level context on what you're trying to achieve, provide the detailed steps that you think needs to be executed and then ask to execute those steps. From 2cc75d79d605b54d66c0cbb3837529adde31bbe8 Mon Sep 17 00:00:00 2001 From: Salman Mohammed Date: Tue, 11 Mar 2025 10:41:25 -0400 Subject: [PATCH 06/15] plan command options --- crates/goose-cli/src/session/input.rs | 85 +++++++++++++++++++++++---- crates/goose-cli/src/session/mod.rs | 9 ++- crates/goose/src/prompts/plan.md | 12 ++-- 3 files changed, 86 insertions(+), 20 deletions(-) diff --git a/crates/goose-cli/src/session/input.rs b/crates/goose-cli/src/session/input.rs index 0b1dbe04b757..c5e6a610d1e8 100644 --- a/crates/goose-cli/src/session/input.rs +++ b/crates/goose-cli/src/session/input.rs @@ -15,7 +15,7 @@ pub enum InputResult { ListPrompts(Option), PromptCommand(PromptCommandOptions), GooseMode(String), - Plan(String), // String contains the planner model name + Plan(PlanCommandOptions), } #[derive(Debug)] @@ -25,6 +25,12 @@ pub struct PromptCommandOptions { pub arguments: HashMap, } +#[derive(Debug)] +pub struct PlanCommandOptions { + pub model: String, + pub message_text: String, +} + pub fn get_input( editor: &mut Editor, ) -> Result { @@ -113,11 +119,7 @@ fn handle_slash_command(input: &str) -> Option { s if s.starts_with(CMD_MODE) => { Some(InputResult::GooseMode(s[CMD_MODE.len()..].to_string())) } - s if s.starts_with(CMD_PLAN) => { - // Extract planner model name - let model = s[CMD_PLAN.len()..].trim().to_string(); - Some(InputResult::Plan(model)) - } + s if s.starts_with(CMD_PLAN) => parse_plan_command(s[CMD_PLAN.len()..].trim().to_string()), _ => None, } } @@ -175,6 +177,39 @@ fn parse_prompt_command(args: &str) -> Option { Some(InputResult::PromptCommand(options)) } +fn parse_plan_command(input: String) -> Option { + let mut options = PlanCommandOptions { + model: String::new(), + message_text: String::new(), + }; + + let parts: Vec = shlex::split(&input).unwrap_or_default(); + + if parts.is_empty() { + println!("For the /plan command, you must provide message text but none was provided."); + println!("Usage: /plan --model= "); + return None; + } + + if parts[0].starts_with("--model=") { + options.model = parts[0] + .strip_prefix("--model=") + .unwrap_or_default() + .to_string(); + } + + // start at index 0 if no model is provided, else start at index 1 & join the rest + options.message_text = parts[if options.model.is_empty() { 0 } else { 1 }..].join(" "); + + if options.message_text.is_empty() { + println!("For the /plan command, you must provide message text but none was provided."); + println!("Usage: /plan --model= "); + return None; + } + + Some(InputResult::Plan(options)) +} + fn print_help() { println!( "Available commands: @@ -185,7 +220,8 @@ fn print_help() { /prompts [--extension ] - List all available prompts, optionally filtered by extension /prompt [--info] [key=value...] - Get prompt info or execute a prompt /mode - Set the goose mode to use ('auto', 'approve', 'chat') -/plan - Create a plan based on the current messages. Model options: o1, o3-mini-high, o3-mini, claude-3-7 +/plan --model= - Create a plan based on the current messages. + Model options: o1, o3-mini-high, o3-mini, claude-3-7. Default model is o1-high. /? or /help - Display this help message Navigation: @@ -381,11 +417,36 @@ mod tests { #[test] fn test_plan_mode() { - // Test plan mode with instructions - if let Some(InputResult::Plan(model)) = handle_slash_command("/plan claude-3-7") { - assert_eq!(model, "claude-3-7"); - } else { - panic!("Expected PlanMode"); + // Test plan mode with no model or text + let result = handle_slash_command("/plan"); + assert!(result.is_none()); + + // Test plan mode with no text + let result = handle_slash_command("/plan --model=claude-3-7"); + assert!(result.is_none()); + + // Test plan mode with model & text + let result = handle_slash_command("/plan --model=claude-3-7 hello world"); + assert!(result.is_some()); + let options = result.unwrap(); + match options { + InputResult::Plan(options) => { + assert_eq!(options.model, "claude-3-7"); + assert_eq!(options.message_text, "hello world"); + } + _ => panic!("Expected Plan"), + } + + // Test plan mode with only text + let result = handle_slash_command("/plan hello world"); + assert!(result.is_some()); + let options = result.unwrap(); + match options { + InputResult::Plan(options) => { + assert_eq!(options.model, ""); + assert_eq!(options.message_text, "hello world"); + } + _ => panic!("Expected Plan"), } } } diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs index e5617ed38834..129d6e329062 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -348,7 +348,10 @@ impl Session { println!("Goose mode set to '{}'", mode); continue; } - input::InputResult::Plan(model) => { + input::InputResult::Plan(options) => { + let model = options.model; + let message_text = options.message_text; + // Copy the messages before the plan // Run the plan -> prompting a reasoner model to create a plan let reasoner_provider: String; @@ -376,7 +379,9 @@ impl Session { let model_config = ModelConfig::new(reasoner_model.to_string()); let reasoner = create(reasoner_provider.as_str(), model_config)?; - let plan_messages = self.messages.clone(); + let mut plan_messages = self.messages.clone(); + plan_messages.push(Message::user().with_text(&message_text)); + let plan_prompt = self.agent.get_plan_prompt().await?; println!("Plan Prompt: {}\n", plan_prompt); // TODO: remove let (plan_response, _usage) = diff --git a/crates/goose/src/prompts/plan.md b/crates/goose/src/prompts/plan.md index 0eeb1369a987..bfab95f8a6f0 100644 --- a/crates/goose/src/prompts/plan.md +++ b/crates/goose/src/prompts/plan.md @@ -12,13 +12,13 @@ Parameters: {{tool.parameters}} No tools are defined. {% endif %} -Instructions: -1. Consider the problem holistically. Determine whether you have enough information to create a full plan. - a. If the request or solution is unclear in any way, prepare clarifying questions. +Guidelines: +1. Determine whether you have enough information to create a full plan. + a. If the request or solution is unclear in any way, prepare all your clarifying questions & ask the user to provide more information. b. If the available tools are insufficient to complete the request, describe the gap and either suggest next steps or ask for guidance. - c. When possible, batch your questions for the user, so it’s easier for them to provide all missing details at once. 2. Turn the high-level request into a concrete, step-by-step plan suitable for execution by a separate AI agent. a. Where appropriate, outline control flow (e.g., conditions or branching decisions) that might be needed to handle different scenarios. - b. If steps depend on outputs from prior steps, clearly indicate how the data will be passed from one step to another (e.g., “Use the ‘image_url’ from Step 2 as input to Step 3”). + b. If steps depend on outputs from prior steps, clearly indicate how the data will be passed from one step to another (e.g., "Use the 'url' from Step 3 as input to Step 5"). c. Include short explanatory notes about control flow, dependencies, or placeholders if it helps to execute the plan. -3. When outputting the plan, write it as an action plan for the "executor" AI agent. You should mention start by providing the higher level context on what you're trying to achieve, provide the detailed steps that you think needs to be executed and then ask to execute those steps. +3. When outputting the plan, write it as an action plan for the "executor" AI agent to make it easy to follow and execute on. Remember the agent executing on the plan will only have access to the plan you provide, i.e. it will not be able to see your message history. That is why it's important to provide the higher level context on what the user is trying to achieve and important details from the chat history, a detailed step-by-step plan that needs to be executed and then ask to execute those steps. +4. You can only respond to the user one time and that response can either contain the plan or all the clarifying questions that you need before proceeding with the plan. From a5b1c764868a5ad09dd2cd504a2d1879b080ff0a Mon Sep 17 00:00:00 2001 From: Salman Mohammed Date: Tue, 11 Mar 2025 11:26:19 -0400 Subject: [PATCH 07/15] update plan prompt --- crates/goose/src/prompts/plan.md | 37 +++++++++++++++++++------------- 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/crates/goose/src/prompts/plan.md b/crates/goose/src/prompts/plan.md index 035dfba377c2..af8764a85112 100644 --- a/crates/goose/src/prompts/plan.md +++ b/crates/goose/src/prompts/plan.md @@ -1,7 +1,8 @@ -You are a specialized "planner" AI. Your job is to review the user's instruction and produce a detailed, actionable plan for accomplishing that instruction. -Your plan will executed by another "executor" AI agent, who has access to these tools: +You are a specialized "planner" AI. Your task is to analyze the user’s request from the chat messages and create either: +1. A detailed step-by-step plan (if you have enough information) on behalf of user that another "executor" AI agent can follow, or +2. A list of clarifying questions (if you do not have enough information) prompting the user to reply with the needed clarifications -{% if (tools is defined) and tools %} +{% if (tools is defined) and tools %} ## Available Tools {% for tool in tools %} **{{tool.name}}** Description: {{tool.description}} @@ -11,15 +12,21 @@ Parameters: {{tool.parameters}} {% else %} No tools are defined. {% endif %} - -Guidelines: -1. Determine whether you have enough information to create a full plan. - a. If the request or solution is unclear in any way, prepare all your clarifying questions & ask the user to provide more information. - b. If the available tools are insufficient to complete the request, describe the gap and either suggest next steps or ask for guidance. -2. Turn the high-level request into a concrete, step-by-step plan suitable for execution by a separate AI agent. - a. Where appropriate, outline control flow (e.g., conditions or branching decisions) that might be needed to handle different scenarios. - b. If steps depend on outputs from prior steps, clearly indicate how the data will be passed from one step to another (e.g., "Use the 'url' from Step 3 as input to Step 5"). - c. Include short explanatory notes about control flow, dependencies, or placeholders if it helps to execute the plan. -3. When outputting the plan, write it as an action plan for the "executor" AI agent on behalf of the human to make it easy for the agent to follow and execute on the plan. -4. Remember the agent executing on the plan will only have access to the plan you provide, i.e. it will not be able to see your message history. That is why it's important to provide the higher level context on what the user is trying to achieve and important details from the chat history, a detailed step-by-step plan that needs to be executed and then ask to execute those steps. -5. You can only respond to the user one time and that response can either contain the plan or all the clarifying questions that you need before proceeding with the plan. +## Guidelines +1. Check for clarity and feasibility + - If the user’s request is ambiguous, incomplete, or requires more information, respond only with all your clarifying questions in a concise list. + - If available tools are inadequate to complete the request, outline the gaps and suggest next steps or ask for additional tools or guidance. +2. Create a detailed plan + - Once you have sufficient clarity, produce a step-by-step plan that covers all actions the executor AI must take. + - Number the steps, and explicitly note any dependencies between steps (e.g., “Use the output from Step 3 as input for Step 4”). + - Include any conditional or branching logic needed (e.g., “If X occurs, do Y; otherwise, do Z”). +3. Provide essential context + - The executor AI will see only your final plan (as a user message) or your questions (as an assistant message) and will not have access to this conversation’s full history. + - Therefore, restate any relevant background, instructions, or prior conversation details needed to execute the plan successfully. +4. One-time response + - You can respond only once. + - If you respond with a plan, it will appear as a user message in a fresh conversation for the executor AI, effectively clearing out the previous context. + - If you respond with clarifying questions, it will appear as an assistant message in this same conversation, prompting the user to reply with the needed clarifications. +5. Keep it action oriented and clear + - In your final output (whether plan or questions), be concise yet thorough. + - The goal is to enable the executor AI to proceed confidently, without further ambiguity. From bc030a644ce055d146866477971d15b27d1b96bd Mon Sep 17 00:00:00 2001 From: Salman Mohammed Date: Tue, 18 Mar 2025 12:45:34 -0400 Subject: [PATCH 08/15] add warmup alias command --- crates/goose-cli/src/session/input.rs | 7 +- crates/goose-cli/src/session/mod.rs | 116 +++++++++++++++++++++----- 2 files changed, 101 insertions(+), 22 deletions(-) diff --git a/crates/goose-cli/src/session/input.rs b/crates/goose-cli/src/session/input.rs index c5e6a610d1e8..7d9299530662 100644 --- a/crates/goose-cli/src/session/input.rs +++ b/crates/goose-cli/src/session/input.rs @@ -16,6 +16,7 @@ pub enum InputResult { PromptCommand(PromptCommandOptions), GooseMode(String), Plan(PlanCommandOptions), + Warmup, } #[derive(Debug)] @@ -79,6 +80,7 @@ fn handle_slash_command(input: &str) -> Option { const CMD_EXTENSION: &str = "/extension "; const CMD_BUILTIN: &str = "/builtin "; const CMD_MODE: &str = "/mode "; + const CMD_WARMUP: &str = "/warmup"; const CMD_PLAN: &str = "/plan"; match input { @@ -119,6 +121,7 @@ fn handle_slash_command(input: &str) -> Option { s if s.starts_with(CMD_MODE) => { Some(InputResult::GooseMode(s[CMD_MODE.len()..].to_string())) } + s if s.starts_with(CMD_WARMUP) => Some(InputResult::Warmup), s if s.starts_with(CMD_PLAN) => parse_plan_command(s[CMD_PLAN.len()..].trim().to_string()), _ => None, } @@ -220,7 +223,9 @@ fn print_help() { /prompts [--extension ] - List all available prompts, optionally filtered by extension /prompt [--info] [key=value...] - Get prompt info or execute a prompt /mode - Set the goose mode to use ('auto', 'approve', 'chat') -/plan --model= - Create a plan based on the current messages. +/warmup - Warm up goose with required context before using '/plan' command. This sets the goose mode to 'approve'. +/plan --model= - Create a plan based on the current messages and asks user if they wanto act on it. + If user acts on the plan, the goose mode is automatically set to 'auto'. Model options: o1, o3-mini-high, o3-mini, claude-3-7. Default model is o1-high. /? or /help - Display this help message diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs index ef8d1014a8f7..7cd1f6e51da8 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -6,6 +6,7 @@ mod prompt; mod thinking; pub use builder::build_session; +use goose::providers::base::Provider; pub use goose::session::Identifier; use anyhow::Result; @@ -57,6 +58,42 @@ impl CompletionCache { } } +pub enum PlannerResponseType { + Plan, + ClarifyingQuestions, +} + +/// Decide if the planner's reponse is a plan or a clarifying question +/// +/// This function is called after the planner has generated a response +/// to the user's message. The response is either a plan or a clarifying +/// question. +pub async fn classify_planner_response( + message_text: String, + provider: Arc>, +) -> Result { + let prompt = format!("The text below is the output from an AI model which can either provide a plan or list of clarifying questions. Based on the text below, decide if the output is a \"plan\" or \"clarifying questions\".\n---\n{message_text}"); + + // Generate the description + let message = Message::user().with_text(&prompt); + let (result, _usage) = provider + .complete( + "Reply only with the classification label: \"plan\" or \"clarifying questions\"", + &[message], + &[], + ) + .await?; + + println!("classify_planner_response: {result:?}\n"); // TODO: remove + + let predicted = result.as_concat_text(); + if predicted.to_lowercase().contains("plan") { + Ok(PlannerResponseType::Plan) + } else { + Ok(PlannerResponseType::ClarifyingQuestions) + } +} + impl Session { pub fn new(agent: Box, session_file: PathBuf, debug: bool) -> Self { let messages = match session::read_messages(&session_file) { @@ -349,6 +386,17 @@ impl Session { println!("Goose mode set to '{}'", mode); continue; } + input::InputResult::Warmup => { + // Alias for setting goose mode to 'approve'. Meant to be used with '/plan' command + save_history(&mut editor); + + let config = Config::global(); + config + .set_param("GOOSE_MODE", Value::String("approve".to_string())) + .unwrap(); + println!("Goose mode set to 'approve'. After warming up with context, use '/plan' command to generate a plan."); + continue; + } input::InputResult::Plan(options) => { let model = options.model; let message_text = options.message_text; @@ -384,33 +432,59 @@ impl Session { plan_messages.push(Message::user().with_text(&message_text)); let plan_prompt = self.agent.get_plan_prompt().await?; - println!("Plan Prompt: {}\n", plan_prompt); // TODO: remove let (plan_response, _usage) = reasoner.complete(&plan_prompt, &plan_messages, &[]).await?; - // Render the plan & ask if user wants to act on it - output::render_message(&plan_response, self.debug); - let confirmed = - cliclack::confirm("Do you want to clear history & act on this plan?") + // Check if plan_response is a plan or clarifying question + let planner_response_type = classify_planner_response( + plan_response.as_concat_text(), + self.agent.provider().await, + ) + .await?; + + match planner_response_type { + PlannerResponseType::Plan => { + // Render the plan & ask if user wants to act on it + output::render_message(&plan_response, self.debug); + let confirmed = cliclack::confirm( + "Do you want to clear message history & act on this plan?", + ) .initial_value(true) .interact()?; + if confirmed { + // set goose mode: auto if that isn't already the case + let config = Config::global(); + let curr_goose_mode = + config.get_param("GOOSE_MODE").unwrap_or("auto".to_string()); + if curr_goose_mode != "auto" { + config + .set_param("GOOSE_MODE", Value::String("auto".to_string())) + .unwrap(); + println!("Goose mode set to 'auto'"); + } - if confirmed { - // clear the messages before the plan - self.messages.clear(); - output::display_session_history_cleared(); - // add the plan response as a user message - let plan_message = - Message::user().with_text(plan_response.as_concat_text()); - self.messages.push(plan_message); - // act on the plan - output::show_thinking(); - self.process_agent_response(true).await?; - output::hide_thinking(); - } else { - // add the plan response (assistant message) & carry the conversation forward - // in the next round, the user might wanna slightly modify the plan - self.messages.push(plan_response); + // clear the messages before acting on the plan + self.messages.clear(); + output::display_session_history_cleared(); + // add the plan response as a user message + let plan_message = + Message::user().with_text(plan_response.as_concat_text()); + self.messages.push(plan_message); + // act on the plan + output::show_thinking(); + self.process_agent_response(true).await?; + output::hide_thinking(); + } else { + // add the plan response (assistant message) & carry the conversation forward + // in the next round, the user might wanna slightly modify the plan + self.messages.push(plan_response); + } + } + PlannerResponseType::ClarifyingQuestions => { + // add the plan response (assistant message) & carry the conversation forward + // in the next round, the user will answer the clarifying questions + self.messages.push(plan_response); + } } } input::InputResult::PromptCommand(opts) => { From 2d718cad87d9f86313a48a1de7ae70b64503202c Mon Sep 17 00:00:00 2001 From: Salman Mohammed Date: Tue, 18 Mar 2025 13:05:22 -0400 Subject: [PATCH 09/15] reset goose mode back after plan execution --- crates/goose-cli/src/session/mod.rs | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs index 7cd1f6e51da8..25888d7a3053 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -84,7 +84,7 @@ pub async fn classify_planner_response( ) .await?; - println!("classify_planner_response: {result:?}\n"); // TODO: remove + // println!("classify_planner_response: {result:?}\n"); // TODO: remove let predicted = result.as_concat_text(); if predicted.to_lowercase().contains("plan") { @@ -432,8 +432,11 @@ impl Session { plan_messages.push(Message::user().with_text(&message_text)); let plan_prompt = self.agent.get_plan_prompt().await?; + output::show_thinking(); let (plan_response, _usage) = reasoner.complete(&plan_prompt, &plan_messages, &[]).await?; + output::render_message(&plan_response, self.debug); + output::hide_thinking(); // Check if plan_response is a plan or clarifying question let planner_response_type = classify_planner_response( @@ -444,14 +447,12 @@ impl Session { match planner_response_type { PlannerResponseType::Plan => { - // Render the plan & ask if user wants to act on it - output::render_message(&plan_response, self.debug); - let confirmed = cliclack::confirm( + let should_act = cliclack::confirm( "Do you want to clear message history & act on this plan?", ) .initial_value(true) .interact()?; - if confirmed { + if should_act { // set goose mode: auto if that isn't already the case let config = Config::global(); let curr_goose_mode = @@ -460,7 +461,7 @@ impl Session { config .set_param("GOOSE_MODE", Value::String("auto".to_string())) .unwrap(); - println!("Goose mode set to 'auto'"); + println!("Goose mode set to 'auto' for plan execution"); } // clear the messages before acting on the plan @@ -474,6 +475,17 @@ impl Session { output::show_thinking(); self.process_agent_response(true).await?; output::hide_thinking(); + + // Reset goose mode + if curr_goose_mode != "auto" { + config + .set_param( + "GOOSE_MODE", + Value::String(curr_goose_mode.to_string()), + ) + .unwrap(); + println!("Goose mode set back to '{curr_goose_mode}'"); + } } else { // add the plan response (assistant message) & carry the conversation forward // in the next round, the user might wanna slightly modify the plan From 694eb074e579a91f53023f72d23272592fe16eb6 Mon Sep 17 00:00:00 2001 From: Salman Mohammed Date: Wed, 19 Mar 2025 21:04:43 -0400 Subject: [PATCH 10/15] run_mode, color outputs --- crates/goose-cli/src/session/input.rs | 47 ++--- crates/goose-cli/src/session/mod.rs | 261 ++++++++++++++----------- crates/goose-cli/src/session/output.rs | 24 +++ 3 files changed, 181 insertions(+), 151 deletions(-) diff --git a/crates/goose-cli/src/session/input.rs b/crates/goose-cli/src/session/input.rs index 7d9299530662..2c52b8162b27 100644 --- a/crates/goose-cli/src/session/input.rs +++ b/crates/goose-cli/src/session/input.rs @@ -16,7 +16,7 @@ pub enum InputResult { PromptCommand(PromptCommandOptions), GooseMode(String), Plan(PlanCommandOptions), - Warmup, + EndPlan, } #[derive(Debug)] @@ -28,7 +28,7 @@ pub struct PromptCommandOptions { #[derive(Debug)] pub struct PlanCommandOptions { - pub model: String, + // pub model: String, pub message_text: String, } @@ -80,8 +80,8 @@ fn handle_slash_command(input: &str) -> Option { const CMD_EXTENSION: &str = "/extension "; const CMD_BUILTIN: &str = "/builtin "; const CMD_MODE: &str = "/mode "; - const CMD_WARMUP: &str = "/warmup"; const CMD_PLAN: &str = "/plan"; + const CMD_ENDPLAN: &str = "/endplan"; match input { "/exit" | "/quit" => Some(InputResult::Exit), @@ -121,8 +121,8 @@ fn handle_slash_command(input: &str) -> Option { s if s.starts_with(CMD_MODE) => { Some(InputResult::GooseMode(s[CMD_MODE.len()..].to_string())) } - s if s.starts_with(CMD_WARMUP) => Some(InputResult::Warmup), s if s.starts_with(CMD_PLAN) => parse_plan_command(s[CMD_PLAN.len()..].trim().to_string()), + s if s == CMD_ENDPLAN => Some(InputResult::EndPlan), _ => None, } } @@ -181,35 +181,10 @@ fn parse_prompt_command(args: &str) -> Option { } fn parse_plan_command(input: String) -> Option { - let mut options = PlanCommandOptions { - model: String::new(), - message_text: String::new(), + let options = PlanCommandOptions { + message_text: input.trim().to_string(), }; - let parts: Vec = shlex::split(&input).unwrap_or_default(); - - if parts.is_empty() { - println!("For the /plan command, you must provide message text but none was provided."); - println!("Usage: /plan --model= "); - return None; - } - - if parts[0].starts_with("--model=") { - options.model = parts[0] - .strip_prefix("--model=") - .unwrap_or_default() - .to_string(); - } - - // start at index 0 if no model is provided, else start at index 1 & join the rest - options.message_text = parts[if options.model.is_empty() { 0 } else { 1 }..].join(" "); - - if options.message_text.is_empty() { - println!("For the /plan command, you must provide message text but none was provided."); - println!("Usage: /plan --model= "); - return None; - } - Some(InputResult::Plan(options)) } @@ -223,10 +198,12 @@ fn print_help() { /prompts [--extension ] - List all available prompts, optionally filtered by extension /prompt [--info] [key=value...] - Get prompt info or execute a prompt /mode - Set the goose mode to use ('auto', 'approve', 'chat') -/warmup - Warm up goose with required context before using '/plan' command. This sets the goose mode to 'approve'. -/plan --model= - Create a plan based on the current messages and asks user if they wanto act on it. - If user acts on the plan, the goose mode is automatically set to 'auto'. - Model options: o1, o3-mini-high, o3-mini, claude-3-7. Default model is o1-high. +/plan - Enters 'plan' mode with optional message. Create a plan based on the current messages and asks user if they want to act on it. + If user acts on the plan, goose mode is set to 'auto' and returns to 'normal' goose mode. + To warm up goose before using '/plan', we recommend setting '/mode approve' & putting appropriate context into goose. + The model is used based on $GOOSE_PLANNER_PROVIDER and $GOOSE_PLANNER_MODEL environment variables. + If no model is set, the default model is used. +/endplan - Exit plan mode and return to 'normal' goose mode. /? or /help - Display this help message Navigation: diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs index 39900b7fea70..220f3df283dd 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -29,6 +29,11 @@ use std::sync::Arc; use std::time::Instant; use tokio; +pub enum RunMode { + Normal, + Plan, +} + pub struct Session { agent: Box, messages: Vec, @@ -36,6 +41,7 @@ pub struct Session { // Cache for completion data - using std::sync for thread safety without async completion_cache: Arc>, debug: bool, // New field for debug mode + run_mode: RunMode, } // Cache structure for completion data @@ -107,6 +113,7 @@ impl Session { session_file, completion_cache: Arc::new(std::sync::RwLock::new(CompletionCache::new())), debug, + run_mode: RunMode::Normal, } } @@ -301,20 +308,35 @@ impl Session { loop { match input::get_input(&mut editor)? { input::InputResult::Message(content) => { - save_history(&mut editor); + match self.run_mode { + RunMode::Normal => { + save_history(&mut editor); - self.messages.push(Message::user().with_text(&content)); + self.messages.push(Message::user().with_text(&content)); - // Get the provider from the agent for description generation - let provider = self.agent.provider().await; + // Get the provider from the agent for description generation + let provider = self.agent.provider().await; - // Persist messages with provider for automatic description generation - session::persist_messages(&self.session_file, &self.messages, Some(provider)) - .await?; + // Persist messages with provider for automatic description generation + session::persist_messages( + &self.session_file, + &self.messages, + Some(provider), + ) + .await?; - output::show_thinking(); - self.process_agent_response(true).await?; - output::hide_thinking(); + output::show_thinking(); + self.process_agent_response(true).await?; + output::hide_thinking(); + } + RunMode::Plan => { + let mut plan_messages = self.messages.clone(); + plan_messages.push(Message::user().with_text(&content)); + let reasoner = get_reasoner()?; + self.plan_with_reasoner_model(plan_messages, reasoner) + .await?; + } + } } input::InputResult::Exit => break, input::InputResult::AddExtension(cmd) => { @@ -381,121 +403,28 @@ impl Session { config .set_param("GOOSE_MODE", Value::String(mode.to_string())) .unwrap(); - println!("Goose mode set to '{}'", mode); - continue; - } - input::InputResult::Warmup => { - // Alias for setting goose mode to 'approve'. Meant to be used with '/plan' command - save_history(&mut editor); - - let config = Config::global(); - config - .set_param("GOOSE_MODE", Value::String("approve".to_string())) - .unwrap(); - println!("Goose mode set to 'approve'. After warming up with context, use '/plan' command to generate a plan."); + output::goose_mode_message(&format!("Goose mode set to '{}'", mode)); continue; } input::InputResult::Plan(options) => { - let model = options.model; - let message_text = options.message_text; + self.run_mode = RunMode::Plan; + output::render_enter_plan_mode(); - // Copy the messages before the plan - // Run the plan -> prompting a reasoner model to create a plan - let reasoner_provider: String; - let reasoner_model: String; - if model.is_empty() { - reasoner_provider = "openai".to_string(); - reasoner_model = "o1-high".to_string(); - } else if model.starts_with("o1") || model.starts_with("o3-mini") { - reasoner_provider = "openai".to_string(); - reasoner_model = model; - } else if model.starts_with("claude") { - reasoner_provider = "anthropic".to_string(); - reasoner_model = "claude-3-7-sonnet-latest".to_string(); - // set env var "ANTHROPIC_THINKING_ENABLED" to "true" - std::env::set_var("ANTHROPIC_THINKING_ENABLED", "true"); - } else { - println!("Invalid planner model: {}", model); + let message_text = options.message_text; + if message_text.is_empty() { continue; } - - use goose::model::ModelConfig; - use goose::providers::create; - - // TODO: hacky to create a new provider for the planner each time plan is called - let model_config = ModelConfig::new(reasoner_model.to_string()); - let reasoner = create(reasoner_provider.as_str(), model_config)?; - let mut plan_messages = self.messages.clone(); plan_messages.push(Message::user().with_text(&message_text)); - let plan_prompt = self.agent.get_plan_prompt().await?; - output::show_thinking(); - let (plan_response, _usage) = - reasoner.complete(&plan_prompt, &plan_messages, &[]).await?; - output::render_message(&plan_response, self.debug); - output::hide_thinking(); - - // Check if plan_response is a plan or clarifying question - let planner_response_type = classify_planner_response( - plan_response.as_concat_text(), - self.agent.provider().await, - ) - .await?; - - match planner_response_type { - PlannerResponseType::Plan => { - let should_act = cliclack::confirm( - "Do you want to clear message history & act on this plan?", - ) - .initial_value(true) - .interact()?; - if should_act { - // set goose mode: auto if that isn't already the case - let config = Config::global(); - let curr_goose_mode = - config.get_param("GOOSE_MODE").unwrap_or("auto".to_string()); - if curr_goose_mode != "auto" { - config - .set_param("GOOSE_MODE", Value::String("auto".to_string())) - .unwrap(); - println!("Goose mode set to 'auto' for plan execution"); - } - - // clear the messages before acting on the plan - self.messages.clear(); - output::display_session_history_cleared(); - // add the plan response as a user message - let plan_message = - Message::user().with_text(plan_response.as_concat_text()); - self.messages.push(plan_message); - // act on the plan - output::show_thinking(); - self.process_agent_response(true).await?; - output::hide_thinking(); - - // Reset goose mode - if curr_goose_mode != "auto" { - config - .set_param( - "GOOSE_MODE", - Value::String(curr_goose_mode.to_string()), - ) - .unwrap(); - println!("Goose mode set back to '{curr_goose_mode}'"); - } - } else { - // add the plan response (assistant message) & carry the conversation forward - // in the next round, the user might wanna slightly modify the plan - self.messages.push(plan_response); - } - } - PlannerResponseType::ClarifyingQuestions => { - // add the plan response (assistant message) & carry the conversation forward - // in the next round, the user will answer the clarifying questions - self.messages.push(plan_response); - } - } + let reasoner = get_reasoner()?; + self.plan_with_reasoner_model(plan_messages, reasoner) + .await?; + } + input::InputResult::EndPlan => { + self.run_mode = RunMode::Normal; + output::render_exit_plan_mode(); + continue; } input::InputResult::PromptCommand(opts) => { save_history(&mut editor); @@ -568,6 +497,75 @@ impl Session { Ok(()) } + async fn plan_with_reasoner_model( + &mut self, + plan_messages: Vec, + reasoner: Box, + ) -> Result<(), anyhow::Error> { + let plan_prompt = self.agent.get_plan_prompt().await?; + output::show_thinking(); + let (plan_response, _usage) = reasoner.complete(&plan_prompt, &plan_messages, &[]).await?; + output::render_message(&plan_response, self.debug); + output::hide_thinking(); + let planner_response_type = + classify_planner_response(plan_response.as_concat_text(), self.agent.provider().await) + .await?; + Ok(match planner_response_type { + PlannerResponseType::Plan => { + println!(); + let should_act = + cliclack::confirm("Do you want to clear message history & act on this plan?") + .initial_value(true) + .interact()?; + if should_act { + // set goose mode: auto if that isn't already the case + let config = Config::global(); + let curr_goose_mode = + config.get_param("GOOSE_MODE").unwrap_or("auto".to_string()); + if curr_goose_mode != "auto" { + config + .set_param("GOOSE_MODE", Value::String("auto".to_string())) + .unwrap(); + output::goose_mode_message("Goose mode set to 'auto' for plan execution"); + } + + // clear the messages before acting on the plan + self.messages.clear(); + output::display_session_history_cleared(); + // add the plan response as a user message + let plan_message = Message::user().with_text(plan_response.as_concat_text()); + self.messages.push(plan_message); + // act on the plan + output::show_thinking(); + self.process_agent_response(true).await?; + output::hide_thinking(); + + // Reset run & goose mode + self.run_mode = RunMode::Normal; + output::render_exit_plan_mode(); + + if curr_goose_mode != "auto" { + config + .set_param("GOOSE_MODE", Value::String(curr_goose_mode.to_string())) + .unwrap(); + output::goose_mode_message(&format!( + "Goose mode set back to '{curr_goose_mode}'" + )); + } + } else { + // add the plan response (assistant message) & carry the conversation forward + // in the next round, the user might wanna slightly modify the plan + self.messages.push(plan_response); + } + } + PlannerResponseType::ClarifyingQuestions => { + // add the plan response (assistant message) & carry the conversation forward + // in the next round, the user will answer the clarifying questions + self.messages.push(plan_response); + } + }) + } + /// Process a single message and exit pub async fn headless(&mut self, message: String) -> Result<()> { self.process_message(message).await @@ -799,3 +797,34 @@ impl Session { Ok(metadata.total_tokens) } } + +fn get_reasoner() -> Result, anyhow::Error> { + use goose::model::ModelConfig; + use goose::providers::create; + + let (reasoner_provider, reasoner_model) = match ( + std::env::var("GOOSE_PLANNER_PROVIDER"), + std::env::var("GOOSE_PLANNER_MODEL"), + ) { + (Ok(provider), Ok(model)) => (provider, model), + _ => { + println!( + "WARNING: GOOSE_PLANNER_PROVIDER or GOOSE_PLANNER_MODEL is not set. \ + Using default model from config..." + ); + let config = Config::global(); + let provider = config + .get_param("GOOSE_PROVIDER") + .expect("No provider configured. Run 'goose configure' first"); + let model = config + .get_param("GOOSE_MODEL") + .expect("No model configured. Run 'goose configure' first"); + (provider, model) + } + }; + + let model_config = ModelConfig::new(reasoner_model); + let reasoner = create(&reasoner_provider, model_config)?; + + Ok(reasoner) +} diff --git a/crates/goose-cli/src/session/output.rs b/crates/goose-cli/src/session/output.rs index fb379faadc3f..36fdff4583d5 100644 --- a/crates/goose-cli/src/session/output.rs +++ b/crates/goose-cli/src/session/output.rs @@ -126,6 +126,30 @@ pub fn render_message(message: &Message, debug: bool) { println!(); } +pub fn render_enter_plan_mode() { + println!( + "\n{} {}\n", + style("Entering plan mode.").green().bold(), + style("Enter your instructions, or type /endplan to exit.") + .green() + .dim() + ); +} + +pub fn render_exit_plan_mode() { + println!( + "\n{} {}\n", + style("Exiting plan mode.").green().bold(), + style("Enter your instructions, or type /plan to enter plan mode.") + .green() + .dim() + ); +} + +pub fn goose_mode_message(text: &str) { + println!("\n{}\n", style(text).yellow(),); +} + fn render_tool_request(req: &ToolRequest, theme: Theme, debug: bool) { match &req.tool_call { Ok(call) => match call.name.as_str() { From 679c44ab47dc959d4749f5ecf57244d635a2ed65 Mon Sep 17 00:00:00 2001 From: Salman Mohammed Date: Wed, 19 Mar 2025 22:19:47 -0400 Subject: [PATCH 11/15] change msg copy --- crates/goose-cli/src/session/mod.rs | 6 +----- crates/goose-cli/src/session/output.rs | 15 +++++++++------ 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs index 220f3df283dd..9f789b6f3f9f 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -541,16 +541,12 @@ impl Session { output::hide_thinking(); // Reset run & goose mode + output::render_act_on_plan(); self.run_mode = RunMode::Normal; - output::render_exit_plan_mode(); - if curr_goose_mode != "auto" { config .set_param("GOOSE_MODE", Value::String(curr_goose_mode.to_string())) .unwrap(); - output::goose_mode_message(&format!( - "Goose mode set back to '{curr_goose_mode}'" - )); } } else { // add the plan response (assistant message) & carry the conversation forward diff --git a/crates/goose-cli/src/session/output.rs b/crates/goose-cli/src/session/output.rs index 36fdff4583d5..7c9431cbb1b8 100644 --- a/crates/goose-cli/src/session/output.rs +++ b/crates/goose-cli/src/session/output.rs @@ -130,22 +130,25 @@ pub fn render_enter_plan_mode() { println!( "\n{} {}\n", style("Entering plan mode.").green().bold(), - style("Enter your instructions, or type /endplan to exit.") + style("You can provide instructions to create a plan and then act on it. To exit early, type /endplan") .green() .dim() ); } -pub fn render_exit_plan_mode() { +pub fn render_act_on_plan() { println!( - "\n{} {}\n", - style("Exiting plan mode.").green().bold(), - style("Enter your instructions, or type /plan to enter plan mode.") + "\n{}\n", + style("Acting on the above plan and exiting plan mode.") .green() - .dim() + .bold(), ); } +pub fn render_exit_plan_mode() { + println!("\n{}\n", style("Exiting plan mode.").green().bold()); +} + pub fn goose_mode_message(text: &str) { println!("\n{}\n", style(text).yellow(),); } From fab907ca437673547b3de8cc5739346525fdd92d Mon Sep 17 00:00:00 2001 From: Salman Mohammed Date: Wed, 19 Mar 2025 22:23:17 -0400 Subject: [PATCH 12/15] fix test_plan_mode --- crates/goose-cli/src/session/input.rs | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/crates/goose-cli/src/session/input.rs b/crates/goose-cli/src/session/input.rs index 2c52b8162b27..9206b86defd5 100644 --- a/crates/goose-cli/src/session/input.rs +++ b/crates/goose-cli/src/session/input.rs @@ -399,33 +399,16 @@ mod tests { #[test] fn test_plan_mode() { - // Test plan mode with no model or text - let result = handle_slash_command("/plan"); - assert!(result.is_none()); - // Test plan mode with no text - let result = handle_slash_command("/plan --model=claude-3-7"); - assert!(result.is_none()); - - // Test plan mode with model & text - let result = handle_slash_command("/plan --model=claude-3-7 hello world"); + let result = handle_slash_command("/plan"); assert!(result.is_some()); - let options = result.unwrap(); - match options { - InputResult::Plan(options) => { - assert_eq!(options.model, "claude-3-7"); - assert_eq!(options.message_text, "hello world"); - } - _ => panic!("Expected Plan"), - } - // Test plan mode with only text + // Test plan mode with text let result = handle_slash_command("/plan hello world"); assert!(result.is_some()); let options = result.unwrap(); match options { InputResult::Plan(options) => { - assert_eq!(options.model, ""); assert_eq!(options.message_text, "hello world"); } _ => panic!("Expected Plan"), From 8a1a9509c4cf718508fff73c7b0657e3771a208a Mon Sep 17 00:00:00 2001 From: Salman Mohammed Date: Wed, 19 Mar 2025 22:29:31 -0400 Subject: [PATCH 13/15] add get_plan_prompt in other agents --- crates/goose-cli/src/session/input.rs | 5 ++--- crates/goose/src/agents/reference.rs | 13 ++++++++++++- crates/goose/src/agents/summarize.rs | 13 ++++++++++++- 3 files changed, 26 insertions(+), 5 deletions(-) diff --git a/crates/goose-cli/src/session/input.rs b/crates/goose-cli/src/session/input.rs index 9206b86defd5..9e826ba3597a 100644 --- a/crates/goose-cli/src/session/input.rs +++ b/crates/goose-cli/src/session/input.rs @@ -28,7 +28,6 @@ pub struct PromptCommandOptions { #[derive(Debug)] pub struct PlanCommandOptions { - // pub model: String, pub message_text: String, } @@ -195,9 +194,9 @@ fn print_help() { /t - Toggle Light/Dark/Ansi theme /extension - Add a stdio extension (format: ENV1=val1 command args...) /builtin - Add builtin extensions by name (comma-separated) -/prompts [--extension ] - List all available prompts, optionally filtered by extension +/prompts [--extension ] - List all available prompts, optionally filtered by extension /prompt [--info] [key=value...] - Get prompt info or execute a prompt -/mode - Set the goose mode to use ('auto', 'approve', 'chat') +/mode - Set the goose mode to use ('auto', 'approve', 'chat') /plan - Enters 'plan' mode with optional message. Create a plan based on the current messages and asks user if they want to act on it. If user acts on the plan, goose mode is set to 'auto' and returns to 'normal' goose mode. To warm up goose before using '/plan', we recommend setting '/mode approve' & putting appropriate context into goose. diff --git a/crates/goose/src/agents/reference.rs b/crates/goose/src/agents/reference.rs index cb737789f0ec..812987b19ac2 100644 --- a/crates/goose/src/agents/reference.rs +++ b/crates/goose/src/agents/reference.rs @@ -8,6 +8,8 @@ use tokio::sync::Mutex; use tracing::{debug, instrument}; use super::agent::SessionConfig; +use super::capabilities::get_parameter_names; +use super::extension::ToolInfo; use super::Agent; use crate::agents::capabilities::Capabilities; use crate::agents::extension::{ExtensionConfig, ExtensionResult}; @@ -244,7 +246,16 @@ impl Agent for ReferenceAgent { } async fn get_plan_prompt(&self) -> anyhow::Result { - todo!() + let mut capabilities = self.capabilities.lock().await; + let tools = capabilities.get_prefixed_tools().await?; + let tools_info = tools + .into_iter() + .map(|tool| ToolInfo::new(&tool.name, &tool.description, get_parameter_names(&tool))) + .collect(); + + let plan_prompt = capabilities.get_planning_prompt(tools_info).await; + + Ok(plan_prompt) } async fn provider(&self) -> Arc> { diff --git a/crates/goose/src/agents/summarize.rs b/crates/goose/src/agents/summarize.rs index e6786ed62387..6c0da8896e93 100644 --- a/crates/goose/src/agents/summarize.rs +++ b/crates/goose/src/agents/summarize.rs @@ -10,7 +10,9 @@ use tokio::sync::Mutex; use tracing::{debug, error, instrument, warn}; use super::agent::SessionConfig; +use super::capabilities::get_parameter_names; use super::detect_read_only_tools; +use super::extension::ToolInfo; use super::Agent; use crate::agents::capabilities::Capabilities; use crate::agents::extension::{ExtensionConfig, ExtensionResult}; @@ -458,7 +460,16 @@ impl Agent for SummarizeAgent { } async fn get_plan_prompt(&self) -> anyhow::Result { - todo!() + let mut capabilities = self.capabilities.lock().await; + let tools = capabilities.get_prefixed_tools().await?; + let tools_info = tools + .into_iter() + .map(|tool| ToolInfo::new(&tool.name, &tool.description, get_parameter_names(&tool))) + .collect(); + + let plan_prompt = capabilities.get_planning_prompt(tools_info).await; + + Ok(plan_prompt) } async fn provider(&self) -> Arc> { From e52775b660fa2de4ac7fa75589ddaae868aea39b Mon Sep 17 00:00:00 2001 From: Salman Mohammed Date: Wed, 19 Mar 2025 22:41:41 -0400 Subject: [PATCH 14/15] show fewer msgs when acting on the plan --- crates/goose-cli/src/session/mod.rs | 6 ++---- crates/goose-cli/src/session/output.rs | 8 ++------ 2 files changed, 4 insertions(+), 10 deletions(-) diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs index 9f789b6f3f9f..f22a7d341bc7 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -518,6 +518,8 @@ impl Session { .initial_value(true) .interact()?; if should_act { + output::render_act_on_plan(); + self.run_mode = RunMode::Normal; // set goose mode: auto if that isn't already the case let config = Config::global(); let curr_goose_mode = @@ -526,12 +528,10 @@ impl Session { config .set_param("GOOSE_MODE", Value::String("auto".to_string())) .unwrap(); - output::goose_mode_message("Goose mode set to 'auto' for plan execution"); } // clear the messages before acting on the plan self.messages.clear(); - output::display_session_history_cleared(); // add the plan response as a user message let plan_message = Message::user().with_text(plan_response.as_concat_text()); self.messages.push(plan_message); @@ -541,8 +541,6 @@ impl Session { output::hide_thinking(); // Reset run & goose mode - output::render_act_on_plan(); - self.run_mode = RunMode::Normal; if curr_goose_mode != "auto" { config .set_param("GOOSE_MODE", Value::String(curr_goose_mode.to_string())) diff --git a/crates/goose-cli/src/session/output.rs b/crates/goose-cli/src/session/output.rs index 7c9431cbb1b8..a86e062fbc65 100644 --- a/crates/goose-cli/src/session/output.rs +++ b/crates/goose-cli/src/session/output.rs @@ -139,7 +139,7 @@ pub fn render_enter_plan_mode() { pub fn render_act_on_plan() { println!( "\n{}\n", - style("Acting on the above plan and exiting plan mode.") + style("Exiting plan mode and acting on the above plan") .green() .bold(), ); @@ -150,7 +150,7 @@ pub fn render_exit_plan_mode() { } pub fn goose_mode_message(text: &str) { - println!("\n{}\n", style(text).yellow(),); + println!("\n{}", style(text).yellow(),); } fn render_tool_request(req: &ToolRequest, theme: Theme, debug: bool) { @@ -519,10 +519,6 @@ pub fn display_greeting() { println!("\nGoose is running! Enter your instructions, or try asking what goose can do.\n"); } -pub fn display_session_history_cleared() { - println!("\n{}\n", style("Session history cleared.").dim().cyan(),); -} - #[cfg(test)] mod tests { use super::*; From 6eb126a59e1378f15e049b6fd00b369446477d32 Mon Sep 17 00:00:00 2001 From: Salman Mohammed Date: Wed, 19 Mar 2025 22:43:52 -0400 Subject: [PATCH 15/15] clippy --- crates/goose-cli/src/session/mod.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs index f22a7d341bc7..97f896a0c970 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -510,7 +510,8 @@ impl Session { let planner_response_type = classify_planner_response(plan_response.as_concat_text(), self.agent.provider().await) .await?; - Ok(match planner_response_type { + + match planner_response_type { PlannerResponseType::Plan => { println!(); let should_act = @@ -557,7 +558,9 @@ impl Session { // in the next round, the user will answer the clarifying questions self.messages.push(plan_response); } - }) + } + + Ok(()) } /// Process a single message and exit