diff --git a/crates/goose-cli/src/session/completion.rs b/crates/goose-cli/src/session/completion.rs index e8e2dfc87d..650e7da19c 100644 --- a/crates/goose-cli/src/session/completion.rs +++ b/crates/goose-cli/src/session/completion.rs @@ -243,7 +243,6 @@ impl GooseCompleter { "/builtin".to_string(), "/mode".to_string(), "/model".to_string(), - "/recipe".to_string(), "/new".to_string(), ]; commands.extend( diff --git a/crates/goose-cli/src/session/input.rs b/crates/goose-cli/src/session/input.rs index d8a1138c61..7a2a1b693b 100644 --- a/crates/goose-cli/src/session/input.rs +++ b/crates/goose-cli/src/session/input.rs @@ -28,7 +28,6 @@ pub enum InputResult { EndPlan, Clear, New, - Recipe(Option), Compact, ToggleFullToolOutput, Edit(Option), @@ -241,7 +240,6 @@ fn handle_slash_command(input: &str) -> Option { const CMD_ENDPLAN: &str = "/endplan"; const CMD_CLEAR: &str = "/clear"; const CMD_NEW: &str = "/new"; - const CMD_RECIPE: &str = "/recipe"; const CMD_COMPACT: &str = "/compact"; const CMD_SUMMARIZE_DEPRECATED: &str = "/summarize"; const CMD_EDIT: &str = "/edit"; @@ -338,7 +336,6 @@ fn handle_slash_command(input: &str) -> Option { s if s == CMD_ENDPLAN => Some(InputResult::EndPlan), s if s == CMD_CLEAR => Some(InputResult::Clear), s if s == CMD_NEW => Some(InputResult::New), - s if s.starts_with(CMD_RECIPE) => parse_recipe_command(s), s if s == CMD_COMPACT => Some(InputResult::Compact), // Match "/skills" exactly or "/skills " with args - avoids matching e.g. "/skillsextra" s if s == CMD_SKILLS || s.starts_with(&format!("{CMD_SKILLS} ")) => { @@ -371,31 +368,6 @@ fn handle_slash_command(input: &str) -> Option { } } -fn parse_recipe_command(s: &str) -> Option { - const CMD_RECIPE: &str = "/recipe"; - - if s == CMD_RECIPE { - // No filepath provided, use default - return Some(InputResult::Recipe(None)); - } - - // Extract the filepath from the command - let filepath = s.get(CMD_RECIPE.len()..).unwrap_or("").trim(); - - if filepath.is_empty() { - return Some(InputResult::Recipe(None)); - } - - // Validate that the filepath ends with .yaml - if !filepath.to_lowercase().ends_with(".yaml") { - println!("{}", console::style("Filepath must end with .yaml").red()); - return Some(InputResult::Retry); - } - - // Return the filepath for validation in the handler - Some(InputResult::Recipe(Some(filepath.to_string()))) -} - fn parse_prompts_command(args: &str) -> Option { let parts: Vec = shlex::split(args).unwrap_or_default(); @@ -486,8 +458,6 @@ fn help_text() -> String { 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. -/recipe [filepath] - Generate a recipe from the current conversation and save it to the specified filepath (must end with .yaml). - If no filepath is provided, it will be saved to ./recipe.yaml. /compact - Compact the current conversation to reduce context length while preserving key information. {additional_builtin_help}/status - Show session status: model, provider, mode, and token usage. /edit [text] - Open your prompt editor to compose a message. Optionally pre-fill with text. @@ -845,26 +815,9 @@ mod tests { } #[test] - fn test_recipe_command() { - // Test recipe with no filepath - if let Some(InputResult::Recipe(filepath)) = handle_slash_command("/recipe") { - assert!(filepath.is_none()); - } else { - panic!("Expected Recipe"); - } - - // Test recipe with filepath - if let Some(InputResult::Recipe(filepath)) = - handle_slash_command("/recipe /path/to/file.yaml") - { - assert_eq!(filepath, Some("/path/to/file.yaml".to_string())); - } else { - panic!("Expected recipe with filepath"); - } - - // Test recipe with invalid extension - let result = handle_slash_command("/recipe /path/to/file.txt"); - assert!(matches!(result, Some(InputResult::Retry))); + fn recipe_command_is_not_builtin() { + assert!(handle_slash_command("/recipe").is_none()); + assert!(handle_slash_command("/recipe recipe.yaml").is_none()); } // --- should_use_editor_always tests --- diff --git a/crates/goose-cli/src/session/mod.rs b/crates/goose-cli/src/session/mod.rs index acd6cf1682..a6e22fcb72 100644 --- a/crates/goose-cli/src/session/mod.rs +++ b/crates/goose-cli/src/session/mod.rs @@ -31,7 +31,7 @@ use goose::providers::base::Provider; use goose::providers::base::ProviderUsage; use goose::utils::safe_truncate; -use anyhow::{Context, Result}; +use anyhow::Result; use completion::GooseCompleter; use goose::agents::extension::{Envs, ExtensionConfig, PLATFORM_EXTENSIONS}; use goose::agents::types::RetryConfig; @@ -807,10 +807,6 @@ impl CliSession { history.save(editor); self.handle_prompt_command(opts).await?; } - InputResult::Recipe(filepath_opt) => { - history.save(editor); - self.handle_recipe(filepath_opt).await; - } InputResult::Compact => { history.save(editor); self.handle_compact().await?; @@ -1299,37 +1295,6 @@ impl CliSession { Ok(new_session_id) } - async fn handle_recipe(&mut self, filepath_opt: Option) { - println!("{}", console::style("Generating Recipe").green()); - - output::show_thinking(); - let recipe = self - .agent - .create_recipe(&self.session_id, self.messages.clone()) - .await; - output::hide_thinking(); - - match recipe { - Ok(recipe) => { - let filepath_str = filepath_opt.as_deref().unwrap_or("recipe.yaml"); - match self.save_recipe(&recipe, filepath_str) { - Ok(path) => println!( - "{}", - console::style(format!("Saved recipe to {}", path.display())).green() - ), - Err(e) => println!("{}", console::style(e).red()), - } - } - Err(e) => { - println!( - "{}: {:?}", - console::style("Failed to generate recipe").red(), - e - ); - } - } - } - async fn handle_load_skills(&mut self, names: &[String]) -> Result<()> { // NOTE: We don't validate the skill names here because the load_skill tool will // handle that and provide feedback to the user if any skill names are invalid. @@ -2209,49 +2174,6 @@ impl CliSession { Ok(()) } - /// Save a recipe to a file - /// - /// # Arguments - /// * `recipe` - The recipe to save - /// * `filepath_str` - The path to save the recipe to - /// - /// # Returns - /// * `Result` - The path the recipe was saved to or an error message - fn save_recipe( - &self, - recipe: &goose::recipe::Recipe, - filepath_str: &str, - ) -> anyhow::Result { - let path_buf = PathBuf::from(filepath_str); - let mut path = path_buf.clone(); - - // Update the final path if it's relative - if path_buf.is_relative() { - // If the path is relative, resolve it relative to the current working directory - let cwd = std::env::current_dir().context("Failed to get current directory")?; - path = cwd.join(&path_buf); - } - - // Check if parent directory exists - if let Some(parent) = path.parent() { - if !parent.exists() { - return Err(anyhow::anyhow!( - "Directory '{}' does not exist", - parent.display() - )); - } - } - - // Try creating the file - let file = std::fs::File::create(path.as_path()) - .context(format!("Failed to create file '{}'", path.display()))?; - - // Write YAML - serde_yaml::to_writer(file, recipe).context("Failed to save recipe")?; - - Ok(path) - } - fn push_message(&mut self, message: Message) { self.messages.push(message); } diff --git a/crates/goose/src/agents/agent.rs b/crates/goose/src/agents/agent.rs index fe739b4684..1764758e55 100644 --- a/crates/goose/src/agents/agent.rs +++ b/crates/goose/src/agents/agent.rs @@ -49,7 +49,7 @@ use crate::agents::types::{ use crate::agents::AgentEvent; use crate::config::extensions::name_to_key; use crate::config::permission::PermissionManager; -use crate::config::{get_enabled_extensions, Config, GooseMode}; +use crate::config::{Config, GooseMode}; use crate::context_mgmt::{ check_if_compaction_needed, compact_messages, DEFAULT_COMPACTION_THRESHOLD, }; @@ -57,14 +57,12 @@ use crate::conversation::message::{ ActionRequiredData, InferenceMetadata, Message, MessageContent, MessageUsage, ProviderMetadata, SystemNotificationType, }; -use crate::conversation::{ - debug_conversation_fix, fix_conversation, merge_consecutive_messages_for_request, Conversation, -}; +use crate::conversation::{debug_conversation_fix, fix_conversation, Conversation}; use crate::permission::permission_inspector::PermissionInspector; use crate::permission::permission_judge::PermissionCheckResult; use crate::permission::{Permission, PermissionConfirmation}; use crate::providers::base::{PermissionRouting, Provider}; -use crate::recipe::{Author, Recipe, Response, Settings}; +use crate::recipe::Response; use crate::scheduler_trait::SchedulerTrait; use crate::security::adversary_inspector::AdversaryInspector; use crate::security::egress_inspector::EgressInspector; @@ -77,7 +75,6 @@ use crate::utils::is_token_cancelled; use goose_providers::conversation::token_usage::{ProviderUsage, Usage}; use goose_providers::errors::ProviderError; use goose_providers::thinking::{ThinkingEffort, ThinkingEffortSupport}; -use regex::Regex; use rmcp::model::{ CallToolRequestParams, CallToolResult, ContentBlock, ElicitationAction, ErrorCode, ErrorData, GetPromptResult, Prompt, ProtocolVersion, Tool, @@ -4000,234 +3997,6 @@ impl Agent { "plan.md", &context, )?) } - - pub async fn create_recipe( - &self, - session_id: &str, - mut messages: Conversation, - ) -> Result { - tracing::info!("Starting recipe creation with {} messages", messages.len()); - - let session = self - .config - .session_manager - .get_session(session_id, false) - .await?; - let extensions_info = self - .extension_manager - .get_extensions_info(&session.working_dir) - .await; - tracing::debug!("Retrieved {} extensions info", extensions_info.len()); - - let model_config = self.model_config_for_session(session_id).await?; - let model_name = &model_config.model_name; - tracing::debug!("Using model: {}", model_name); - - let goose_mode = *self.current_goose_mode.lock().await; - let prompt_manager = self.prompt_manager.lock().await; - let system_prompt = prompt_manager - .builder() - .with_extensions(extensions_info.into_iter()) - .with_goose_mode(goose_mode) - .build(); - - let recipe_prompt = prompt_manager.get_recipe_prompt().await; - let tools: Vec<_> = self - .extension_manager - .get_prefixed_tools(session_id, None) - .await - .map_err(|e| { - tracing::error!("Failed to get tools for recipe creation: {}", e); - e - })? - .into_iter() - .filter(super::reply_parts::is_tool_visible_to_model) - .collect(); - - messages = Conversation::new_unvalidated(recipe_conversation_history(&messages)); - messages.push(Message::user().with_text(recipe_prompt)); - - let (messages, issues) = fix_conversation(messages); - if !issues.is_empty() { - issues - .iter() - .for_each(|issue| tracing::warn!(recipe.conversation.issue = issue)); - } - let messages = Conversation::new_unvalidated(merge_consecutive_messages_for_request( - messages.messages().clone(), - )); - - tracing::debug!( - "Added recipe prompt to messages, total messages: {}", - messages.len() - ); - - tracing::info!("Calling provider to generate recipe content"); - let provider = self.provider.lock().await; - let provider = provider.as_ref().ok_or_else(|| { - let error = anyhow!("Provider not available during recipe creation"); - tracing::error!("{}", error); - error - })?; - let (result, _usage) = crate::session_context::with_session_id( - Some(session_id.to_string()), - provider.complete(&model_config, &system_prompt, messages.messages(), &tools), - ) - .await - .map_err(|e| { - tracing::error!("Provider completion failed during recipe creation: {}", e); - e - })?; - - let content = result.as_concat_text(); - tracing::debug!( - "Provider returned content with {} characters", - content.len() - ); - - // the response may be contained in ```json ```, strip that before parsing json - let re = Regex::new(r"(?s)```[^\n]*\n(.*?)\n```").unwrap(); - let clean_content = re - .captures(&content) - .and_then(|caps| caps.get(1).map(|m| m.as_str())) - .unwrap_or(&content) - .trim() - .to_string(); - - let (instructions, activities) = - if let Ok(json_content) = serde_json::from_str::(&clean_content) { - let instructions = json_content - .get("instructions") - .ok_or_else(|| anyhow!("Missing 'instructions' in json response"))? - .as_str() - .ok_or_else(|| anyhow!("instructions' is not a string"))? - .to_string(); - - let activities = json_content - .get("activities") - .ok_or_else(|| anyhow!("Missing 'activities' in json response"))? - .as_array() - .ok_or_else(|| anyhow!("'activities' is not an array'"))? - .iter() - .map(|act| { - act.as_str() - .map(|s| s.to_string()) - .ok_or(anyhow!("'activities' array element is not a string")) - }) - .collect::>()?; - - (instructions, activities) - } else { - tracing::warn!("Failed to parse JSON, falling back to string parsing"); - // If we can't get valid JSON, try string parsing - // Use split_once to get the content after "Instructions:". - let after_instructions = content - .split_once("instructions:") - .map(|(_, rest)| rest) - .unwrap_or(&content); - - // Split once more to separate instructions from activities. - let (instructions_part, activities_text) = after_instructions - .split_once("activities:") - .unwrap_or((after_instructions, "")); - - let instructions = instructions_part - .trim_end_matches(|c: char| c.is_whitespace() || c == '#') - .trim() - .to_string(); - let activities_text = activities_text.trim(); - - // Regex to remove bullet markers or numbers with an optional dot. - let bullet_re = Regex::new(r"^[•\-*\d]+\.?\s*").expect("Invalid regex"); - - // Process each line in the activities section. - let activities: Vec = activities_text - .lines() - .map(|line| bullet_re.replace(line, "").to_string()) - .map(|s| s.trim().to_string()) - .filter(|line| !line.is_empty()) - .collect(); - - (instructions, activities) - }; - - let extension_configs = get_enabled_extensions(); - - let author = Author { - contact: std::env::var("USER") - .or_else(|_| std::env::var("USERNAME")) - .ok(), - metadata: None, - }; - - // Ideally we'd get the name of the provider we are using from the provider itself, - // but it doesn't know and the plumbing looks complicated. - let config = Config::global(); - let provider_name: String = config - .get_goose_provider() - .expect("No provider configured. Run 'goose configure' first"); - - let settings = Settings { - goose_provider: Some(provider_name.clone()), - goose_model: Some(model_name.clone()), - temperature: Some(model_config.temperature.unwrap_or(0.0)), - max_turns: None, - }; - - tracing::debug!( - "Building recipe with {} activities and {} extensions", - activities.len(), - extension_configs.len() - ); - - let (title, description) = - if let Ok(json_content) = serde_json::from_str::(&clean_content) { - let title = json_content - .get("title") - .and_then(|t| t.as_str()) - .unwrap_or("Custom recipe from chat") - .to_string(); - - let description = json_content - .get("description") - .and_then(|d| d.as_str()) - .unwrap_or("a custom recipe instance from this chat session") - .to_string(); - - (title, description) - } else { - ( - "Custom recipe from chat".to_string(), - "a custom recipe instance from this chat session".to_string(), - ) - }; - - let recipe = Recipe::builder() - .title(title) - .description(description) - .instructions(instructions) - .activities(activities) - .extensions(extension_configs) - .settings(settings) - .author(author) - .build() - .map_err(|e| { - tracing::error!("Failed to build recipe: {}", e); - anyhow!("Recipe build failed: {}", e) - })?; - - tracing::info!("Recipe creation completed successfully"); - Ok(recipe) - } -} - -fn recipe_conversation_history(messages: &Conversation) -> Vec { - // The recipe prompt has no turn-context instructions; drop the blocks. - messages - .agent_visible_messages() - .into_iter() - .filter(|message| !message.is_turn_context()) - .collect() } #[cfg(test)] @@ -4318,25 +4087,6 @@ mod tests { ); } - #[test] - fn recipe_history_excludes_turn_context_events() { - use crate::conversation::message::MessageMetadata; - - let history = Conversation::new_unvalidated([ - Message::user().with_text("build me a recipe"), - Message::user() - .with_text("cwd /repo") - .with_metadata(MessageMetadata::agent_only().with_turn_context()), - Message::assistant().with_text("on it"), - ]); - - let texts: Vec = recipe_conversation_history(&history) - .iter() - .map(|message| message.as_concat_text()) - .collect(); - assert_eq!(texts, ["build me a recipe", "on it"]); - } - async fn tracing_test_agent_and_session() -> (Agent, Session, TempDir) { let data_dir = TempDir::new().unwrap(); let data_path = data_dir.path().to_path_buf(); diff --git a/crates/goose/src/agents/prompt_manager.rs b/crates/goose/src/agents/prompt_manager.rs index a319050553..52f2dad368 100644 --- a/crates/goose/src/agents/prompt_manager.rs +++ b/crates/goose/src/agents/prompt_manager.rs @@ -3,8 +3,6 @@ use chrono::DateTime; use chrono::Utc; use indexmap::IndexMap; use serde::Serialize; -use serde_json::Value; -use std::collections::HashMap; use crate::agents::{extension::ExtensionInfo, moim}; use crate::hints::load_hints::build_gitignore; @@ -266,12 +264,6 @@ impl PromptManager { goose_mode: None, } } - - pub async fn get_recipe_prompt(&self) -> String { - let context: HashMap<&str, Value> = HashMap::new(); - prompt_template::render_template("recipe.md", &context) - .unwrap_or_else(|_| "The recipe prompt is busted. Tell the user.".to_string()) - } } #[cfg(test)] diff --git a/crates/goose/src/prompt_template.rs b/crates/goose/src/prompt_template.rs index cd0055d818..787d22525f 100644 --- a/crates/goose/src/prompt_template.rs +++ b/crates/goose/src/prompt_template.rs @@ -23,10 +23,6 @@ static TEMPLATE_REGISTRY: &[(&str, &str)] = &[ "subagent_system.md", "System prompt for subagents spawned to handle specific tasks", ), - ( - "recipe.md", - "Prompt for generating recipe files from conversations", - ), ( "apps_create.md", "Prompt for generating new Goose apps based on the user instructions", diff --git a/crates/goose/src/prompts/recipe.md b/crates/goose/src/prompts/recipe.md deleted file mode 100644 index 9f9d4aa082..0000000000 --- a/crates/goose/src/prompts/recipe.md +++ /dev/null @@ -1,20 +0,0 @@ -Based on our conversation so far, could you create: - -1. A concise title (5-10 words) that captures the main topic or task -2. A brief description (1-2 sentences) that summarizes what this recipe helps with -3. A concise set of instructions (1-2 paragraphs) that describe what you've been helping with. Make the instructions generic, and higher-level so that can be re-used across various similar tasks. Pay special attention if any output styles or formats are requested (and make it clear), and note any non standard tools used or required. -4. A list of 3-5 example activities (as a few words each at most) that would be relevant to this topic - -Format your response in _VALID_ json, with keys being `title`, `description`, `instructions` (string), and `activities` (array of strings). -For example, perhaps we have been discussing fruit and you might write: - -{ -"title": "Fruit Information Assistant", -"description": "A recipe for finding and sharing information about different types of fruit.", -"instructions": "Using web searches we find pictures of fruit, and always check what language to reply in.", -"activities": [ -"Show pics of apples", -"say a random fruit", -"share a fruit fact" -] -} diff --git a/documentation/docs/guides/context-engineering/prompt-templates.md b/documentation/docs/guides/context-engineering/prompt-templates.md index 2ade1cf6f1..d9aa1f2d97 100644 --- a/documentation/docs/guides/context-engineering/prompt-templates.md +++ b/documentation/docs/guides/context-engineering/prompt-templates.md @@ -96,7 +96,6 @@ The following default templates can be customized. | [compaction.md](https://github.com/aaif-goose/goose/blob/main/crates/goose/src/prompts/compaction.md) | Prompt for summarizing conversation history when context limits are reached | Desktop and CLI | | [permission_judge.md](https://github.com/aaif-goose/goose/blob/main/crates/goose/src/prompts/permission_judge.md) | Prompt for analyzing tool operations for read-only detection | Desktop and CLI | | [plan.md](https://github.com/aaif-goose/goose/blob/main/crates/goose/src/prompts/plan.md) | Instructions for creating detailed, actionable plans with clarifying questions | CLI only | -| [recipe.md](https://github.com/aaif-goose/goose/blob/main/crates/goose/src/prompts/recipe.md) | Prompt for generating recipe files from conversations | Desktop and CLI | | [subagent_system.md](https://github.com/aaif-goose/goose/blob/main/crates/goose/src/prompts/subagent_system.md) | System prompt for subagents spawned to handle specific tasks | Desktop and CLI | | [tiny_model_system.md](https://github.com/aaif-goose/goose/blob/main/crates/goose/src/prompts/tiny_model_system.md) | System prompt for tiny local models using shell command emulation | CLI | | [session_name.md](https://github.com/aaif-goose/goose/blob/main/crates/goose/src/prompts/session_name.md) | Prompt for generating short session names from conversation history | Desktop and CLI | diff --git a/documentation/docs/guides/context-engineering/slash-commands.md b/documentation/docs/guides/context-engineering/slash-commands.md index 602d5167ab..01432f7941 100644 --- a/documentation/docs/guides/context-engineering/slash-commands.md +++ b/documentation/docs/guides/context-engineering/slash-commands.md @@ -85,7 +85,7 @@ When you run a recipe using a slash command, the recipe's instructions and promp - Slash commands accept only one [parameter](/docs/guides/recipes/recipe-reference#parameters). Any additional parameters in the recipe must have default values. - Command names are case-insensitive (`/Bug` and `/bug` are treated as the same command). - Command names must be unique and contain no spaces. -- You cannot use names that conflict with [built-in CLI slash commands](/docs/guides/goose-cli-commands#slash-commands) like `/recipe`, `/compact`, or `/help`. +- You cannot use names that conflict with [built-in CLI slash commands](/docs/guides/goose-cli-commands#slash-commands) like `/compact` or `/help`. - If the recipe file is missing or invalid, the command will be treated as regular text sent to the model. ## Additional Resources diff --git a/documentation/docs/guides/goose-cli-commands.md b/documentation/docs/guides/goose-cli-commands.md index bbb92c374d..554e6f5c44 100644 --- a/documentation/docs/guides/goose-cli-commands.md +++ b/documentation/docs/guides/goose-cli-commands.md @@ -797,7 +797,6 @@ Once you're in an interactive session (via `goose session` or `goose run --inter - **`/plan `** - Enter 'plan' mode with optional message. Create a plan based on the current messages and ask user if they want to act on it - **`/prompt [--info] [key=value...]`** - Get prompt info or execute a prompt - **`/prompts [--extension ]`** - List all available prompts, optionally filtered by extension -- **`/recipe [filepath]`** - Generate a recipe from the current conversation and save it to the specified filepath (must end with .yaml). If no filepath is provided, it will be saved to ./recipe.yaml - **`/compact`** - Compact and summarize the current conversation to reduce context length while preserving key information - **`/r`** - Toggle full tool output display (show complete tool parameters without truncation) - **`/skills [...]`** - List available skills, or load one or more skills by name diff --git a/documentation/docs/guides/recipes/session-recipes.md b/documentation/docs/guides/recipes/session-recipes.md index 4ecf314996..3d85bd7463 100644 --- a/documentation/docs/guides/recipes/session-recipes.md +++ b/documentation/docs/guides/recipes/session-recipes.md @@ -1,7 +1,7 @@ --- sidebar_position: 1 title: Reusable Recipes -description: "Share a goose session setup (including tools, goals, and instructions) as a reusable recipe that others can launch with a single click" +description: "Package tools, goals, and instructions as a reusable recipe that others can launch with a single click" --- import Tabs from '@theme/Tabs'; @@ -9,57 +9,27 @@ import TabItem from '@theme/TabItem'; import { PanelLeft, ChefHat, SquarePen, Link, Clock, Terminal, Share2 } from 'lucide-react'; import RecipeFields from '@site/src/components/RecipeFields'; -Sometimes you finish a task in goose and realize, "Hey, this setup could be useful again." Maybe you have curated a great combination of tools, defined a clear goal, and want to preserve that flow. Or maybe you're trying to help someone else replicate what you just did without walking them through it step by step. - -You can turn your current goose session into a reusable recipe that includes the tools, goals, and setup you're using right now and package it into a new Agent that others (or future you) can launch with a single click. +Recipes package tools, goals, and instructions into reusable workflows that others (or future you) can launch with a single click. ## Create Recipe - Create a recipe from the current session or from a template. - - - - 1. While in the session you want to save as a recipe, click the button at the bottom of the app - 2. In the dialog that opens, review and edit the recipe fields as needed: - - 3. When you're finished, you can: - - Click `Create Recipe` to save the recipe to your Recipe Library - - Click `Create & Run Recipe` to save and immediately run the recipe in a new session - - - 1. Click the button in the top-left to open the sidebar - 2. Click `Recipes` in the sidebar - 3. Click `Create Recipe` - 4. In the dialog that opens, fill in the recipe fields as needed: - - 5. When you're finished, you can: - - Copy the recipe link to share the recipe with others - - Click `Save Recipe` to save the recipe to your Recipe Library - - Click `Save & Run Recipe` to save and immediately run the recipe in a new session - - - - :::warning - You cannot create a recipe from an existing recipe session, but you can view or [edit the recipe](#edit-recipe). - ::: + 1. Click the button in the top-left to open the sidebar + 2. Click `Recipes` in the sidebar + 3. Click `Create Recipe` + 4. In the dialog that opens, fill in the recipe fields as needed: + + 5. When you're finished, you can: + - Copy the recipe link to share the recipe with others + - Click `Save Recipe` to save the recipe to your Recipe Library + - Click `Save & Run Recipe` to save and immediately run the recipe in a new session - Recipe files can be either JSON (.json) or YAML (.yaml) files. While in a [session](/docs/guides/sessions/session-management#start-session), run this command to generate a recipe.yaml file in your current directory: - - ```sh - /recipe - ``` - - If you want to specify a different name, you can provide it as an argument: - - ```sh - /recipe my-custom-recipe.yaml - ``` + Create a JSON or YAML file in your editor using the recipe structure below.
recipe file structure @@ -92,10 +62,6 @@ You can turn your current goose session into a reusable recipe that includes the For detailed descriptions and example configurations of all recipe fields, see the [Recipe Reference Guide](/docs/guides/recipes/recipe-reference). - :::warning - You cannot create a recipe from an existing recipe session - the `/recipe` command will not work. - ::: - :::tip Validate Your Recipe You should [validate your recipe](#validate-recipe) to verify that it's complete and properly formatted. ::: @@ -161,10 +127,6 @@ You can turn your current goose session into a reusable recipe that includes the -:::tip Customize Recipe Generation -You can customize how goose generates recipes by editing the `recipe.md` [prompt template](/docs/guides/context-engineering/prompt-templates). -::: - ## Edit Recipe diff --git a/documentation/docs/guides/recipes/storing-recipes.md b/documentation/docs/guides/recipes/storing-recipes.md index a761484c99..4a12ac59cc 100644 --- a/documentation/docs/guides/recipes/storing-recipes.md +++ b/documentation/docs/guides/recipes/storing-recipes.md @@ -44,8 +44,8 @@ Before saving recipes, it's important to understand where they can be stored and **Save New Recipe:** -1. To create a recipe from your chat session, see [Create Recipe](/docs/guides/recipes/session-recipes#create-recipe) -2. Once in the Recipe Editor, click `Save Recipe` to save it to your Recipe Library +1. Open `Recipes` from the sidebar and click `Create Recipe` +2. Complete the Recipe Editor, then click `Save Recipe` to save it to your Recipe Library **Save Modified Recipe:** @@ -61,15 +61,7 @@ When you modify and save a recipe with a new name, a new recipe and new link are - When you [create a recipe](/docs/guides/recipes/recipe-reference), it gets saved to: - - * Your working directory by default: `./recipe.yaml` - * Any path you specify: `/recipe /path/to/my-recipe.yaml` - * Local project recipes: `/recipe .goose/recipes/my-recipe.yaml` - - :::note - The CLI saves recipes as `.yaml` files. While the CLI can run recipes in `.json` format, it does not provide an option to save recipes as JSON. - ::: + Create recipe files with your preferred editor. Store project-specific recipes in `.goose/recipes/` and global recipes in `~/.config/goose/recipes/`. The CLI can run recipes in either YAML or JSON format. diff --git a/documentation/docs/guides/tips.md b/documentation/docs/guides/tips.md index 1051cc4432..7594a5f5c4 100644 --- a/documentation/docs/guides/tips.md +++ b/documentation/docs/guides/tips.md @@ -46,7 +46,7 @@ Commit your code changes early and often. This allows you to rollback any unexpe Administrators can use an [allowlist](/docs/guides/allowlist) to restrict goose to approved extensions only. This helps prevent risky installs from unknown MCP servers. ### Set up starter templates -You can turn a successful session into a reusable "[recipe](/docs/guides/recipes/session-recipes)" to share with others or use again later—no need to start from scratch. +Create a reusable [recipe](/docs/guides/recipes/session-recipes) for workflows you want to share or run again later. ### Embrace an experimental mindset You don’t need to get it right the first time. Iterating on prompts and tools is part of the workflow. diff --git a/ui/desktop/src/components/recipes/RecipesView.tsx b/ui/desktop/src/components/recipes/RecipesView.tsx index 62c921ceee..377ef68949 100644 --- a/ui/desktop/src/components/recipes/RecipesView.tsx +++ b/ui/desktop/src/components/recipes/RecipesView.tsx @@ -234,7 +234,7 @@ const i18n = defineMessages({ }, noSavedRecipesDescription: { id: 'recipesView.noSavedRecipesDescription', - defaultMessage: 'Recipe saved from chats will show up here.', + defaultMessage: 'Saved recipes will show up here.', }, noMatchingRecipes: { id: 'recipesView.noMatchingRecipes', diff --git a/ui/desktop/src/i18n/messages/de.json b/ui/desktop/src/i18n/messages/de.json index 56f6a6a375..ae2cfda278 100644 --- a/ui/desktop/src/i18n/messages/de.json +++ b/ui/desktop/src/i18n/messages/de.json @@ -3159,7 +3159,7 @@ "defaultMessage": "Keine gespeicherten Rezepte" }, "recipesView.noSavedRecipesDescription": { - "defaultMessage": "Aus Chats gespeicherte Rezepte werden hier angezeigt." + "defaultMessage": "Gespeicherte Rezepte werden hier angezeigt." }, "recipesView.openInNewWindow": { "defaultMessage": "In neuem Fenster öffnen" diff --git a/ui/desktop/src/i18n/messages/en.json b/ui/desktop/src/i18n/messages/en.json index 748eb98271..c959a59d11 100644 --- a/ui/desktop/src/i18n/messages/en.json +++ b/ui/desktop/src/i18n/messages/en.json @@ -3207,7 +3207,7 @@ "defaultMessage": "No saved recipes" }, "recipesView.noSavedRecipesDescription": { - "defaultMessage": "Recipe saved from chats will show up here." + "defaultMessage": "Saved recipes will show up here." }, "recipesView.openInNewWindow": { "defaultMessage": "Open in new window" diff --git a/ui/desktop/src/i18n/messages/es.json b/ui/desktop/src/i18n/messages/es.json index 1e1e5a0063..977d8fa653 100644 --- a/ui/desktop/src/i18n/messages/es.json +++ b/ui/desktop/src/i18n/messages/es.json @@ -3159,7 +3159,7 @@ "defaultMessage": "No hay recetas guardadas" }, "recipesView.noSavedRecipesDescription": { - "defaultMessage": "Las recetas guardadas desde los chats aparecerán aquí." + "defaultMessage": "Las recetas guardadas aparecerán aquí." }, "recipesView.openInNewWindow": { "defaultMessage": "Abrir en una ventana nueva" diff --git a/ui/desktop/src/i18n/messages/fr.json b/ui/desktop/src/i18n/messages/fr.json index 2ab5777157..759d23994e 100644 --- a/ui/desktop/src/i18n/messages/fr.json +++ b/ui/desktop/src/i18n/messages/fr.json @@ -3159,7 +3159,7 @@ "defaultMessage": "Aucune recette enregistrée" }, "recipesView.noSavedRecipesDescription": { - "defaultMessage": "Les recettes enregistrées à partir des conversations apparaîtront ici." + "defaultMessage": "Les recettes enregistrées apparaîtront ici." }, "recipesView.openInNewWindow": { "defaultMessage": "Ouvrir dans une nouvelle fenêtre" diff --git a/ui/desktop/src/i18n/messages/hi.json b/ui/desktop/src/i18n/messages/hi.json index b02e731f43..bd60f31856 100644 --- a/ui/desktop/src/i18n/messages/hi.json +++ b/ui/desktop/src/i18n/messages/hi.json @@ -3159,7 +3159,7 @@ "defaultMessage": "कोई सहेजी गई रेसिपी नहीं" }, "recipesView.noSavedRecipesDescription": { - "defaultMessage": "चैट से सहेजी गई रेसिपी यहां दिखाई देगी." + "defaultMessage": "सहेजी गई रेसिपी यहां दिखाई देगी." }, "recipesView.openInNewWindow": { "defaultMessage": "नई विंडो में खोलें" diff --git a/ui/desktop/src/i18n/messages/id.json b/ui/desktop/src/i18n/messages/id.json index 9a936ed083..be47af36a8 100644 --- a/ui/desktop/src/i18n/messages/id.json +++ b/ui/desktop/src/i18n/messages/id.json @@ -3159,7 +3159,7 @@ "defaultMessage": "Tidak ada resep tersimpan" }, "recipesView.noSavedRecipesDescription": { - "defaultMessage": "Resep yang disimpan dari obrolan akan muncul di sini." + "defaultMessage": "Resep yang disimpan akan muncul di sini." }, "recipesView.openInNewWindow": { "defaultMessage": "Buka di jendela baru" diff --git a/ui/desktop/src/i18n/messages/it.json b/ui/desktop/src/i18n/messages/it.json index bf7b704dc8..db569e39ac 100644 --- a/ui/desktop/src/i18n/messages/it.json +++ b/ui/desktop/src/i18n/messages/it.json @@ -3159,7 +3159,7 @@ "defaultMessage": "Nessuna ricetta salvata" }, "recipesView.noSavedRecipesDescription": { - "defaultMessage": "Le ricette salvate dalle chat verranno visualizzate qui." + "defaultMessage": "Le ricette salvate verranno visualizzate qui." }, "recipesView.openInNewWindow": { "defaultMessage": "Apri in una nuova finestra" diff --git a/ui/desktop/src/i18n/messages/ja.json b/ui/desktop/src/i18n/messages/ja.json index 14dfe6f4fe..0614eca2bb 100644 --- a/ui/desktop/src/i18n/messages/ja.json +++ b/ui/desktop/src/i18n/messages/ja.json @@ -3159,7 +3159,7 @@ "defaultMessage": "保存済みのレシピはありません" }, "recipesView.noSavedRecipesDescription": { - "defaultMessage": "チャットから保存したレシピがここに表示されます。" + "defaultMessage": "保存したレシピがここに表示されます。" }, "recipesView.openInNewWindow": { "defaultMessage": "新しいウィンドウで開く" diff --git a/ui/desktop/src/i18n/messages/ko.json b/ui/desktop/src/i18n/messages/ko.json index f6765194de..3efc90efe6 100644 --- a/ui/desktop/src/i18n/messages/ko.json +++ b/ui/desktop/src/i18n/messages/ko.json @@ -3159,7 +3159,7 @@ "defaultMessage": "저장된 레시피가 없습니다." }, "recipesView.noSavedRecipesDescription": { - "defaultMessage": "채팅에서 저장된 레시피가 여기에 표시됩니다." + "defaultMessage": "저장된 레시피가 여기에 표시됩니다." }, "recipesView.openInNewWindow": { "defaultMessage": "새 창에서 열기" diff --git a/ui/desktop/src/i18n/messages/ms.json b/ui/desktop/src/i18n/messages/ms.json index c8f4588b4a..a42eefe6e7 100644 --- a/ui/desktop/src/i18n/messages/ms.json +++ b/ui/desktop/src/i18n/messages/ms.json @@ -3159,7 +3159,7 @@ "defaultMessage": "Tiada resipi disimpan" }, "recipesView.noSavedRecipesDescription": { - "defaultMessage": "Resipi yang disimpan daripada sembang akan dipaparkan di sini." + "defaultMessage": "Resipi yang disimpan akan dipaparkan di sini." }, "recipesView.openInNewWindow": { "defaultMessage": "Buka dalam tetingkap baharu" diff --git a/ui/desktop/src/i18n/messages/pt.json b/ui/desktop/src/i18n/messages/pt.json index 2ac538659b..1bfa99004e 100644 --- a/ui/desktop/src/i18n/messages/pt.json +++ b/ui/desktop/src/i18n/messages/pt.json @@ -3159,7 +3159,7 @@ "defaultMessage": "Sem receitas guardadas" }, "recipesView.noSavedRecipesDescription": { - "defaultMessage": "As receitas guardadas a partir de conversas aparecerão aqui." + "defaultMessage": "As receitas guardadas aparecerão aqui." }, "recipesView.openInNewWindow": { "defaultMessage": "Abrir numa nova janela" diff --git a/ui/desktop/src/i18n/messages/ru.json b/ui/desktop/src/i18n/messages/ru.json index 528c049f25..e867b60cac 100644 --- a/ui/desktop/src/i18n/messages/ru.json +++ b/ui/desktop/src/i18n/messages/ru.json @@ -3159,7 +3159,7 @@ "defaultMessage": "Нет сохраненных рецептов" }, "recipesView.noSavedRecipesDescription": { - "defaultMessage": "Рецепты, сохраненные из чатов, появятся здесь." + "defaultMessage": "Сохраненные рецепты появятся здесь." }, "recipesView.openInNewWindow": { "defaultMessage": "Открыть в новом окне" diff --git a/ui/desktop/src/i18n/messages/tr.json b/ui/desktop/src/i18n/messages/tr.json index 663359c0a2..c2563ce8be 100644 --- a/ui/desktop/src/i18n/messages/tr.json +++ b/ui/desktop/src/i18n/messages/tr.json @@ -3159,7 +3159,7 @@ "defaultMessage": "Kayıtlı tarif yok" }, "recipesView.noSavedRecipesDescription": { - "defaultMessage": "Sohbetlerden kaydedilen tarif burada görünecek." + "defaultMessage": "Kaydedilen tarifler burada görünecek." }, "recipesView.openInNewWindow": { "defaultMessage": "Yeni pencerede aç" diff --git a/ui/desktop/src/i18n/messages/vi.json b/ui/desktop/src/i18n/messages/vi.json index 8ca723139c..e3085445d0 100644 --- a/ui/desktop/src/i18n/messages/vi.json +++ b/ui/desktop/src/i18n/messages/vi.json @@ -3159,7 +3159,7 @@ "defaultMessage": "Chưa có công thức nào được lưu" }, "recipesView.noSavedRecipesDescription": { - "defaultMessage": "Các công thức được lưu từ cuộc trò chuyện sẽ hiển thị ở đây." + "defaultMessage": "Các công thức đã lưu sẽ hiển thị ở đây." }, "recipesView.openInNewWindow": { "defaultMessage": "Mở trong cửa sổ mới" diff --git a/ui/desktop/src/i18n/messages/zh-CN.json b/ui/desktop/src/i18n/messages/zh-CN.json index 497036fb4b..5a2a348235 100644 --- a/ui/desktop/src/i18n/messages/zh-CN.json +++ b/ui/desktop/src/i18n/messages/zh-CN.json @@ -3159,7 +3159,7 @@ "defaultMessage": "暂无已保存的配方" }, "recipesView.noSavedRecipesDescription": { - "defaultMessage": "从聊天中保存的配方会显示在这里。" + "defaultMessage": "已保存的配方会显示在这里。" }, "recipesView.openInNewWindow": { "defaultMessage": "在新窗口中打开" diff --git a/ui/desktop/src/i18n/messages/zh-TW.json b/ui/desktop/src/i18n/messages/zh-TW.json index ae886987cf..d0d4c98740 100644 --- a/ui/desktop/src/i18n/messages/zh-TW.json +++ b/ui/desktop/src/i18n/messages/zh-TW.json @@ -3159,7 +3159,7 @@ "defaultMessage": "尚無已儲存的食譜" }, "recipesView.noSavedRecipesDescription": { - "defaultMessage": "從聊天儲存的食譜會顯示在這裡。" + "defaultMessage": "已儲存的食譜會顯示在這裡。" }, "recipesView.openInNewWindow": { "defaultMessage": "在新視窗中開啟" diff --git a/ui/desktop/src/utils/analytics.ts b/ui/desktop/src/utils/analytics.ts index ab25e1defb..dfa20d6365 100644 --- a/ui/desktop/src/utils/analytics.ts +++ b/ui/desktop/src/utils/analytics.ts @@ -145,7 +145,6 @@ export type AnalyticsEvent = } | { name: 'input_mode_changed'; properties: { from_mode: string; to_mode: string } } | { name: 'input_diagnostics_opened'; properties: Record } - | { name: 'input_create_recipe_opened'; properties: Record } | { name: 'input_edit_recipe_opened'; properties: Record } // Auto-update tracking events | {