Skip to main content

spool/
sampling.rs

1//! Shared sampling client trait for MCP `sampling/createMessage` reverse-calls.
2//!
3//! Used by both the distill pipeline (transcript → candidates) and the
4//! knowledge compile pipeline (clusters → knowledge pages).
5
6use std::future::Future;
7use std::pin::Pin;
8
9/// Boxed future returned by [`SamplingClient::create_message`].
10pub type SamplingFuture<'a> =
11    Pin<Box<dyn Future<Output = Result<String, SamplingError>> + Send + 'a>>;
12
13/// MCP `sampling/createMessage` reverse-call client.
14///
15/// Implementations:
16/// - [`NoopSamplingClient`] — always unavailable (CLI/hook contexts).
17/// - `McpSamplingClient` (in `src/mcp.rs`) — real MCP reverse-call.
18/// - Test fakes for unit testing.
19pub trait SamplingClient: Sync {
20    fn is_available(&self) -> bool;
21    fn create_message<'a>(&'a self, prompt: &'a str) -> SamplingFuture<'a>;
22}
23
24/// Reasons the sampling reverse-call may fail or be skipped.
25#[derive(Debug, Clone)]
26pub enum SamplingError {
27    Unavailable,
28    Rejected(String),
29    Timeout,
30    Other(String),
31}
32
33impl std::fmt::Display for SamplingError {
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        match self {
36            SamplingError::Unavailable => write!(f, "sampling unavailable"),
37            SamplingError::Rejected(why) => write!(f, "sampling rejected: {why}"),
38            SamplingError::Timeout => write!(f, "sampling timeout"),
39            SamplingError::Other(msg) => write!(f, "sampling failure: {msg}"),
40        }
41    }
42}
43
44/// Default sampling client that always reports unavailable.
45#[derive(Debug, Default, Clone, Copy)]
46pub struct NoopSamplingClient;
47
48impl SamplingClient for NoopSamplingClient {
49    fn is_available(&self) -> bool {
50        false
51    }
52    fn create_message<'a>(&'a self, _prompt: &'a str) -> SamplingFuture<'a> {
53        Box::pin(async { Err(SamplingError::Unavailable) })
54    }
55}