foundation_ai 0.0.1

AI foundation crate for the eweplatform
use foundation_ai::types::{Tool, ToolDefinition};
use std::collections::HashMap;
use std::sync::Arc;

use async_trait::async_trait;
use foundation_ai::agentic::tool_impl::*;
use foundation_ai::types::{ArgType, Args, ExecutionHint, SessionId, TextContent, UserModelContent};

struct EchoTool;

#[async_trait]
impl ToolImpl for EchoTool {
    fn definition(&self) -> Tool {
        Tool::SingleCommand(ToolDefinition {
            name: "echo".into(),
            description: "Echoes the message argument".into(),
            arguments: Args::from_value(serde_json::json!({})),
            category: "shell".into(),
        returns: None,
        })
    }

    async fn execute(
        &self,
        arguments: HashMap<String, ArgType>,
    ) -> Result<ToolCallResult, ToolError> {
        let msg = arguments
            .get("message")
            .and_then(|v| match v {
                ArgType::Text(s) => Some(s.clone()),
                _ => None,
            })
            .unwrap_or_default();
        Ok(ToolCallResult {
            content: UserModelContent::Text(TextContent {
                content: format!("echo: {msg}"),
                signature: None,
            }),
            error_detail: None,
        })
    }
}

#[test]
fn register_and_execute() {
    futures_lite::future::block_on(async {
        let mgr = ToolCallManager::new(SessionId::new());
        mgr.register(Arc::new(EchoTool));

        assert_eq!(mgr.names(), vec!["echo"]);
        assert!(mgr.get("echo").is_some());
        assert!(mgr.get("nonexistent").is_none());

        let request = ToolCallRequest {
            id: "call-1".into(),
            name: "echo".into(),
            arguments: HashMap::from([("message".into(), ArgType::Text("hello".into()))]),
            depends_on: vec![],
            execution_hint: ExecutionHint::default(),
        };
        let result = mgr.execute_one(&request).await.unwrap();
        if let UserModelContent::Text(tc) = result.content {
            assert_eq!(tc.content, "echo: hello");
        }

        mgr.deregister("echo");
        assert!(mgr.get("echo").is_none());
        assert_eq!(mgr.names().len(), 0);
    })
}

#[test]
fn unknown_tool_returns_error() {
    futures_lite::future::block_on(async {
        let mgr = ToolCallManager::new(SessionId::new());
        let request = ToolCallRequest {
            id: "call-1".into(),
            name: "nonexistent".into(),
            arguments: HashMap::new(),
            depends_on: vec![],
            execution_hint: ExecutionHint::default(),
        };
        let err = mgr.execute_one(&request).await.unwrap_err();
        assert_eq!(err, ToolError::UnknownTool("nonexistent".into()));
    })
}

#[test]
fn build_toolshed_populates_by_category() {
    futures_lite::future::block_on(async {
        let mgr = ToolCallManager::new(SessionId::new());
        mgr.register(Arc::new(EchoTool));

        let shed = mgr.build_toolshed();
        // One tool, collected into `tools` (no named slots under F19).
        assert_eq!(shed.tools.len(), 1);
        let echo = &shed.tools[0];
        assert_eq!(echo.name(), "echo");
        assert_eq!(
            echo.definitions().next().unwrap().description,
            "Echoes the message argument"
        );
    })
}

#[test]
fn build_toolshed_empty_has_no_shed_meta_tool() {
    // An empty registry must NOT advertise the `shed` meta-tool: there is
    // nothing to discover, and a phantom `shed()` in every prompt confused
    // small models (docs/fixes/007).
    futures_lite::future::block_on(async {
        let mgr = ToolCallManager::new(SessionId::new());
        let shed = mgr.build_toolshed();
        assert!(
            shed.shed.is_none(),
            "empty registry must not include the shed meta-tool"
        );
        assert!(shed.tools.is_empty());
    })
}

#[test]
fn build_toolshed_with_tools_includes_shed_meta_tool() {
    // With real tools present, the discovery meta-tool is appropriate.
    futures_lite::future::block_on(async {
        let mgr = ToolCallManager::new(SessionId::new());
        mgr.register(Arc::new(EchoTool));
        let shed = mgr.build_toolshed();
        assert_eq!(
            shed.shed.as_ref().unwrap().name(),
            "shed",
            "a populated registry should offer the shed meta-tool"
        );
    })
}