Implement SEP-1577: Sampling With Tools (#628)

* feat: implement SEP-1577 sampling with tools support

* feat: add TryFrom<Content> for backward-compatible migration
This commit is contained in:
Dale Seo 2026-02-06 06:51:26 -05:00 • committed by GitHub
commit 8bd3fcb890
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 1850 additions and 141 deletions

View file

@ -194,6 +194,19 @@ pub struct ElicitationCapability {
pub schema_validation: Option<bool>,
}
/// Sampling capability with optional sub-capabilities (SEP-1577).
#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct SamplingCapability {
/// Support for `tools` and `toolChoice` parameters
#[serde(skip_serializing_if = "Option::is_none")]
pub tools: Option<JsonObject>,
/// Support for `includeContext` (soft-deprecated)
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<JsonObject>,
}
///
/// # Builder
/// ```rust
@ -217,8 +230,9 @@ pub struct ClientCapabilities {
pub extensions: Option<ExtensionCapabilities>,
#[serde(skip_serializing_if = "Option::is_none")]
pub roots: Option<RootsCapabilities>,
/// Capability for LLM sampling requests (SEP-1577)
#[serde(skip_serializing_if = "Option::is_none")]
pub sampling: Option<JsonObject>,
pub sampling: Option<SamplingCapability>,
/// Capability to handle elicitation requests from servers for interactive user input
#[serde(skip_serializing_if = "Option::is_none")]
pub elicitation: Option<ElicitationCapability>,
@ -449,7 +463,7 @@ builder! {
experimental: ExperimentalCapabilities,
extensions: ExtensionCapabilities,
roots: RootsCapabilities,
sampling: JsonObject,
sampling: SamplingCapability,
elicitation: ElicitationCapability,
tasks: TasksCapability,
}
@ -466,6 +480,26 @@ impl<const E: bool, const EXT: bool, const S: bool, const EL: bool, const TASKS:
}
}
impl<const E: bool, const EXT: bool, const R: bool, const EL: bool, const TASKS: bool>
ClientCapabilitiesBuilder<ClientCapabilitiesBuilderState<E, EXT, R, true, EL, TASKS>>
{
/// Enable tool calling in sampling requests
pub fn enable_sampling_tools(mut self) -> Self {
if let Some(c) = self.sampling.as_mut() {
c.tools = Some(JsonObject::default());
}
self
}
/// Enable context inclusion in sampling (soft-deprecated)
pub fn enable_sampling_context(mut self) -> Self {
if let Some(c) = self.sampling.as_mut() {
c.context = Some(JsonObject::default());
}
self
}
}
#[cfg(feature = "elicitation")]
impl<const E: bool, const EXT: bool, const R: bool, const S: bool, const TASKS: bool>
ClientCapabilitiesBuilder<ClientCapabilitiesBuilderState<E, EXT, R, S, true, TASKS>>

View file

@ -59,6 +59,137 @@ pub struct RawAudioContent {
pub type AudioContent = Annotated<RawAudioContent>;
/// Tool call request from assistant (SEP-1577).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ToolUseContent {
/// Unique identifier for this tool call
pub id: String,
/// Name of the tool to call
pub name: String,
/// Input arguments for the tool
pub input: super::JsonObject,
/// Optional metadata (preserved for caching)
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<super::Meta>,
}
/// Tool execution result in user message (SEP-1577).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ToolResultContent {
/// Optional metadata
#[serde(rename = "_meta", skip_serializing_if = "Option::is_none")]
pub meta: Option<super::Meta>,
/// ID of the corresponding tool use
pub tool_use_id: String,
/// Content blocks returned by the tool
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub content: Vec<Content>,
/// Optional structured result
#[serde(skip_serializing_if = "Option::is_none")]
pub structured_content: Option<super::JsonObject>,
/// Whether tool execution failed
#[serde(skip_serializing_if = "Option::is_none")]
pub is_error: Option<bool>,
}
impl ToolUseContent {
pub fn new(id: impl Into<String>, name: impl Into<String>, input: super::JsonObject) -> Self {
Self {
id: id.into(),
name: name.into(),
input,
meta: None,
}
}
}
impl ToolResultContent {
pub fn new(tool_use_id: impl Into<String>, content: Vec<Content>) -> Self {
Self {
meta: None,
tool_use_id: tool_use_id.into(),
content,
structured_content: None,
is_error: None,
}
}
pub fn error(tool_use_id: impl Into<String>, content: Vec<Content>) -> Self {
Self {
meta: None,
tool_use_id: tool_use_id.into(),
content,
structured_content: None,
is_error: Some(true),
}
}
}
/// Assistant message content types (SEP-1577).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum AssistantMessageContent {
Text(RawTextContent),
Image(RawImageContent),
Audio(RawAudioContent),
ToolUse(ToolUseContent),
}
/// User message content types (SEP-1577).
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub enum UserMessageContent {
Text(RawTextContent),
Image(RawImageContent),
Audio(RawAudioContent),
ToolResult(ToolResultContent),
}
impl AssistantMessageContent {
/// Create a text content
pub fn text(text: impl Into<String>) -> Self {
Self::Text(RawTextContent {
text: text.into(),
meta: None,
})
}
/// Create a tool use content
pub fn tool_use(
id: impl Into<String>,
name: impl Into<String>,
input: super::JsonObject,
) -> Self {
Self::ToolUse(ToolUseContent::new(id, name, input))
}
}
impl UserMessageContent {
/// Create a text content
pub fn text(text: impl Into<String>) -> Self {
Self::Text(RawTextContent {
text: text.into(),
meta: None,
})
}
/// Create a tool result content
pub fn tool_result(tool_use_id: impl Into<String>, content: Vec<Content>) -> Self {
Self::ToolResult(ToolResultContent::new(tool_use_id, content))
}
/// Create an error tool result content
pub fn tool_result_error(tool_use_id: impl Into<String>, content: Vec<Content>) -> Self {
Self::ToolResult(ToolResultContent::error(tool_use_id, content))
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]