fix: match shape of the calltoolresult schema (#377)
BREAKING CHANGE: makes the `content` field non-optional
This commit is contained in:
parent
81ff3159f9
commit
a47cbaac4b
5 changed files with 26 additions and 29 deletions
|
|
@ -183,7 +183,7 @@ impl<'de> Deserialize<'de> for ProtocolVersion {
|
|||
pub enum NumberOrString {
|
||||
/// A numeric identifier
|
||||
Number(u32),
|
||||
/// A string identifier
|
||||
/// A string identifier
|
||||
String(Arc<str>),
|
||||
}
|
||||
|
||||
|
|
@ -1257,8 +1257,7 @@ pub type CreateElicitationRequest =
|
|||
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
|
||||
pub struct CallToolResult {
|
||||
/// The content returned by the tool (text, images, etc.)
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub content: Option<Vec<Content>>,
|
||||
pub content: Vec<Content>,
|
||||
/// An optional JSON object that represents the structured result of the tool call
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub structured_content: Option<Value>,
|
||||
|
|
@ -1271,7 +1270,7 @@ impl CallToolResult {
|
|||
/// Create a successful tool result with unstructured content
|
||||
pub fn success(content: Vec<Content>) -> Self {
|
||||
CallToolResult {
|
||||
content: Some(content),
|
||||
content,
|
||||
structured_content: None,
|
||||
is_error: Some(false),
|
||||
}
|
||||
|
|
@ -1279,7 +1278,7 @@ impl CallToolResult {
|
|||
/// Create an error tool result with unstructured content
|
||||
pub fn error(content: Vec<Content>) -> Self {
|
||||
CallToolResult {
|
||||
content: Some(content),
|
||||
content,
|
||||
structured_content: None,
|
||||
is_error: Some(true),
|
||||
}
|
||||
|
|
@ -1300,7 +1299,7 @@ impl CallToolResult {
|
|||
/// ```
|
||||
pub fn structured(value: Value) -> Self {
|
||||
CallToolResult {
|
||||
content: Some(vec![Content::text(value.to_string())]),
|
||||
content: vec![Content::text(value.to_string())],
|
||||
structured_content: Some(value),
|
||||
is_error: Some(false),
|
||||
}
|
||||
|
|
@ -1325,7 +1324,7 @@ impl CallToolResult {
|
|||
/// ```
|
||||
pub fn structured_error(value: Value) -> Self {
|
||||
CallToolResult {
|
||||
content: Some(vec![Content::text(value.to_string())]),
|
||||
content: vec![Content::text(value.to_string())],
|
||||
structured_content: Some(value),
|
||||
is_error: Some(true),
|
||||
}
|
||||
|
|
@ -1341,10 +1340,10 @@ impl CallToolResult {
|
|||
where
|
||||
T: DeserializeOwned,
|
||||
{
|
||||
let raw_text = match (self.structured_content, &self.content) {
|
||||
let raw_text = match (self.structured_content, &self.content.first()) {
|
||||
(Some(value), _) => return serde_json::from_value(value),
|
||||
(None, Some(contents)) => {
|
||||
if let Some(text) = contents.first().and_then(|c| c.as_text()) {
|
||||
if let Some(text) = contents.as_text() {
|
||||
let text = &text.text;
|
||||
Some(text)
|
||||
} else {
|
||||
|
|
@ -1379,13 +1378,13 @@ impl<'de> Deserialize<'de> for CallToolResult {
|
|||
|
||||
let helper = CallToolResultHelper::deserialize(deserializer)?;
|
||||
let result = CallToolResult {
|
||||
content: helper.content,
|
||||
content: helper.content.unwrap_or_default(),
|
||||
structured_content: helper.structured_content,
|
||||
is_error: helper.is_error,
|
||||
};
|
||||
|
||||
// Validate mutual exclusivity
|
||||
if result.content.is_none() && result.structured_content.is_none() {
|
||||
if result.content.is_empty() && result.structured_content.is_none() {
|
||||
return Err(serde::de::Error::custom(
|
||||
"CallToolResult must have either content or structured_content",
|
||||
));
|
||||
|
|
|
|||
|
|
@ -304,10 +304,7 @@
|
|||
"properties": {
|
||||
"content": {
|
||||
"description": "The content returned by the tool (text, images, etc.)",
|
||||
"type": [
|
||||
"array",
|
||||
"null"
|
||||
],
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/definitions/Annotated"
|
||||
}
|
||||
|
|
@ -322,7 +319,10 @@
|
|||
"structuredContent": {
|
||||
"description": "An optional JSON object that represents the structured result of the tool call"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"content"
|
||||
]
|
||||
},
|
||||
"CancelledNotificationMethod": {
|
||||
"type": "string",
|
||||
|
|
|
|||
|
|
@ -122,10 +122,10 @@ async fn test_structured_content_in_call_result() {
|
|||
|
||||
let result = CallToolResult::structured(structured_data.clone());
|
||||
|
||||
assert!(result.content.is_some());
|
||||
assert!(!result.content.is_empty());
|
||||
assert!(result.structured_content.is_some());
|
||||
|
||||
let contents = result.content.unwrap();
|
||||
let contents = result.content;
|
||||
|
||||
assert_eq!(contents.len(), 1);
|
||||
|
||||
|
|
@ -150,10 +150,10 @@ async fn test_structured_error_in_call_result() {
|
|||
|
||||
let result = CallToolResult::structured_error(error_data.clone());
|
||||
|
||||
assert!(result.content.is_some());
|
||||
assert!(!result.content.is_empty());
|
||||
assert!(result.structured_content.is_some());
|
||||
|
||||
let contents = result.content.unwrap();
|
||||
let contents = result.content;
|
||||
|
||||
assert_eq!(contents.len(), 1);
|
||||
|
||||
|
|
@ -217,10 +217,10 @@ async fn test_structured_return_conversion() {
|
|||
|
||||
// Tools which return structured content should also return a serialized version as
|
||||
// Content::text for backwards compatibility.
|
||||
assert!(call_result.content.is_some());
|
||||
assert!(!call_result.content.is_empty());
|
||||
assert!(call_result.structured_content.is_some());
|
||||
|
||||
let contents = call_result.content.unwrap();
|
||||
let contents = call_result.content;
|
||||
|
||||
assert_eq!(contents.len(), 1);
|
||||
|
||||
|
|
@ -278,5 +278,5 @@ async fn test_output_schema_requires_structured_content() {
|
|||
|
||||
// Verify it has structured_content and content
|
||||
assert!(call_result.structured_content.is_some());
|
||||
assert!(call_result.content.is_some());
|
||||
assert!(!call_result.content.is_empty());
|
||||
}
|
||||
|
|
|
|||
|
|
@ -301,8 +301,7 @@ async fn test_optional_i64_field_with_null_input() -> anyhow::Result<()> {
|
|||
|
||||
let result_text = result
|
||||
.content
|
||||
.as_ref()
|
||||
.and_then(|contents| contents.first())
|
||||
.first()
|
||||
.and_then(|content| content.raw.as_text())
|
||||
.map(|text| text.text.as_str())
|
||||
.expect("Expected text content");
|
||||
|
|
@ -330,8 +329,7 @@ async fn test_optional_i64_field_with_null_input() -> anyhow::Result<()> {
|
|||
|
||||
let some_result_text = some_result
|
||||
.content
|
||||
.as_ref()
|
||||
.and_then(|contents| contents.first())
|
||||
.first()
|
||||
.and_then(|content| content.raw.as_text())
|
||||
.map(|text| text.text.as_str())
|
||||
.expect("Expected text content");
|
||||
|
|
|
|||
|
|
@ -85,8 +85,8 @@ impl ChatSession {
|
|||
if result.is_error.is_some_and(|b| b) {
|
||||
self.messages
|
||||
.push(Message::user("tool call failed, mcp call error"));
|
||||
} else if let Some(contents) = &result.content {
|
||||
contents.iter().for_each(|content| {
|
||||
} else {
|
||||
result.content.iter().for_each(|content| {
|
||||
if let Some(content_text) = content.as_text() {
|
||||
let json_result = serde_json::from_str::<serde_json::Value>(
|
||||
&content_text.text,
|
||||
|
|
|
|||
Loading…
Reference in a new issue