diff --git a/crates/goose/src/conversation/message.rs b/crates/goose/src/conversation/message.rs index 20ea3588..0651ecb5 100644 --- a/crates/goose/src/conversation/message.rs +++ b/crates/goose/src/conversation/message.rs @@ -86,7 +86,7 @@ impl ToolRequest { #[derive(ToSchema)] pub struct ToolResponse { pub id: String, - #[serde(with = "tool_result_serde")] + #[serde(with = "tool_result_serde::call_tool_result")] #[schema(value_type = Object)] pub tool_result: ToolResult, } @@ -1303,4 +1303,87 @@ mod tests { assert!(metadata.user_visible); assert!(metadata.agent_visible); } + + #[test] + fn test_legacy_tool_response_deserialization() { + let legacy_json = r#"{ + "role": "user", + "created": 1640995200, + "content": [{ + "type": "toolResponse", + "id": "tool123", + "toolResult": { + "status": "success", + "value": [ + { + "type": "text", + "text": "Tool output text" + } + ] + } + }], + "metadata": { "agentVisible": true, "userVisible": true } + }"#; + + let message: Message = serde_json::from_str(legacy_json).unwrap(); + assert_eq!(message.content.len(), 1); + + if let MessageContent::ToolResponse(response) = &message.content[0] { + assert_eq!(response.id, "tool123"); + if let Ok(result) = &response.tool_result { + assert_eq!(result.content.len(), 1); + assert_eq!( + result.content[0].as_text().unwrap().text, + "Tool output text" + ); + } else { + panic!("Expected successful tool result"); + } + } else { + panic!("Expected ToolResponse content"); + } + } + + #[test] + fn test_new_tool_response_deserialization() { + let new_json = r#"{ + "role": "user", + "created": 1640995200, + "content": [{ + "type": "toolResponse", + "id": "tool456", + "toolResult": { + "status": "success", + "value": { + "content": [ + { + "type": "text", + "text": "New format output" + } + ], + "isError": false + } + } + }], + "metadata": { "agentVisible": true, "userVisible": true } + }"#; + + let message: Message = serde_json::from_str(new_json).unwrap(); + assert_eq!(message.content.len(), 1); + + if let MessageContent::ToolResponse(response) = &message.content[0] { + assert_eq!(response.id, "tool456"); + if let Ok(result) = &response.tool_result { + assert_eq!(result.content.len(), 1); + assert_eq!( + result.content[0].as_text().unwrap().text, + "New format output" + ); + } else { + panic!("Expected successful tool result"); + } + } else { + panic!("Expected ToolResponse content"); + } + } } diff --git a/crates/goose/src/conversation/tool_result_serde.rs b/crates/goose/src/conversation/tool_result_serde.rs index abbf9d61..ef359e28 100644 --- a/crates/goose/src/conversation/tool_result_serde.rs +++ b/crates/goose/src/conversation/tool_result_serde.rs @@ -25,13 +25,11 @@ where } } -// For deserialization, let's use a simpler approach that works with the format we're serializing to pub fn deserialize<'de, T, D>(deserializer: D) -> Result, D::Error> where T: Deserialize<'de>, D: Deserializer<'de>, { - // Define a helper enum to handle the two possible formats #[derive(Deserialize)] #[serde(untagged)] enum ResultFormat { @@ -68,3 +66,79 @@ where } } } + +pub mod call_tool_result { + use super::*; + use rmcp::model::{CallToolResult, Content}; + + pub fn serialize( + value: &ToolResult, + serializer: S, + ) -> Result + where + S: Serializer, + { + super::serialize(value, serializer) + } + + pub fn deserialize<'de, D>(deserializer: D) -> Result, D::Error> + where + D: Deserializer<'de>, + { + #[derive(Deserialize)] + #[serde(untagged)] + enum ResultFormat { + NewSuccess { + status: String, + value: CallToolResult, + }, + LegacySuccess { + status: String, + value: Vec, + }, + Error { + status: String, + error: String, + }, + } + + let format = ResultFormat::deserialize(deserializer)?; + + match format { + ResultFormat::NewSuccess { status, value } => { + if status == "success" { + Ok(Ok(value)) + } else { + Err(serde::de::Error::custom(format!( + "Expected status 'success', got '{}'", + status + ))) + } + } + ResultFormat::LegacySuccess { status, value } => { + if status == "success" { + Ok(Ok(CallToolResult::success(value))) + } else { + Err(serde::de::Error::custom(format!( + "Expected status 'success', got '{}'", + status + ))) + } + } + ResultFormat::Error { status, error } => { + if status == "error" { + Ok(Err(ErrorData { + code: ErrorCode::INTERNAL_ERROR, + message: Cow::from(error), + data: None, + })) + } else { + Err(serde::de::Error::custom(format!( + "Expected status 'error', got '{}'", + status + ))) + } + } + } + } +}