Update to rmcp 1.1.0 (#7619)
Co-authored-by: Alex Hancock <alexhancock@block.xyz>
This commit is contained in:
@@ -295,12 +295,11 @@ impl MessageContent {
|
||||
|
||||
// Preserve ToolResponse even when content is empty - some providers
|
||||
// (like Google) need to handle empty tool responses specially
|
||||
let mut tool_result = result.clone();
|
||||
tool_result.content = filtered_content;
|
||||
Some(MessageContent::ToolResponse(ToolResponse {
|
||||
id: res.id.clone(),
|
||||
tool_result: Ok(CallToolResult {
|
||||
content: filtered_content,
|
||||
..result.clone()
|
||||
}),
|
||||
tool_result: Ok(tool_result),
|
||||
metadata: res.metadata.clone(),
|
||||
}))
|
||||
}
|
||||
@@ -999,12 +998,8 @@ mod tests {
|
||||
.with_text("Hello, I'll help you with that.")
|
||||
.with_tool_request(
|
||||
"tool123",
|
||||
Ok(CallToolRequestParams {
|
||||
meta: None,
|
||||
task: None,
|
||||
name: "test_tool".into(),
|
||||
arguments: Some(object!({"param": "value"})),
|
||||
}),
|
||||
Ok(CallToolRequestParams::new("test_tool")
|
||||
.with_arguments(object!({"param": "value"}))),
|
||||
);
|
||||
|
||||
let json_str = serde_json::to_string_pretty(&message).unwrap();
|
||||
@@ -1120,10 +1115,7 @@ mod tests {
|
||||
text: "Hello, world!".to_string(),
|
||||
};
|
||||
|
||||
let prompt_message = PromptMessage {
|
||||
role: PromptMessageRole::User,
|
||||
content: prompt_content,
|
||||
};
|
||||
let prompt_message = PromptMessage::new(PromptMessageRole::User, prompt_content);
|
||||
|
||||
let message = Message::from(prompt_message);
|
||||
|
||||
@@ -1145,10 +1137,7 @@ mod tests {
|
||||
.no_annotation(),
|
||||
};
|
||||
|
||||
let prompt_message = PromptMessage {
|
||||
role: PromptMessageRole::User,
|
||||
content: prompt_content,
|
||||
};
|
||||
let prompt_message = PromptMessage::new(PromptMessageRole::User, prompt_content);
|
||||
|
||||
let message = Message::from(prompt_message);
|
||||
|
||||
@@ -1177,10 +1166,7 @@ mod tests {
|
||||
.no_annotation(),
|
||||
};
|
||||
|
||||
let prompt_message = PromptMessage {
|
||||
role: PromptMessageRole::User,
|
||||
content: prompt_content,
|
||||
};
|
||||
let prompt_message = PromptMessage::new(PromptMessageRole::User, prompt_content);
|
||||
|
||||
let message = Message::from(prompt_message);
|
||||
|
||||
@@ -1194,12 +1180,12 @@ mod tests {
|
||||
#[test]
|
||||
fn test_from_prompt_message() {
|
||||
// Test user message conversion
|
||||
let prompt_message = PromptMessage {
|
||||
role: PromptMessageRole::User,
|
||||
content: PromptMessageContent::Text {
|
||||
let prompt_message = PromptMessage::new(
|
||||
PromptMessageRole::User,
|
||||
PromptMessageContent::Text {
|
||||
text: "Hello, world!".to_string(),
|
||||
},
|
||||
};
|
||||
);
|
||||
|
||||
let message = Message::from(prompt_message);
|
||||
assert_eq!(message.role, Role::User);
|
||||
@@ -1207,12 +1193,12 @@ mod tests {
|
||||
assert_eq!(message.as_concat_text(), "Hello, world!");
|
||||
|
||||
// Test assistant message conversion
|
||||
let prompt_message = PromptMessage {
|
||||
role: PromptMessageRole::Assistant,
|
||||
content: PromptMessageContent::Text {
|
||||
let prompt_message = PromptMessage::new(
|
||||
PromptMessageRole::Assistant,
|
||||
PromptMessageContent::Text {
|
||||
text: "I can help with that.".to_string(),
|
||||
},
|
||||
};
|
||||
);
|
||||
|
||||
let message = Message::from(prompt_message);
|
||||
assert_eq!(message.role, Role::Assistant);
|
||||
@@ -1228,12 +1214,7 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_message_with_tool_request() {
|
||||
let tool_call = Ok(CallToolRequestParams {
|
||||
meta: None,
|
||||
task: None,
|
||||
name: "test_tool".into(),
|
||||
arguments: Some(object!({})),
|
||||
});
|
||||
let tool_call = Ok(CallToolRequestParams::new("test_tool").with_arguments(object!({})));
|
||||
|
||||
let message = Message::assistant().with_tool_request("req1", tool_call);
|
||||
assert!(message.is_tool_call());
|
||||
|
||||
@@ -6,7 +6,7 @@ use thiserror::Error;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub mod message;
|
||||
pub mod tool_result_serde;
|
||||
mod tool_result_serde;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, ToSchema, PartialEq)]
|
||||
pub struct Conversation(Vec<Message>);
|
||||
@@ -550,22 +550,11 @@ mod tests {
|
||||
.with_text("I'll help you search.")
|
||||
.with_tool_request(
|
||||
"search_1",
|
||||
Ok(CallToolRequestParams {
|
||||
meta: None,
|
||||
task: None,
|
||||
name: "web_search".into(),
|
||||
arguments: Some(object!({"query": "rust programming"})),
|
||||
}),
|
||||
Ok(CallToolRequestParams::new("web_search")
|
||||
.with_arguments(object!({"query": "rust programming"}))),
|
||||
),
|
||||
Message::user().with_tool_response(
|
||||
"search_1",
|
||||
Ok(rmcp::model::CallToolResult {
|
||||
content: vec![],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
),
|
||||
Message::user()
|
||||
.with_tool_response("search_1", Ok(rmcp::model::CallToolResult::success(vec![]))),
|
||||
Message::assistant().with_text("Based on the search results, here's what I found..."),
|
||||
];
|
||||
|
||||
@@ -602,25 +591,12 @@ mod tests {
|
||||
Message::user().with_text("Another user message"),
|
||||
Message::assistant()
|
||||
.with_text("Response")
|
||||
.with_tool_response(
|
||||
"orphan_1",
|
||||
Ok(rmcp::model::CallToolResult {
|
||||
content: vec![],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
), // Wrong role
|
||||
.with_tool_response("orphan_1", Ok(rmcp::model::CallToolResult::success(vec![]))), // Wrong role
|
||||
Message::assistant().with_thinking("Let me think", "sig"),
|
||||
Message::user()
|
||||
.with_tool_request(
|
||||
"bad_req",
|
||||
Ok(CallToolRequestParams {
|
||||
meta: None,
|
||||
task: None,
|
||||
name: "search".into(),
|
||||
arguments: Some(object!({})),
|
||||
}),
|
||||
Ok(CallToolRequestParams::new("search").with_arguments(object!({}))),
|
||||
)
|
||||
.with_text("User with bad tool request"),
|
||||
];
|
||||
@@ -656,31 +632,14 @@ mod tests {
|
||||
.with_text("I'll search for you")
|
||||
.with_tool_request(
|
||||
"search_1",
|
||||
Ok(CallToolRequestParams {
|
||||
meta: None,
|
||||
task: None,
|
||||
name: "search".into(),
|
||||
arguments: Some(object!({})),
|
||||
}),
|
||||
Ok(CallToolRequestParams::new("search").with_arguments(object!({}))),
|
||||
),
|
||||
Message::user(),
|
||||
Message::user().with_tool_response(
|
||||
"wrong_id",
|
||||
Ok(rmcp::model::CallToolResult {
|
||||
content: vec![],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
),
|
||||
Message::user()
|
||||
.with_tool_response("wrong_id", Ok(rmcp::model::CallToolResult::success(vec![]))),
|
||||
Message::assistant().with_tool_request(
|
||||
"search_2",
|
||||
Ok(CallToolRequestParams {
|
||||
meta: None,
|
||||
task: None,
|
||||
name: "search".into(),
|
||||
arguments: Some(object!({})),
|
||||
}),
|
||||
Ok(CallToolRequestParams::new("search").with_arguments(object!({}))),
|
||||
),
|
||||
];
|
||||
|
||||
@@ -712,19 +671,14 @@ mod tests {
|
||||
|
||||
Message::assistant()
|
||||
.with_text("I'll help you run `ls` in the current directory and then perform a word count on the smallest file. Let me start by listing the directory contents.")
|
||||
.with_tool_request("toolu_bdrk_018adWbP4X26CfoJU5hkhu3i", Ok(CallToolRequestParams { meta: None, task: None, name: "developer__shell".into(), arguments: Some(object!({"command": "ls -la"})) })),
|
||||
.with_tool_request("toolu_bdrk_018adWbP4X26CfoJU5hkhu3i", Ok(CallToolRequestParams::new("developer__shell").with_arguments(object!({"command": "ls -la"})))),
|
||||
|
||||
Message::assistant()
|
||||
.with_text("Now I'll identify the smallest file by size. Looking at the output, I can see that both `slack.yaml` and `subrecipes.yaml` have a size of 0 bytes, making them the smallest files. I'll run a word count on one of them:")
|
||||
.with_tool_request("toolu_bdrk_01KgDYHs4fAodi22NqxRzmwx", Ok(CallToolRequestParams { meta: None, task: None, name: "developer__shell".into(), arguments: Some(object!({"command": "wc slack.yaml"})) })),
|
||||
.with_tool_request("toolu_bdrk_01KgDYHs4fAodi22NqxRzmwx", Ok(CallToolRequestParams::new("developer__shell").with_arguments(object!({"command": "wc slack.yaml"})))),
|
||||
|
||||
Message::user()
|
||||
.with_tool_response("toolu_bdrk_01KgDYHs4fAodi22NqxRzmwx", Ok(rmcp::model::CallToolResult {
|
||||
content: vec![],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
})),
|
||||
.with_tool_response("toolu_bdrk_01KgDYHs4fAodi22NqxRzmwx", Ok(rmcp::model::CallToolResult::success(vec![]))),
|
||||
|
||||
Message::assistant()
|
||||
.with_text("I ran `ls -la` in the current directory and found several files. Looking at the file sizes, I can see that both `slack.yaml` and `subrecipes.yaml` are 0 bytes (the smallest files). I ran a word count on `slack.yaml` which shows: **0 lines**, **0 words**, **0 characters**"),
|
||||
@@ -750,22 +704,10 @@ mod tests {
|
||||
.with_text("I'll search for you")
|
||||
.with_tool_request(
|
||||
"search_1",
|
||||
Ok(CallToolRequestParams {
|
||||
meta: None,
|
||||
task: None,
|
||||
name: "search".into(),
|
||||
arguments: Some(object!({})),
|
||||
}),
|
||||
Ok(CallToolRequestParams::new("search").with_arguments(object!({}))),
|
||||
),
|
||||
Message::user().with_tool_response(
|
||||
"search_1",
|
||||
Ok(rmcp::model::CallToolResult {
|
||||
content: vec![],
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
meta: None,
|
||||
}),
|
||||
),
|
||||
Message::user()
|
||||
.with_tool_response("search_1", Ok(rmcp::model::CallToolResult::success(vec![]))),
|
||||
Message::user().with_text("Thanks!"),
|
||||
];
|
||||
|
||||
|
||||
@@ -42,11 +42,12 @@ impl ToolCallWithValueArguments {
|
||||
Some(map)
|
||||
}
|
||||
};
|
||||
CallToolRequestParams {
|
||||
meta: None,
|
||||
task: None,
|
||||
name: Cow::Owned(self.name),
|
||||
arguments,
|
||||
{
|
||||
let mut params = CallToolRequestParams::new(self.name);
|
||||
if let Some(args) = arguments {
|
||||
params = params.with_arguments(args);
|
||||
}
|
||||
params
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -147,17 +148,7 @@ pub mod call_tool_result {
|
||||
},
|
||||
}
|
||||
|
||||
let original_value = serde_json::Value::deserialize(deserializer)?;
|
||||
|
||||
let format = ResultFormat::deserialize(&original_value).map_err(|e| {
|
||||
tracing::debug!(
|
||||
"Failed to deserialize call_tool_result: {}. Original data: {}",
|
||||
e,
|
||||
serde_json::to_string(&original_value)
|
||||
.unwrap_or_else(|_| "<invalid json>".to_string())
|
||||
);
|
||||
serde::de::Error::custom(e)
|
||||
})?;
|
||||
let format = ResultFormat::deserialize(deserializer)?;
|
||||
|
||||
match format {
|
||||
ResultFormat::SuccessWithCallToolResult { status, value } => {
|
||||
@@ -196,87 +187,4 @@ pub mod call_tool_result {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn validate(result: ToolResult<CallToolResult>) -> ToolResult<CallToolResult> {
|
||||
match &result {
|
||||
Ok(call_tool_result) => match serde_json::to_string(call_tool_result) {
|
||||
Ok(json_str) => match serde_json::from_str::<CallToolResult>(&json_str) {
|
||||
Ok(_) => result,
|
||||
Err(e) => {
|
||||
tracing::error!("CallToolResult failed validation by deserialization: {}. Original data: {}", e, json_str);
|
||||
Err(ErrorData {
|
||||
code: ErrorCode::INTERNAL_ERROR,
|
||||
message: Cow::from(format!("Tool result validation failed: {}", e)),
|
||||
data: None,
|
||||
})
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
tracing::error!("CallToolResult failed serialization: {}", e);
|
||||
Err(ErrorData {
|
||||
code: ErrorCode::INTERNAL_ERROR,
|
||||
message: Cow::from(format!("Tool result serialization failed: {}", e)),
|
||||
data: None,
|
||||
})
|
||||
}
|
||||
},
|
||||
Err(_) => result,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use rmcp::model::{CallToolResult, Content, ErrorCode, ErrorData};
|
||||
use std::borrow::Cow;
|
||||
#[test]
|
||||
fn test_validate_accepts_valid_call_tool_result() {
|
||||
let valid_result = CallToolResult {
|
||||
content: vec![Content::text("test")],
|
||||
is_error: Some(false),
|
||||
structured_content: None,
|
||||
meta: None,
|
||||
};
|
||||
|
||||
let tool_result: ToolResult<CallToolResult> = Ok(valid_result);
|
||||
let validated = call_tool_result::validate(tool_result);
|
||||
|
||||
assert!(
|
||||
validated.is_ok(),
|
||||
"Expected validation to pass for valid CallToolResult"
|
||||
);
|
||||
}
|
||||
#[test]
|
||||
fn test_validate_returns_error_for_invalid_calltoolresult() {
|
||||
let valid_result = CallToolResult {
|
||||
content: vec![],
|
||||
is_error: Some(false),
|
||||
structured_content: None,
|
||||
meta: None,
|
||||
};
|
||||
|
||||
let tool_result: ToolResult<CallToolResult> = Ok(valid_result);
|
||||
let validated = call_tool_result::validate(tool_result);
|
||||
|
||||
assert!(validated.is_err());
|
||||
assert!(validated
|
||||
.unwrap_err()
|
||||
.message
|
||||
.contains("Tool result validation failed"))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_passes_through_errors() {
|
||||
let error_result: ToolResult<CallToolResult> = Err(ErrorData {
|
||||
code: ErrorCode::INTERNAL_ERROR,
|
||||
message: Cow::from("test error"),
|
||||
data: None,
|
||||
});
|
||||
|
||||
let validated = call_tool_result::validate(error_result.clone());
|
||||
|
||||
assert!(validated.is_err());
|
||||
assert_eq!(validated.unwrap_err().message, "test error");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user