klieo-core 0.41.3

Core traits + runtime for the klieo agent framework.
Documentation
//! Server-initiated outbound JSON-RPC primitive — transport-agnostic.
//!
//! Implementations live in transport crates (e.g.
//! `klieo-mcp-server::outbound::OutboundRequests`). Tools that need
//! to call back out to the peer obtain an `Arc<dyn ServerOutbound>`
//! via `ToolCtx::server_outbound` and issue typed requests via
//! extension traits owned by the same transport crate (e.g.
//! `McpOutboundExt::sample()` in `klieo-mcp-server`).

use async_trait::async_trait;
use std::time::Duration;

/// Transport-agnostic surface for issuing a JSON-RPC request from the
/// server back to its peer.
///
/// Implementations correlate the outbound `id` with the matching
/// inbound response, drive the underlying transport write, and yield
/// the raw `result` payload (or a typed [`ServerOutboundError`]).
#[async_trait]
pub trait ServerOutbound: Send + Sync {
    /// Issue an outbound JSON-RPC request to the peer and await the
    /// matched response. Returns the raw `result` payload on success.
    async fn outbound_request(
        &self,
        method: &str,
        params: serde_json::Value,
        timeout: Duration,
    ) -> Result<serde_json::Value, ServerOutboundError>;
}

/// Failure modes for a [`ServerOutbound::outbound_request`] call.
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum ServerOutboundError {
    /// No response arrived before the supplied timeout elapsed.
    #[error("outbound request timed out")]
    Timeout,
    /// The peer answered with a JSON-RPC error envelope.
    #[error("peer returned error: code={code} message={message}")]
    PeerError {
        /// JSON-RPC error code reported by the peer.
        code: i64,
        /// JSON-RPC error message reported by the peer.
        message: String,
    },
    /// The underlying transport closed before a response was received.
    #[error("transport closed")]
    TransportClosed,
    /// The active transport does not support server-initiated outbound
    /// requests (e.g. plain HTTP without a reverse channel).
    #[error("outbound channel unsupported on this transport")]
    Unsupported,
    /// The outbound request frame could not be serialised. Indicates an
    /// internal invariant violation rather than a transport failure.
    #[error("outbound frame serialisation failed: {0}")]
    Serialisation(#[source] serde_json::Error),
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn display_messages_are_stable() {
        assert_eq!(
            ServerOutboundError::Timeout.to_string(),
            "outbound request timed out"
        );
        assert_eq!(
            ServerOutboundError::PeerError {
                code: -32601,
                message: "Method not found".to_string(),
            }
            .to_string(),
            "peer returned error: code=-32601 message=Method not found"
        );
        assert_eq!(
            ServerOutboundError::TransportClosed.to_string(),
            "transport closed"
        );
        assert_eq!(
            ServerOutboundError::Unsupported.to_string(),
            "outbound channel unsupported on this transport"
        );
    }

    #[test]
    fn serialisation_variant_display_starts_with_prefix() {
        let serde_err =
            serde_json::from_str::<serde_json::Value>("{invalid}").unwrap_err();
        let msg = ServerOutboundError::Serialisation(serde_err).to_string();
        assert!(
            msg.starts_with("outbound frame serialisation failed: "),
            "unexpected display message: {msg}"
        );
    }

    #[test]
    fn trait_object_is_send_and_sync() {
        fn assert_send_sync<T: Send + Sync + ?Sized>() {}
        assert_send_sync::<dyn ServerOutbound>();
    }

    #[tokio::test]
    async fn outbound_request_stub_dispatches_and_returns_value() {
        struct StubOutbound;
        #[async_trait::async_trait]
        impl ServerOutbound for StubOutbound {
            async fn outbound_request(
                &self,
                _method: &str,
                _params: serde_json::Value,
                _timeout: Duration,
            ) -> Result<serde_json::Value, ServerOutboundError> {
                Ok(serde_json::json!({"ok": true}))
            }
        }
        let stub = StubOutbound;
        let result = stub
            .outbound_request("test/method", serde_json::Value::Null, Duration::from_secs(5))
            .await
            .unwrap();
        assert_eq!(result["ok"], true);
    }
}