spool-memory 0.2.3

Local-first developer memory system — persistent, structured knowledge for AI coding tools
Documentation
//! Shared sampling client trait for MCP `sampling/createMessage` reverse-calls.
//!
//! Used by both the distill pipeline (transcript → candidates) and the
//! knowledge compile pipeline (clusters → knowledge pages).

use std::future::Future;
use std::pin::Pin;

/// Boxed future returned by [`SamplingClient::create_message`].
pub type SamplingFuture<'a> =
    Pin<Box<dyn Future<Output = Result<String, SamplingError>> + Send + 'a>>;

/// MCP `sampling/createMessage` reverse-call client.
///
/// Implementations:
/// - [`NoopSamplingClient`] — always unavailable (CLI/hook contexts).
/// - `McpSamplingClient` (in `src/mcp.rs`) — real MCP reverse-call.
/// - Test fakes for unit testing.
pub trait SamplingClient: Sync {
    fn is_available(&self) -> bool;
    fn create_message<'a>(&'a self, prompt: &'a str) -> SamplingFuture<'a>;
}

/// Reasons the sampling reverse-call may fail or be skipped.
#[derive(Debug, Clone)]
pub enum SamplingError {
    Unavailable,
    Rejected(String),
    Timeout,
    Other(String),
}

impl std::fmt::Display for SamplingError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            SamplingError::Unavailable => write!(f, "sampling unavailable"),
            SamplingError::Rejected(why) => write!(f, "sampling rejected: {why}"),
            SamplingError::Timeout => write!(f, "sampling timeout"),
            SamplingError::Other(msg) => write!(f, "sampling failure: {msg}"),
        }
    }
}

/// Default sampling client that always reports unavailable.
#[derive(Debug, Default, Clone, Copy)]
pub struct NoopSamplingClient;

impl SamplingClient for NoopSamplingClient {
    fn is_available(&self) -> bool {
        false
    }
    fn create_message<'a>(&'a self, _prompt: &'a str) -> SamplingFuture<'a> {
        Box::pin(async { Err(SamplingError::Unavailable) })
    }
}