Skip to main content

meerkat_workgraph/
tool_surface.rs

1use std::sync::Arc;
2
3use async_trait::async_trait;
4use meerkat_core::AgentToolDispatcher;
5use meerkat_core::error::ToolError;
6use meerkat_core::types::{ToolCallView, ToolDef, ToolProvenance, ToolResult, ToolSourceKind};
7use serde_json::Value;
8
9use crate::{WorkGraphService, handle_workgraph_tools_call, workgraph_tools_list};
10
11pub struct WorkGraphToolSurface {
12    service: WorkGraphService,
13    tool_defs: Arc<[Arc<ToolDef>]>,
14}
15
16impl WorkGraphToolSurface {
17    pub fn new(service: WorkGraphService) -> Self {
18        Self {
19            service,
20            tool_defs: build_tool_defs(),
21        }
22    }
23
24    pub fn service(&self) -> &WorkGraphService {
25        &self.service
26    }
27}
28
29#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
30#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
31impl AgentToolDispatcher for WorkGraphToolSurface {
32    fn tools(&self) -> Arc<[Arc<ToolDef>]> {
33        Arc::clone(&self.tool_defs)
34    }
35
36    async fn dispatch(
37        &self,
38        call: ToolCallView<'_>,
39    ) -> Result<meerkat_core::ops::ToolDispatchOutcome, ToolError> {
40        if !self.tool_defs.iter().any(|tool| tool.name == call.name) {
41            return Err(ToolError::NotFound {
42                name: call.name.into(),
43            });
44        }
45        let args: Value = serde_json::from_str(call.args.get())
46            .unwrap_or_else(|_| Value::String(call.args.get().to_string()));
47        let result = handle_workgraph_tools_call(&self.service, call.name, &args)
48            .await
49            .map_err(|error| ToolError::ExecutionFailed {
50                message: format!("{} (code {})", error.message, error.code),
51            })?;
52        Ok(ToolResult::new(call.id.to_string(), result.to_string(), false).into())
53    }
54}
55
56fn build_tool_defs() -> Arc<[Arc<ToolDef>]> {
57    workgraph_tools_list()
58        .into_iter()
59        .map(|tool| {
60            Arc::new(ToolDef {
61                name: tool["name"].as_str().unwrap_or_default().into(),
62                description: tool["description"].as_str().unwrap_or_default().to_string(),
63                input_schema: tool["inputSchema"].clone(),
64                provenance: Some(ToolProvenance {
65                    kind: ToolSourceKind::WorkGraph,
66                    source_id: "workgraph".into(),
67                }),
68            })
69        })
70        .collect::<Vec<_>>()
71        .into()
72}
73
74#[cfg(test)]
75#[allow(clippy::expect_used, clippy::unwrap_used)]
76mod tests {
77    use super::*;
78
79    use serde_json::json;
80
81    use crate::{MemoryWorkGraphStore, WorkGraphService};
82
83    #[tokio::test]
84    async fn workgraph_tool_surface_dispatches_tools() {
85        let surface =
86            WorkGraphToolSurface::new(WorkGraphService::new(Arc::new(MemoryWorkGraphStore::new())));
87        let args = serde_json::value::RawValue::from_string(
88            json!({ "title": "surface item" }).to_string(),
89        )
90        .unwrap();
91        let outcome = surface
92            .dispatch(ToolCallView {
93                id: "call-1",
94                name: "workgraph_create",
95                args: &args,
96            })
97            .await
98            .expect("dispatch");
99        let value: Value = serde_json::from_str(&outcome.result.text_content()).unwrap();
100        assert_eq!(value["item"]["title"].as_str(), Some("surface item"));
101    }
102
103    #[test]
104    fn workgraph_tool_defs_have_workgraph_provenance() {
105        let surface =
106            WorkGraphToolSurface::new(WorkGraphService::new(Arc::new(MemoryWorkGraphStore::new())));
107        assert!(surface.tools().iter().all(|tool| {
108            tool.provenance
109                .as_ref()
110                .is_some_and(|p| p.kind == ToolSourceKind::WorkGraph)
111        }));
112    }
113}