mirror of
https://github.com/block/goose.git
synced 2026-10-19 17:43:18 +00:00
refactoring: removed create recipe in cli (#11942)
This commit is contained in:
parent
925948dc23
commit
32bf18e0c2
31 changed files with 41 additions and 498 deletions
|
|
@ -243,7 +243,6 @@ impl GooseCompleter {
|
|||
"/builtin".to_string(),
|
||||
"/mode".to_string(),
|
||||
"/model".to_string(),
|
||||
"/recipe".to_string(),
|
||||
"/new".to_string(),
|
||||
];
|
||||
commands.extend(
|
||||
|
|
|
|||
|
|
@ -28,7 +28,6 @@ pub enum InputResult {
|
|||
EndPlan,
|
||||
Clear,
|
||||
New,
|
||||
Recipe(Option<String>),
|
||||
Compact,
|
||||
ToggleFullToolOutput,
|
||||
Edit(Option<String>),
|
||||
|
|
@ -241,7 +240,6 @@ fn handle_slash_command(input: &str) -> Option<InputResult> {
|
|||
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<InputResult> {
|
|||
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<InputResult> {
|
|||
}
|
||||
}
|
||||
|
||||
fn parse_recipe_command(s: &str) -> Option<InputResult> {
|
||||
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<InputResult> {
|
||||
let parts: Vec<String> = 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 ---
|
||||
|
|
|
|||
|
|
@ -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<String>) {
|
||||
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<PathBuf, String>` - The path the recipe was saved to or an error message
|
||||
fn save_recipe(
|
||||
&self,
|
||||
recipe: &goose::recipe::Recipe,
|
||||
filepath_str: &str,
|
||||
) -> anyhow::Result<PathBuf> {
|
||||
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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Recipe> {
|
||||
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::<Value>(&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::<Result<_, _>>()?;
|
||||
|
||||
(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<String> = 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::<Value>(&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<Message> {
|
||||
// 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("<turn-context>cwd /repo</turn-context>")
|
||||
.with_metadata(MessageMetadata::agent_only().with_turn_context()),
|
||||
Message::assistant().with_text("on it"),
|
||||
]);
|
||||
|
||||
let texts: Vec<String> = 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();
|
||||
|
|
|
|||
|
|
@ -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)]
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
]
|
||||
}
|
||||
|
|
@ -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 |
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -797,7 +797,6 @@ Once you're in an interactive session (via `goose session` or `goose run --inter
|
|||
- **`/plan <message_text>`** - 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 <n> [--info] [key=value...]`** - Get prompt info or execute a prompt
|
||||
- **`/prompts [--extension <name>]`** - 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 [<name>...]`** - List available skills, or load one or more skills by name
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
<Tabs groupId="interface">
|
||||
<TabItem value="ui" label="goose Desktop" default>
|
||||
|
||||
Create a recipe from the current session or from a template.
|
||||
|
||||
<Tabs>
|
||||
<TabItem value="session" label="Current Session" default>
|
||||
1. While in the session you want to save as a recipe, click the <ChefHat className="inline" size={16} /> button at the bottom of the app
|
||||
2. In the dialog that opens, review and edit the recipe fields as needed:
|
||||
<RecipeFields />
|
||||
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
|
||||
</TabItem>
|
||||
<TabItem value="new" label="Template">
|
||||
1. Click the <PanelLeft className="inline" size={16} /> 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:
|
||||
<RecipeFields />
|
||||
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
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::warning
|
||||
You cannot create a recipe from an existing recipe session, but you can view or [edit the recipe](#edit-recipe).
|
||||
:::
|
||||
1. Click the <PanelLeft className="inline" size={16} /> 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:
|
||||
<RecipeFields />
|
||||
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
|
||||
|
||||
</TabItem>
|
||||
|
||||
<TabItem value="cli" label="goose CLI">
|
||||
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.
|
||||
|
||||
<details>
|
||||
<summary>recipe file structure</summary>
|
||||
|
|
@ -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
|
|||
</TabItem>
|
||||
</Tabs>
|
||||
|
||||
:::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
|
||||
<Tabs groupId="interface">
|
||||
<TabItem value="ui" label="goose Desktop" default>
|
||||
|
|
|
|||
|
|
@ -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
|
|||
</TabItem>
|
||||
<TabItem value="cli" label="goose CLI">
|
||||
|
||||
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.
|
||||
|
||||
</TabItem>
|
||||
</Tabs>
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -3159,7 +3159,7 @@
|
|||
"defaultMessage": "कोई सहेजी गई रेसिपी नहीं"
|
||||
},
|
||||
"recipesView.noSavedRecipesDescription": {
|
||||
"defaultMessage": "चैट से सहेजी गई रेसिपी यहां दिखाई देगी."
|
||||
"defaultMessage": "सहेजी गई रेसिपी यहां दिखाई देगी."
|
||||
},
|
||||
"recipesView.openInNewWindow": {
|
||||
"defaultMessage": "नई विंडो में खोलें"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -3159,7 +3159,7 @@
|
|||
"defaultMessage": "保存済みのレシピはありません"
|
||||
},
|
||||
"recipesView.noSavedRecipesDescription": {
|
||||
"defaultMessage": "チャットから保存したレシピがここに表示されます。"
|
||||
"defaultMessage": "保存したレシピがここに表示されます。"
|
||||
},
|
||||
"recipesView.openInNewWindow": {
|
||||
"defaultMessage": "新しいウィンドウで開く"
|
||||
|
|
|
|||
|
|
@ -3159,7 +3159,7 @@
|
|||
"defaultMessage": "저장된 레시피가 없습니다."
|
||||
},
|
||||
"recipesView.noSavedRecipesDescription": {
|
||||
"defaultMessage": "채팅에서 저장된 레시피가 여기에 표시됩니다."
|
||||
"defaultMessage": "저장된 레시피가 여기에 표시됩니다."
|
||||
},
|
||||
"recipesView.openInNewWindow": {
|
||||
"defaultMessage": "새 창에서 열기"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -3159,7 +3159,7 @@
|
|||
"defaultMessage": "Нет сохраненных рецептов"
|
||||
},
|
||||
"recipesView.noSavedRecipesDescription": {
|
||||
"defaultMessage": "Рецепты, сохраненные из чатов, появятся здесь."
|
||||
"defaultMessage": "Сохраненные рецепты появятся здесь."
|
||||
},
|
||||
"recipesView.openInNewWindow": {
|
||||
"defaultMessage": "Открыть в новом окне"
|
||||
|
|
|
|||
|
|
@ -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ç"
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -3159,7 +3159,7 @@
|
|||
"defaultMessage": "暂无已保存的配方"
|
||||
},
|
||||
"recipesView.noSavedRecipesDescription": {
|
||||
"defaultMessage": "从聊天中保存的配方会显示在这里。"
|
||||
"defaultMessage": "已保存的配方会显示在这里。"
|
||||
},
|
||||
"recipesView.openInNewWindow": {
|
||||
"defaultMessage": "在新窗口中打开"
|
||||
|
|
|
|||
|
|
@ -3159,7 +3159,7 @@
|
|||
"defaultMessage": "尚無已儲存的食譜"
|
||||
},
|
||||
"recipesView.noSavedRecipesDescription": {
|
||||
"defaultMessage": "從聊天儲存的食譜會顯示在這裡。"
|
||||
"defaultMessage": "已儲存的食譜會顯示在這裡。"
|
||||
},
|
||||
"recipesView.openInNewWindow": {
|
||||
"defaultMessage": "在新視窗中開啟"
|
||||
|
|
|
|||
|
|
@ -145,7 +145,6 @@ export type AnalyticsEvent =
|
|||
}
|
||||
| { name: 'input_mode_changed'; properties: { from_mode: string; to_mode: string } }
|
||||
| { name: 'input_diagnostics_opened'; properties: Record<string, never> }
|
||||
| { name: 'input_create_recipe_opened'; properties: Record<string, never> }
|
||||
| { name: 'input_edit_recipe_opened'; properties: Record<string, never> }
|
||||
// Auto-update tracking events
|
||||
| {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue