Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 11 additions & 3 deletions crates/goose/src/agents/agent.rs
Original file line number Diff line number Diff line change
Expand Up @@ -793,7 +793,15 @@ impl Agent {
.await;

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(
&session_config.id,
&user_message.clone().with_visibility(true, false),
Expand Down Expand Up @@ -826,7 +834,7 @@ impl Agent {
}
}));
}
Some(resolved_message) => {
Ok(Some(resolved_message)) => {
SessionManager::add_message(
&session_config.id,
&user_message.clone().with_visibility(true, false),
Expand All @@ -838,7 +846,7 @@ impl Agent {
)
.await?;
}
None => {
Ok(None) => {
SessionManager::add_message(&session_config.id, &user_message).await?;
}
}
Expand Down
88 changes: 71 additions & 17 deletions crates/goose/src/agents/execute_commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,45 +41,42 @@ pub fn list_commands() -> &'static [CommandDef] {
}

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();

if COMPACT_TRIGGERS.contains(&trimmed.as_str()) {
trimmed = COMPACT_TRIGGERS[0].to_string();
}

if !trimmed.starts_with('/') {
return None;
return Ok(None);
}

let command_str = trimmed.strip_prefix('/').unwrap_or(&trimmed);
let (command, params) = command_str
let (command, params_str) = command_str
.split_once(' ')
.map(|(cmd, p)| (cmd, p.trim()))
.unwrap_or((command_str, ""));

let params: Vec<&str> = if params.is_empty() {
let params: Vec<&str> = if params_str.is_empty() {
vec![]
} 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,
"prompt" => self.handle_prompt_command(&params, session_id).await,
"compact" => self.handle_compact_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
}
};

match result {
Ok(msg) => msg,
Err(e) => {
Some(Message::assistant().with_text(format!("Error executing /{}: {}", command, e)))
}
}
}

Expand Down Expand Up @@ -264,7 +261,7 @@ impl Agent {
async fn handle_recipe_command(
&self,
command: &str,
params: &[&str],
params_str: &str,
_session_id: &str,
) -> Result<Option<Message>> {
let full_command = format!("/{}", command);
Expand All @@ -284,7 +281,64 @@ impl Agent {
.parent()
.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 {
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When there are no required parameters (params_without_default == 0) but the user provides input, the code creates a vec with params_str. This will cause the parameter to be passed to a recipe that doesn't expect any positional parameters. The condition should be params_without_default == 1 instead of <= 1 to avoid this issue.

Suggested change
if params_without_default <= 1 {
if params_without_default == 1 {

Copilot uses AI. Check for mistakes.
Copy link

Copilot AI Dec 18, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When a recipe has only optional parameters (all with defaults) and the user provides a value, this code will pass that value as the first parameter. This could cause unexpected behavior since the recipe doesn't require any parameters. Consider checking if params_without_default is 0 and params_str is not empty, and either ignore the value or return an informative error.

Suggested change
if params_without_default <= 1 {
if params_without_default == 0 {
let error_message = format!(
"The /{} recipe does not require any parameters, \
but you provided: `{}`.\n\n\
Slash command recipes only support passing at most one \
required parameter.\n\n\
**To customize this recipe's optional parameters,** \
run it directly as a recipe (for example from the recipes sidebar).",
command,
params_str
);
return Err(anyhow!(error_message));
} else if params_without_default == 1 {

Copilot uses AI. Check for mistakes.
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(
recipe_content,
Expand All @@ -298,7 +352,7 @@ impl Agent {
"Recipe requires {} parameter(s): {}. Provided: {}",
parameters.len(),
parameters.join(", "),
params.len()
param_values_len
))));
}
Err(e) => return Err(anyhow!("Failed to build recipe: {}", e)),
Expand Down
Loading