diff --git a/crates/goose/src/agents/reply_parts.rs b/crates/goose/src/agents/reply_parts.rs index 1d8fdaea06b4..7e33de435e00 100644 --- a/crates/goose/src/agents/reply_parts.rs +++ b/crates/goose/src/agents/reply_parts.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use async_stream::try_stream; use futures::stream::StreamExt; +use serde_json::{json, Value}; use tracing::debug; use super::super::agents::Agent; @@ -19,6 +20,79 @@ use crate::agents::recipe_tools::dynamic_task_tools::should_enabled_subagents; use crate::session::SessionManager; use rmcp::model::Tool; +fn coerce_value(s: &str, schema: &Value) -> Value { + let type_str = schema.get("type"); + + match type_str { + Some(Value::String(t)) => match t.as_str() { + "number" | "integer" => try_coerce_number(s), + "boolean" => try_coerce_boolean(s), + _ => Value::String(s.to_string()), + }, + Some(Value::Array(types)) => { + // Try each type in order + for t in types { + if let Value::String(type_name) = t { + match type_name.as_str() { + "number" | "integer" if s.parse::().is_ok() => { + return try_coerce_number(s) + } + "boolean" if matches!(s.to_lowercase().as_str(), "true" | "false") => { + return try_coerce_boolean(s) + } + _ => continue, + } + } + } + Value::String(s.to_string()) + } + _ => Value::String(s.to_string()), + } +} + +fn try_coerce_number(s: &str) -> Value { + if let Ok(n) = s.parse::() { + if n.fract() == 0.0 && n >= i64::MIN as f64 && n <= i64::MAX as f64 { + json!(n as i64) + } else { + json!(n) + } + } else { + Value::String(s.to_string()) + } +} + +fn try_coerce_boolean(s: &str) -> Value { + match s.to_lowercase().as_str() { + "true" => json!(true), + "false" => json!(false), + _ => Value::String(s.to_string()), + } +} + +fn coerce_tool_arguments( + arguments: Option>, + tool_schema: &Value, +) -> Option> { + let args = arguments?; + + let properties = tool_schema.get("properties").and_then(|p| p.as_object())?; + + let mut coerced = serde_json::Map::new(); + + for (key, value) in args.iter() { + let coerced_value = + if let (Value::String(s), Some(prop_schema)) = (value, properties.get(key)) { + coerce_value(s, prop_schema) + } else { + value.clone() + }; + coerced.insert(key.clone(), coerced_value); + } + + Some(coerced) +} + async fn toolshim_postprocess( response: Message, toolshim_tools: &[Tool], @@ -190,13 +264,25 @@ impl Agent { &self, response: &Message, ) -> (Vec, Vec, Message) { - // First collect all tool requests + let tools = self.list_tools(None).await; + + // First collect all tool requests with coercion applied let tool_requests: Vec = response .content .iter() .filter_map(|content| { if let MessageContent::ToolRequest(req) = content { - Some(req.clone()) + let mut coerced_req = req.clone(); + + if let Ok(ref mut tool_call) = coerced_req.tool_call { + if let Some(tool) = tools.iter().find(|t| t.name == tool_call.name) { + let schema_value = Value::Object(tool.input_schema.as_ref().clone()); + tool_call.arguments = + coerce_tool_arguments(tool_call.arguments.clone(), &schema_value); + } + } + + Some(coerced_req) } else { None }