meerkat-tools 0.7.9

Tool validation and dispatch for Meerkat
Documentation
//! Comms tool implementations: `send` and `peers`.

use crate::builtin::{BuiltinTool, BuiltinToolError, ToolOutput};
use crate::schema::empty_object_schema;
use async_trait::async_trait;
use meerkat_comms::{Router, ToolContext, TrustedPeersView, handle_tools_call, tools_list};
use meerkat_core::ToolDef;
use meerkat_core::types::{ToolProvenance, ToolSourceKind};
use serde_json::Value;
use std::sync::Arc;

/// Shared state for all comms tools
#[derive(Clone)]
pub struct CommsToolState {
    tool_context: Arc<ToolContext>,
}

impl CommsToolState {
    pub fn new(router: Arc<Router>, trusted_peers: TrustedPeersView) -> Self {
        Self {
            tool_context: Arc::new(ToolContext {
                router,
                trusted_peers,
                runtime: None,
            }),
        }
    }
}

fn comms_provenance() -> Option<ToolProvenance> {
    Some(ToolProvenance {
        kind: ToolSourceKind::Comms,
        source_id: "comms".into(),
    })
}

fn get_tool_def(name: &str) -> ToolDef {
    tools_list()
        .into_iter()
        .find(|t| t["name"].as_str() == Some(name))
        .map_or_else(
            || ToolDef {
                name: name.into(),
                description: String::new(),
                input_schema: empty_object_schema(),
                provenance: comms_provenance(),
            },
            |t| ToolDef {
                name: t["name"].as_str().unwrap_or_default().into(),
                description: t["description"].as_str().unwrap_or_default().to_string(),
                input_schema: t["inputSchema"].clone(),
                provenance: comms_provenance(),
            },
        )
}

/// Unified send tool — dispatches peer_message, peer_request, peer_response via `kind`.
pub struct SendTool {
    state: CommsToolState,
}

impl SendTool {
    pub fn new(state: CommsToolState) -> Self {
        Self { state }
    }
}

#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl BuiltinTool for SendTool {
    fn name(&self) -> &'static str {
        "send"
    }

    fn def(&self) -> ToolDef {
        get_tool_def("send")
    }

    fn default_enabled(&self) -> bool {
        true
    }

    async fn call(&self, args: Value) -> Result<ToolOutput, BuiltinToolError> {
        handle_tools_call(&self.state.tool_context, "send", &args)
            .await
            .map(ToolOutput::Json)
            .map_err(BuiltinToolError::ExecutionFailed)
    }
}

/// Peers discovery tool.
pub struct PeersTool {
    state: CommsToolState,
}

impl PeersTool {
    pub fn new(state: CommsToolState) -> Self {
        Self { state }
    }
}

#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl BuiltinTool for PeersTool {
    fn name(&self) -> &'static str {
        "peers"
    }

    fn def(&self) -> ToolDef {
        get_tool_def("peers")
    }

    fn default_enabled(&self) -> bool {
        true
    }

    async fn call(&self, args: Value) -> Result<ToolOutput, BuiltinToolError> {
        handle_tools_call(&self.state.tool_context, "peers", &args)
            .await
            .map(ToolOutput::Json)
            .map_err(BuiltinToolError::ExecutionFailed)
    }
}

#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
    use super::*;
    use meerkat_comms::{CommsConfig, Keypair};

    fn make_test_state() -> CommsToolState {
        let keypair = Keypair::generate();
        let (_, inbox_sender) = meerkat_comms::Inbox::new();
        let router = Arc::new(Router::new(
            keypair,
            CommsConfig::default(),
            inbox_sender,
            true,
        ));
        let trusted_peers = router.trusted_peers_view();
        CommsToolState::new(router, trusted_peers)
    }

    #[test]
    fn test_send_tool_name() {
        let state = make_test_state();
        let tool = SendTool::new(state);
        assert_eq!(tool.name(), "send");
    }

    #[test]
    fn test_peers_tool_name() {
        let state = make_test_state();
        let tool = PeersTool::new(state);
        assert_eq!(tool.name(), "peers");
    }

    #[tokio::test]
    async fn test_peers_tool_works() {
        let state = make_test_state();
        let tool = PeersTool::new(state);
        let result = tool.call(serde_json::json!({})).await;
        assert!(result.is_ok());
        let output = result.unwrap().into_json().unwrap();
        assert!(output.get("peers").is_some());
    }
}