Recipe slash command parsing (#6173)

This commit is contained in:
David Katz
2025-12-19 13:15:56 -05:00
committed by GitHub
parent 41588e8795
commit bf95995e1d
2 changed files with 82 additions and 20 deletions
+11 -3
View File
@@ -797,7 +797,15 @@ impl Agent {
.await; .await;
match command_result { match command_result {
Some(response) if response.role == rmcp::model::Role::Assistant => { Err(e) => {
let error_message = Message::assistant()
.with_text(e.to_string())
.with_visibility(true, false);
return Ok(Box::pin(stream::once(async move {
Ok(AgentEvent::Message(error_message))
})));
}
Ok(Some(response)) if response.role == rmcp::model::Role::Assistant => {
SessionManager::add_message( SessionManager::add_message(
&session_config.id, &session_config.id,
&user_message.clone().with_visibility(true, false), &user_message.clone().with_visibility(true, false),
@@ -830,7 +838,7 @@ impl Agent {
} }
})); }));
} }
Some(resolved_message) => { Ok(Some(resolved_message)) => {
SessionManager::add_message( SessionManager::add_message(
&session_config.id, &session_config.id,
&user_message.clone().with_visibility(true, false), &user_message.clone().with_visibility(true, false),
@@ -842,7 +850,7 @@ impl Agent {
) )
.await?; .await?;
} }
None => { Ok(None) => {
SessionManager::add_message(&session_config.id, &user_message).await?; SessionManager::add_message(&session_config.id, &user_message).await?;
} }
} }
+71 -17
View File
@@ -41,7 +41,11 @@ pub fn list_commands() -> &'static [CommandDef] {
} }
impl Agent { impl Agent {
pub async fn execute_command(&self, message_text: &str, session_id: &str) -> Option<Message> { pub async fn execute_command(
&self,
message_text: &str,
session_id: &str,
) -> Result<Option<Message>> {
let mut trimmed = message_text.trim().to_string(); let mut trimmed = message_text.trim().to_string();
if COMPACT_TRIGGERS.contains(&trimmed.as_str()) { if COMPACT_TRIGGERS.contains(&trimmed.as_str()) {
@@ -49,37 +53,30 @@ impl Agent {
} }
if !trimmed.starts_with('/') { if !trimmed.starts_with('/') {
return None; return Ok(None);
} }
let command_str = trimmed.strip_prefix('/').unwrap_or(&trimmed); let command_str = trimmed.strip_prefix('/').unwrap_or(&trimmed);
let (command, params) = command_str let (command, params_str) = command_str
.split_once(' ') .split_once(' ')
.map(|(cmd, p)| (cmd, p.trim())) .map(|(cmd, p)| (cmd, p.trim()))
.unwrap_or((command_str, "")); .unwrap_or((command_str, ""));
let params: Vec<&str> = if params.is_empty() { let params: Vec<&str> = if params_str.is_empty() {
vec![] vec![]
} else { } else {
params.split_whitespace().collect() params_str.split_whitespace().collect()
}; };
let result = match command { match command {
"prompts" => self.handle_prompts_command(&params, session_id).await, "prompts" => self.handle_prompts_command(&params, session_id).await,
"prompt" => self.handle_prompt_command(&params, session_id).await, "prompt" => self.handle_prompt_command(&params, session_id).await,
"compact" => self.handle_compact_command(session_id).await, "compact" => self.handle_compact_command(session_id).await,
"clear" => self.handle_clear_command(session_id).await, "clear" => self.handle_clear_command(session_id).await,
_ => { _ => {
self.handle_recipe_command(command, &params, session_id) self.handle_recipe_command(command, params_str, session_id)
.await .await
} }
};
match result {
Ok(msg) => msg,
Err(e) => {
Some(Message::assistant().with_text(format!("Error executing /{}: {}", command, e)))
}
} }
} }
@@ -264,7 +261,7 @@ impl Agent {
async fn handle_recipe_command( async fn handle_recipe_command(
&self, &self,
command: &str, command: &str,
params: &[&str], params_str: &str,
_session_id: &str, _session_id: &str,
) -> Result<Option<Message>> { ) -> Result<Option<Message>> {
let full_command = format!("/{}", command); let full_command = format!("/{}", command);
@@ -284,7 +281,64 @@ impl Agent {
.parent() .parent()
.ok_or_else(|| anyhow!("Recipe path has no parent directory"))?; .ok_or_else(|| anyhow!("Recipe path has no parent directory"))?;
let param_values: Vec<String> = params.iter().map(|s| s.to_string()).collect(); let recipe_dir_str = recipe_dir.display().to_string();
let validation_result =
crate::recipe::validate_recipe::validate_recipe_template_from_content(
&recipe_content,
Some(recipe_dir_str),
)
.map_err(|e| anyhow!("Failed to parse recipe: {}", e))?;
let param_values: Vec<String> = if params_str.is_empty() {
vec![]
} else {
let params_without_default = validation_result
.parameters
.as_ref()
.map(|params| params.iter().filter(|p| p.default.is_none()).count())
.unwrap_or(0);
if params_without_default <= 1 {
vec![params_str.to_string()]
} else {
let param_names: Vec<String> = validation_result
.parameters
.as_ref()
.map(|params| {
params
.iter()
.filter(|p| p.default.is_none())
.map(|p| p.key.clone())
.collect()
})
.unwrap_or_default();
let error_message = format!(
"The /{} recipe requires {} parameters: {}.\n\n\
Slash command recipes only support 1 parameter.\n\n\
**To use this recipe:**\n\
• **CLI:** `goose run --recipe {} {}`\n\
• **Desktop:** Launch from the recipes sidebar to fill in parameters",
command,
params_without_default,
param_names
.iter()
.map(|name| format!("**{}**", name))
.collect::<Vec<_>>()
.join(", "),
command,
param_names
.iter()
.map(|name| format!("--params {}=\"...\"", name))
.collect::<Vec<_>>()
.join(" ")
);
return Err(anyhow!(error_message));
}
};
let param_values_len = param_values.len();
let recipe = match build_recipe_from_template_with_positional_params( let recipe = match build_recipe_from_template_with_positional_params(
recipe_content, recipe_content,
@@ -298,7 +352,7 @@ impl Agent {
"Recipe requires {} parameter(s): {}. Provided: {}", "Recipe requires {} parameter(s): {}. Provided: {}",
parameters.len(), parameters.len(),
parameters.join(", "), parameters.join(", "),
params.len() param_values_len
)))); ))));
} }
Err(e) => return Err(anyhow!("Failed to build recipe: {}", e)), Err(e) => return Err(anyhow!("Failed to build recipe: {}", e)),