meerkat_workgraph/
tool_surface.rs1use 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}