-
Notifications
You must be signed in to change notification settings - Fork 5.9k
feat: add /plan command in CLI to invoke reasoner with plan system prompt #1616
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 8 commits
2ffd55b
858c1f3
bff1e70
61235ac
066e63e
2cc75d7
e63c662
a5b1c76
bc030a6
2d718ca
ead7c39
694eb07
679c44a
fab907c
8a1a950
e52775b
6eb126a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -15,6 +15,7 @@ pub enum InputResult { | |
| ListPrompts(Option<String>), | ||
| PromptCommand(PromptCommandOptions), | ||
| GooseMode(String), | ||
| Plan(PlanCommandOptions), | ||
| } | ||
|
|
||
| #[derive(Debug)] | ||
|
|
@@ -24,6 +25,12 @@ pub struct PromptCommandOptions { | |
| pub arguments: HashMap<String, String>, | ||
| } | ||
|
|
||
| #[derive(Debug)] | ||
| pub struct PlanCommandOptions { | ||
| pub model: String, | ||
| pub message_text: String, | ||
| } | ||
|
|
||
| pub fn get_input( | ||
| editor: &mut Editor<GooseCompleter, rustyline::history::DefaultHistory>, | ||
| ) -> Result<InputResult> { | ||
|
|
@@ -72,6 +79,7 @@ fn handle_slash_command(input: &str) -> Option<InputResult> { | |
| const CMD_EXTENSION: &str = "/extension "; | ||
| const CMD_BUILTIN: &str = "/builtin "; | ||
| const CMD_MODE: &str = "/mode "; | ||
| const CMD_PLAN: &str = "/plan"; | ||
|
|
||
| match input { | ||
| "/exit" | "/quit" => Some(InputResult::Exit), | ||
|
|
@@ -111,6 +119,7 @@ fn handle_slash_command(input: &str) -> Option<InputResult> { | |
| s if s.starts_with(CMD_MODE) => { | ||
| Some(InputResult::GooseMode(s[CMD_MODE.len()..].to_string())) | ||
| } | ||
| s if s.starts_with(CMD_PLAN) => parse_plan_command(s[CMD_PLAN.len()..].trim().to_string()), | ||
| _ => None, | ||
| } | ||
| } | ||
|
|
@@ -168,16 +177,51 @@ fn parse_prompt_command(args: &str) -> Option<InputResult> { | |
| Some(InputResult::PromptCommand(options)) | ||
| } | ||
|
|
||
| fn parse_plan_command(input: String) -> Option<InputResult> { | ||
| let mut options = PlanCommandOptions { | ||
| model: String::new(), | ||
| message_text: String::new(), | ||
| }; | ||
|
|
||
| let parts: Vec<String> = 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=<model> <message_text>"); | ||
| 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=<model> <message_text>"); | ||
| return None; | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Same as above |
||
| } | ||
|
|
||
| Some(InputResult::Plan(options)) | ||
| } | ||
|
|
||
| fn print_help() { | ||
| println!( | ||
| "Available commands: | ||
| /exit or /quit - Exit the session | ||
| /t - Toggle Light/Dark/Ansi theme | ||
| /extension <command> - Add a stdio extension (format: ENV1=val1 command args...) | ||
| /builtin <names> - Add builtin extensions by name (comma-separated) | ||
| /prompts [--extension <name>] - List all available prompts, optionally filtered by extension | ||
| /prompts [--extension <n>] - List all available prompts, optionally filtered by extension | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. minor: why i guess
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this was probably copilot making random change |
||
| /prompt <n> [--info] [key=value...] - Get prompt info or execute a prompt | ||
| /mode <name> - Set the goose mode to use ('auto', 'approve', 'chat') | ||
| /mode <n> - Set the goose mode to use ('auto', 'approve', 'chat') | ||
| /plan --model=<model> <message_text> - 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: | ||
|
|
@@ -370,4 +414,39 @@ mod tests { | |
| panic!("Expected PromptCommand"); | ||
| } | ||
| } | ||
|
|
||
| #[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"); | ||
| 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"), | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -349,6 +349,70 @@ impl Session { | |
| println!("Goose mode set to '{}'", mode); | ||
| continue; | ||
| } | ||
| 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; | ||
| 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(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. do we have to validate the effort - high / medium / low only?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this should be validated in the openai provider |
||
| 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; | ||
| } | ||
|
|
||
| use goose::model::ModelConfig; | ||
| use goose::providers::create; | ||
|
|
||
| // TODO: hacky to create a new provider for the planner each time plan is called | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is the reasoning model only used for the /plan call? Assuming we switch back to the prior model_config after planning is finished?
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes right now its only used for the |
||
| 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?; | ||
| 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?") | ||
|
salman1993 marked this conversation as resolved.
Outdated
|
||
| .initial_value(true) | ||
| .interact()?; | ||
|
|
||
| if confirmed { | ||
| // clear the messages before the plan | ||
| self.messages.clear(); | ||
|
salman1993 marked this conversation as resolved.
Outdated
|
||
| 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); | ||
| } | ||
| } | ||
| input::InputResult::PromptCommand(opts) => { | ||
| save_history(&mut editor); | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you just type
/planand hit enter you get this message and then the agent loop continues:Probably want to return
Some(InputResult::Retry)to put them back into the prompt