use crate::error::Result;
use crate::types::content::Content;
use crate::types::sampling::{
CreateMessageParams, CreateMessageResult, CreateMessageResultWithTools, SamplingMessageContent,
};
use async_trait::async_trait;
use futures::future::BoxFuture;
use std::sync::Arc;
#[async_trait]
pub trait HostSamplingHandler: Send + Sync {
async fn handle_create_message(
&self,
params: CreateMessageParams,
) -> Result<CreateMessageResult>;
}
#[async_trait]
pub trait HostSamplingHandlerWithTools: Send + Sync {
async fn handle_create_message_with_tools(
&self,
params: CreateMessageParams,
) -> Result<CreateMessageResultWithTools>;
}
#[must_use]
pub fn lift_content_to_sampling(content: Content) -> SamplingMessageContent {
SamplingMessageContent::from_content(content)
}
#[must_use]
pub fn lift_result_to_with_tools(result: CreateMessageResult) -> CreateMessageResultWithTools {
CreateMessageResultWithTools::from_single(result)
}
pub struct LegacyHostSamplingAdapter {
inner: Arc<dyn HostSamplingHandler>,
}
impl std::fmt::Debug for LegacyHostSamplingAdapter {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LegacyHostSamplingAdapter").finish()
}
}
impl LegacyHostSamplingAdapter {
#[must_use]
pub fn new(inner: Arc<dyn HostSamplingHandler>) -> Self {
Self { inner }
}
}
#[async_trait]
impl HostSamplingHandlerWithTools for LegacyHostSamplingAdapter {
async fn handle_create_message_with_tools(
&self,
params: CreateMessageParams,
) -> Result<CreateMessageResultWithTools> {
let legacy = self.inner.handle_create_message(params).await?;
Ok(lift_result_to_with_tools(legacy))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ApprovalDecision {
Allow,
Deny(String),
}
pub type PreflightApproval =
Arc<dyn Fn(CreateMessageParams) -> BoxFuture<'static, ApprovalDecision> + Send + Sync>;
pub type SamplingResultReview = Arc<
dyn Fn(CreateMessageParams, CreateMessageResult) -> BoxFuture<'static, ApprovalDecision>
+ Send
+ Sync,
>;
#[cfg(test)]
mod tests {
use super::*;
use crate::types::content::Role;
struct LegacyText;
#[async_trait]
impl HostSamplingHandler for LegacyText {
async fn handle_create_message(
&self,
_params: CreateMessageParams,
) -> Result<CreateMessageResult> {
Ok(
CreateMessageResult::new(Content::text("hello"), "legacy-model")
.with_stop_reason("endTurn"),
)
}
}
#[tokio::test]
async fn legacy_adapter_lifts_single_content_into_with_tools() {
let adapter = LegacyHostSamplingAdapter::new(Arc::new(LegacyText));
let result = adapter
.handle_create_message_with_tools(CreateMessageParams::new(Vec::new()))
.await
.expect("adapter must succeed");
assert_eq!(result.model, "legacy-model");
assert_eq!(result.stop_reason.as_deref(), Some("endTurn"));
assert_eq!(result.role, Role::Assistant);
assert_eq!(
result.content.len(),
1,
"single content lifts to one element"
);
match &result.content[0] {
SamplingMessageContent::Text { text, .. } => assert_eq!(text, "hello"),
other => panic!("expected lifted Text content, got {other:?}"),
}
}
#[test]
fn lift_content_maps_each_variant() {
assert!(matches!(
lift_content_to_sampling(Content::text("t")),
SamplingMessageContent::Text { .. }
));
assert!(matches!(
lift_content_to_sampling(Content::image("d", "image/png")),
SamplingMessageContent::Image { .. }
));
assert!(matches!(
lift_content_to_sampling(Content::resource("file:///x")),
SamplingMessageContent::Text { .. }
));
}
}