Skip to content

Commit 093b076

Browse files
authored
added validation and debug for invalid call tool result (#6368)
1 parent c68308d commit 093b076

3 files changed

Lines changed: 98 additions & 2 deletions

File tree

crates/goose/src/agents/agent.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ use crate::conversation::message::{
3333
ActionRequiredData, Message, MessageContent, ProviderMetadata, SystemNotificationType,
3434
ToolRequest,
3535
};
36+
use crate::conversation::tool_result_serde::call_tool_result;
3637
use crate::conversation::{debug_conversation_fix, fix_conversation, Conversation};
3738
use crate::mcp_utils::ToolResult;
3839
use crate::permission::permission_inspector::PermissionInspector;
@@ -1123,6 +1124,8 @@ impl Agent {
11231124

11241125
match item {
11251126
ToolStreamItem::Result(output) => {
1127+
let output = call_tool_result::validate(output);
1128+
11261129
if enable_extension_request_ids.contains(&request_id)
11271130
&& output.is_err()
11281131
{

crates/goose/src/conversation/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use thiserror::Error;
66
use utoipa::ToSchema;
77

88
pub mod message;
9-
mod tool_result_serde;
9+
pub mod tool_result_serde;
1010

1111
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, PartialEq)]
1212
pub struct Conversation(Vec<Message>);

crates/goose/src/conversation/tool_result_serde.rs

Lines changed: 94 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,17 @@ pub mod call_tool_result {
145145
},
146146
}
147147

148-
let format = ResultFormat::deserialize(deserializer)?;
148+
let original_value = serde_json::Value::deserialize(deserializer)?;
149+
150+
let format = ResultFormat::deserialize(&original_value).map_err(|e| {
151+
tracing::debug!(
152+
"Failed to deserialize call_tool_result: {}. Original data: {}",
153+
e,
154+
serde_json::to_string(&original_value)
155+
.unwrap_or_else(|_| "<invalid json>".to_string())
156+
);
157+
serde::de::Error::custom(e)
158+
})?;
149159

150160
match format {
151161
ResultFormat::SuccessWithCallToolResult { status, value } => {
@@ -184,4 +194,87 @@ pub mod call_tool_result {
184194
}
185195
}
186196
}
197+
198+
pub fn validate(result: ToolResult<CallToolResult>) -> ToolResult<CallToolResult> {
199+
match &result {
200+
Ok(call_tool_result) => match serde_json::to_string(call_tool_result) {
201+
Ok(json_str) => match serde_json::from_str::<CallToolResult>(&json_str) {
202+
Ok(_) => result,
203+
Err(e) => {
204+
tracing::error!("CallToolResult failed validation by deserialization: {}. Original data: {}", e, json_str);
205+
Err(ErrorData {
206+
code: ErrorCode::INTERNAL_ERROR,
207+
message: Cow::from(format!("Tool result validation failed: {}", e)),
208+
data: None,
209+
})
210+
}
211+
},
212+
Err(e) => {
213+
tracing::error!("CallToolResult failed serialization: {}", e);
214+
Err(ErrorData {
215+
code: ErrorCode::INTERNAL_ERROR,
216+
message: Cow::from(format!("Tool result serialization failed: {}", e)),
217+
data: None,
218+
})
219+
}
220+
},
221+
Err(_) => result,
222+
}
223+
}
224+
}
225+
226+
#[cfg(test)]
227+
mod tests {
228+
use super::*;
229+
use rmcp::model::{CallToolResult, Content, ErrorCode, ErrorData};
230+
use std::borrow::Cow;
231+
#[test]
232+
fn test_validate_accepts_valid_call_tool_result() {
233+
let valid_result = CallToolResult {
234+
content: vec![Content::text("test")],
235+
is_error: Some(false),
236+
structured_content: None,
237+
meta: None,
238+
};
239+
240+
let tool_result: ToolResult<CallToolResult> = Ok(valid_result);
241+
let validated = call_tool_result::validate(tool_result);
242+
243+
assert!(
244+
validated.is_ok(),
245+
"Expected validation to pass for valid CallToolResult"
246+
);
247+
}
248+
#[test]
249+
fn test_validate_returns_error_for_invalid_calltoolresult() {
250+
let valid_result = CallToolResult {
251+
content: vec![],
252+
is_error: Some(false),
253+
structured_content: None,
254+
meta: None,
255+
};
256+
257+
let tool_result: ToolResult<CallToolResult> = Ok(valid_result);
258+
let validated = call_tool_result::validate(tool_result);
259+
260+
assert!(validated.is_err());
261+
assert!(validated
262+
.unwrap_err()
263+
.message
264+
.contains("Tool result validation failed"))
265+
}
266+
267+
#[test]
268+
fn test_validate_passes_through_errors() {
269+
let error_result: ToolResult<CallToolResult> = Err(ErrorData {
270+
code: ErrorCode::INTERNAL_ERROR,
271+
message: Cow::from("test error"),
272+
data: None,
273+
});
274+
275+
let validated = call_tool_result::validate(error_result.clone());
276+
277+
assert!(validated.is_err());
278+
assert_eq!(validated.unwrap_err().message, "test error");
279+
}
187280
}

0 commit comments

Comments
 (0)