pub mod elicitation;
pub mod roots;
pub mod sampling;
use crate::types::mrtr::{InputRequest, InputRequestKind, InputRequests};
use crate::types::protocol::{ClientRequest, Request, ServerRequest};
use std::sync::Arc;
pub use elicitation::HostElicitationHandler;
pub use roots::RootsProvider;
pub use sampling::{
lift_content_to_sampling, lift_result_to_with_tools, ApprovalDecision, HostSamplingHandler,
HostSamplingHandlerWithTools, LegacyHostSamplingAdapter, PreflightApproval,
SamplingResultReview,
};
#[derive(Clone, Default)]
pub struct ClientHostRegistry {
pub(crate) sampling: Option<Arc<dyn HostSamplingHandler>>,
pub(crate) sampling_with_tools: Option<Arc<dyn HostSamplingHandlerWithTools>>,
pub(crate) elicitation: Option<Arc<dyn HostElicitationHandler>>,
pub(crate) roots: Option<RootsProvider>,
pub(crate) approval: Option<PreflightApproval>,
pub(crate) result_review: Option<SamplingResultReview>,
}
impl std::fmt::Debug for ClientHostRegistry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ClientHostRegistry")
.field("has_sampling", &self.sampling.is_some())
.field(
"has_sampling_with_tools",
&self.sampling_with_tools.is_some(),
)
.field("has_elicitation", &self.elicitation.is_some())
.field("has_roots", &self.roots.is_some())
.field("has_approval", &self.approval.is_some())
.field("has_result_review", &self.result_review.is_some())
.finish()
}
}
#[doc(hidden)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HostRequestKind {
Sampling,
Elicitation,
Roots,
Ping,
Unhandled,
}
#[doc(hidden)]
pub fn classify_host_request(request: &Request) -> HostRequestKind {
match request {
Request::Client(client) => match client.as_ref() {
ClientRequest::CreateMessage(_) => HostRequestKind::Sampling,
ClientRequest::Ping => HostRequestKind::Ping,
_ => HostRequestKind::Unhandled,
},
Request::Server(server) => match server.as_ref() {
ServerRequest::CreateMessage(_) => HostRequestKind::Sampling,
ServerRequest::ElicitationCreate(_) => HostRequestKind::Elicitation,
ServerRequest::ListRoots => HostRequestKind::Roots,
},
}
}
#[doc(hidden)]
#[must_use]
pub fn classify_input_request(request: &InputRequest) -> HostRequestKind {
host_kind_of(request.kind())
}
const fn host_kind_of(kind: InputRequestKind) -> HostRequestKind {
match kind {
InputRequestKind::Elicitation => HostRequestKind::Elicitation,
InputRequestKind::Sampling => HostRequestKind::Sampling,
InputRequestKind::Roots => HostRequestKind::Roots,
}
}
impl ClientHostRegistry {
fn can_fulfil(&self, kind: HostRequestKind) -> bool {
match kind {
HostRequestKind::Sampling => {
self.sampling.is_some() || self.sampling_with_tools.is_some()
},
HostRequestKind::Elicitation => self.elicitation.is_some(),
HostRequestKind::Roots => self.roots.is_some(),
HostRequestKind::Ping | HostRequestKind::Unhandled => false,
}
}
pub(crate) fn preflight_input_requests(
&self,
requests: &InputRequests,
) -> std::result::Result<(), HostRequestKind> {
for request in requests.values() {
let kind = classify_input_request(request);
if !self.can_fulfil(kind) {
return Err(kind);
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::sampling::CreateMessageParams;
#[test]
fn registry_default_is_all_none() {
let reg = ClientHostRegistry::default();
assert!(reg.sampling.is_none());
assert!(reg.elicitation.is_none());
assert!(reg.roots.is_none());
assert!(reg.approval.is_none());
assert!(reg.result_review.is_none());
}
#[test]
fn registry_debug_reports_has_flags() {
let reg = ClientHostRegistry::default();
let dbg = format!("{reg:?}");
assert!(dbg.contains("has_sampling: false"));
assert!(dbg.contains("has_elicitation: false"));
assert!(dbg.contains("has_roots: false"));
}
#[test]
fn classify_sampling_server_variant() {
let req = Request::Server(Box::new(ServerRequest::CreateMessage(Box::new(
CreateMessageParams::new(Vec::new()),
))));
assert_eq!(classify_host_request(&req), HostRequestKind::Sampling);
}
#[test]
fn classify_sampling_client_alias_variant() {
let req = Request::Client(Box::new(ClientRequest::CreateMessage(Box::new(
CreateMessageParams::new(Vec::new()),
))));
assert_eq!(classify_host_request(&req), HostRequestKind::Sampling);
}
#[test]
fn classify_roots_and_elicitation() {
let roots = Request::Server(Box::new(ServerRequest::ListRoots));
assert_eq!(classify_host_request(&roots), HostRequestKind::Roots);
let elicit = Request::Server(Box::new(ServerRequest::ElicitationCreate(Box::new(
crate::types::elicitation::ElicitRequestParams::Form {
message: "please".to_string(),
requested_schema: serde_json::json!({}),
},
))));
assert_eq!(classify_host_request(&elicit), HostRequestKind::Elicitation);
}
#[test]
fn classify_unhandled_client_request() {
let req = Request::Client(Box::new(ClientRequest::ListTools(Default::default())));
assert_eq!(classify_host_request(&req), HostRequestKind::Unhandled);
}
#[test]
fn classify_inbound_ping_is_ping_not_unhandled() {
let req = Request::Client(Box::new(ClientRequest::Ping));
assert_eq!(classify_host_request(&req), HostRequestKind::Ping);
}
mod input_requests {
use super::*;
use crate::types::elicitation::ElicitRequestParams;
fn elicitation_entry() -> InputRequest {
InputRequest::Elicitation(Box::new(ElicitRequestParams::Form {
message: "who?".to_string(),
requested_schema: serde_json::json!({}),
}))
}
fn sampling_entry() -> InputRequest {
InputRequest::Sampling(Box::new(CreateMessageParams::new(Vec::new())))
}
fn registry_with_elicitation() -> ClientHostRegistry {
struct Handler;
#[async_trait::async_trait]
impl HostElicitationHandler for Handler {
async fn handle_elicitation(
&self,
_params: ElicitRequestParams,
) -> crate::Result<crate::types::elicitation::ElicitResult> {
unreachable!("preflight must not invoke anything")
}
}
ClientHostRegistry {
elicitation: Some(Arc::new(Handler)),
..ClientHostRegistry::default()
}
}
#[test]
fn classify_maps_the_three_kinds() {
assert_eq!(
classify_input_request(&elicitation_entry()),
HostRequestKind::Elicitation
);
assert_eq!(
classify_input_request(&sampling_entry()),
HostRequestKind::Sampling
);
assert_eq!(
classify_input_request(&InputRequest::ListRoots),
HostRequestKind::Roots
);
}
#[test]
fn preflight_passes_when_every_kind_has_a_handler() {
let registry = registry_with_elicitation();
let mut requests = InputRequests::new();
requests.insert("a".to_string(), elicitation_entry());
assert!(registry.preflight_input_requests(&requests).is_ok());
}
#[test]
fn preflight_names_the_first_unfulfillable_kind() {
let registry = registry_with_elicitation();
let mut requests = InputRequests::new();
requests.insert("a".to_string(), elicitation_entry());
requests.insert("b".to_string(), sampling_entry());
assert_eq!(
registry.preflight_input_requests(&requests),
Err(HostRequestKind::Sampling)
);
assert!(registry
.preflight_input_requests(&InputRequests::new())
.is_ok());
}
#[test]
fn preflight_accepts_either_sampling_handler_shape() {
struct WithTools;
#[async_trait::async_trait]
impl HostSamplingHandlerWithTools for WithTools {
async fn handle_create_message_with_tools(
&self,
_params: CreateMessageParams,
) -> crate::Result<crate::types::sampling::CreateMessageResultWithTools>
{
unreachable!("preflight must not invoke anything")
}
}
let registry = ClientHostRegistry {
sampling_with_tools: Some(Arc::new(WithTools)),
..ClientHostRegistry::default()
};
let mut requests = InputRequests::new();
requests.insert("a".to_string(), sampling_entry());
assert!(registry.preflight_input_requests(&requests).is_ok());
}
}
}