codewhale-tui 0.8.62

Terminal UI for open-source and open-weight coding models
use async_trait::async_trait;
use codewhale_protocol::runtime::DynamicToolSpec;
use serde_json::Value;

use crate::tools::spec::{
    ApprovalRequirement, ToolCapability, ToolContext, ToolError, ToolResult, ToolSpec,
};

pub struct RuntimeDynamicTool {
    spec: DynamicToolSpec,
}

impl RuntimeDynamicTool {
    pub fn new(spec: DynamicToolSpec) -> Self {
        Self { spec }
    }
}

#[async_trait]
impl ToolSpec for RuntimeDynamicTool {
    fn name(&self) -> &str {
        &self.spec.name
    }

    fn description(&self) -> &str {
        &self.spec.description
    }

    fn input_schema(&self) -> Value {
        self.spec.input_schema.clone()
    }

    fn capabilities(&self) -> Vec<ToolCapability> {
        Vec::new()
    }

    fn approval_requirement(&self) -> ApprovalRequirement {
        ApprovalRequirement::Auto
    }

    fn supports_parallel(&self) -> bool {
        false
    }

    fn defer_loading(&self) -> bool {
        self.spec.defer_loading
    }

    async fn execute(&self, input: Value, context: &ToolContext) -> Result<ToolResult, ToolError> {
        let executor = context
            .runtime
            .dynamic_tool_executor
            .as_ref()
            .ok_or_else(|| {
                ToolError::not_available(format!(
                    "runtime dynamic tool '{}' has no executor",
                    self.spec.name
                ))
            })?;
        executor
            .execute_dynamic_tool(
                context.runtime.active_thread_id.clone(),
                self.spec.namespace.clone(),
                self.spec.name.clone(),
                input,
            )
            .await
    }
}

#[cfg(test)]
mod tests {
    use std::sync::Arc;

    use async_trait::async_trait;
    use serde_json::{Value, json};

    use super::*;
    use crate::tools::spec::{DynamicToolExecutor, RuntimeToolServices};

    struct EchoExecutor;

    #[async_trait]
    impl DynamicToolExecutor for EchoExecutor {
        async fn execute_dynamic_tool(
            &self,
            thread_id: Option<String>,
            namespace: Option<String>,
            name: String,
            input: Value,
        ) -> Result<ToolResult, ToolError> {
            Ok(ToolResult::success(
                json!({
                    "thread_id": thread_id,
                    "namespace": namespace,
                    "name": name,
                    "input": input,
                })
                .to_string(),
            ))
        }
    }

    #[tokio::test]
    async fn runtime_dynamic_tool_delegates_to_runtime_executor() {
        let tool = RuntimeDynamicTool::new(DynamicToolSpec {
            namespace: Some("bench".to_string()),
            name: "lookup".to_string(),
            description: "Lookup a record".to_string(),
            input_schema: json!({"type": "object"}),
            defer_loading: true,
        });
        let ctx = ToolContext::new(".").with_runtime_services(RuntimeToolServices {
            active_thread_id: Some("thr_1".to_string()),
            dynamic_tool_executor: Some(Arc::new(EchoExecutor)),
            ..RuntimeToolServices::default()
        });

        let result = tool.execute(json!({"id": "123"}), &ctx).await.unwrap();

        assert!(result.success);
        assert!(result.content.contains("\"thread_id\":\"thr_1\""));
        assert!(result.content.contains("\"namespace\":\"bench\""));
        assert!(result.content.contains("\"name\":\"lookup\""));
    }
}