feat!: relax tool result structuredContent type (SEP-2106) (#933)
Re-applies #919 (reverted by #932): ToolResultContent.structured_content becomes Option<Value> so non-object structured content is accepted, matching CallToolResult and SEP-2106. BREAKING CHANGE: ToolResultContent.structured_content changes from Option<JsonObject> to Option<Value>.
This commit is contained in:
parent
f4ff56b81d
commit
ba00b15097
6 changed files with 40 additions and 30 deletions
|
|
@ -10,7 +10,7 @@
|
|||
// ToolUseContent/ToolResultContent are SEP-2577-deprecated; internal references are expected.
|
||||
#![expect(deprecated)]
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{Annotations, Meta, resource::ResourceContents};
|
||||
|
||||
|
|
@ -207,7 +207,7 @@ pub struct ToolResultContent {
|
|||
pub tool_use_id: String,
|
||||
pub content: Vec<ContentBlock>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub structured_content: Option<super::JsonObject>,
|
||||
pub structured_content: Option<Value>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub is_error: Option<bool>,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2397,13 +2397,7 @@
|
|||
"null"
|
||||
]
|
||||
},
|
||||
"structuredContent": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"structuredContent": true,
|
||||
"toolUseId": {
|
||||
"type": "string"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2397,13 +2397,7 @@
|
|||
"null"
|
||||
]
|
||||
},
|
||||
"structuredContent": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"structuredContent": true,
|
||||
"toolUseId": {
|
||||
"type": "string"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3562,13 +3562,7 @@
|
|||
"null"
|
||||
]
|
||||
},
|
||||
"structuredContent": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"structuredContent": true,
|
||||
"toolUseId": {
|
||||
"type": "string"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3562,13 +3562,7 @@
|
|||
"null"
|
||||
]
|
||||
},
|
||||
"structuredContent": {
|
||||
"type": [
|
||||
"object",
|
||||
"null"
|
||||
],
|
||||
"additionalProperties": true
|
||||
},
|
||||
"structuredContent": true,
|
||||
"toolUseId": {
|
||||
"type": "string"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ use rmcp::{
|
|||
model::*,
|
||||
service::{RequestContext, Service},
|
||||
};
|
||||
use rstest::rstest;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_basic_sampling_message_creation() -> Result<()> {
|
||||
|
|
@ -370,6 +371,39 @@ fn test_tool_result_content_requires_content() {
|
|||
assert!(err.to_string().contains("missing field `content`"));
|
||||
}
|
||||
|
||||
#[rstest]
|
||||
#[case::array(serde_json::json!([{ "city": "SF", "temp": 72 }, { "city": "NY", "temp": 65 }]))]
|
||||
#[case::string(serde_json::json!("sunny"))]
|
||||
#[case::integer(serde_json::json!(42))]
|
||||
#[case::float(serde_json::json!(3.14))]
|
||||
#[case::boolean(serde_json::json!(true))]
|
||||
fn tool_result_content_round_trips_non_object_structured_content(
|
||||
#[case] structured: serde_json::Value,
|
||||
) -> Result<()> {
|
||||
let mut tool_result = ToolResultContent::new("call_123", vec![ContentBlock::text("x")]);
|
||||
tool_result.structured_content = Some(structured);
|
||||
|
||||
let json = serde_json::to_string(&tool_result)?;
|
||||
let deserialized: ToolResultContent = serde_json::from_str(&json)?;
|
||||
assert_eq!(tool_result, deserialized);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// `null` is a valid JSON value, but `Option<Value>` deserializes JSON `null`
|
||||
// as `None`, so `Some(Value::Null)` collapses to `None` on round-trip.
|
||||
#[test]
|
||||
fn tool_result_content_null_structured_content_round_trips_to_none() -> Result<()> {
|
||||
let mut tool_result = ToolResultContent::new("call_123", vec![ContentBlock::text("x")]);
|
||||
tool_result.structured_content = Some(serde_json::Value::Null);
|
||||
|
||||
let json = serde_json::to_string(&tool_result)?;
|
||||
let deserialized: ToolResultContent = serde_json::from_str(&json)?;
|
||||
assert_eq!(deserialized.structured_content, None);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_sampling_message_with_tool_use() -> Result<()> {
|
||||
let message = SamplingMessage::assistant_tool_use(
|
||||
|
|
|
|||
Loading…
Reference in a new issue