meerkat-workgraph 0.6.16

Realm-scoped durable work graph subsystem for Meerkat
Documentation
use std::sync::Arc;

use async_trait::async_trait;
use meerkat_core::AgentToolDispatcher;
use meerkat_core::error::ToolError;
use meerkat_core::types::{ToolCallView, ToolDef, ToolProvenance, ToolResult, ToolSourceKind};
use serde_json::Value;

use crate::{WorkGraphService, handle_workgraph_tools_call, workgraph_tools_list};

pub struct WorkGraphToolSurface {
    service: WorkGraphService,
    tool_defs: Arc<[Arc<ToolDef>]>,
}

impl WorkGraphToolSurface {
    pub fn new(service: WorkGraphService) -> Self {
        Self {
            service,
            tool_defs: build_tool_defs(),
        }
    }

    pub fn service(&self) -> &WorkGraphService {
        &self.service
    }
}

#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
impl AgentToolDispatcher for WorkGraphToolSurface {
    fn tools(&self) -> Arc<[Arc<ToolDef>]> {
        Arc::clone(&self.tool_defs)
    }

    async fn dispatch(
        &self,
        call: ToolCallView<'_>,
    ) -> Result<meerkat_core::ops::ToolDispatchOutcome, ToolError> {
        if !self.tool_defs.iter().any(|tool| tool.name == call.name) {
            return Err(ToolError::NotFound {
                name: call.name.into(),
            });
        }
        let args: Value = serde_json::from_str(call.args.get())
            .unwrap_or_else(|_| Value::String(call.args.get().to_string()));
        let result = handle_workgraph_tools_call(&self.service, call.name, &args)
            .await
            .map_err(|error| ToolError::ExecutionFailed {
                message: format!("{} (code {})", error.message, error.code),
            })?;
        Ok(ToolResult::new(call.id.to_string(), result.to_string(), false).into())
    }
}

fn build_tool_defs() -> Arc<[Arc<ToolDef>]> {
    workgraph_tools_list()
        .into_iter()
        .map(|tool| {
            Arc::new(ToolDef {
                name: tool["name"].as_str().unwrap_or_default().into(),
                description: tool["description"].as_str().unwrap_or_default().to_string(),
                input_schema: tool["inputSchema"].clone(),
                provenance: Some(ToolProvenance {
                    kind: ToolSourceKind::WorkGraph,
                    source_id: "workgraph".into(),
                }),
            })
        })
        .collect::<Vec<_>>()
        .into()
}

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

    use serde_json::json;

    use crate::{MemoryWorkGraphStore, WorkGraphService};

    #[tokio::test]
    async fn workgraph_tool_surface_dispatches_tools() {
        let surface =
            WorkGraphToolSurface::new(WorkGraphService::new(Arc::new(MemoryWorkGraphStore::new())));
        let args = serde_json::value::RawValue::from_string(
            json!({ "title": "surface item" }).to_string(),
        )
        .unwrap();
        let outcome = surface
            .dispatch(ToolCallView {
                id: "call-1",
                name: "workgraph_create",
                args: &args,
            })
            .await
            .expect("dispatch");
        let value: Value = serde_json::from_str(&outcome.result.text_content()).unwrap();
        assert_eq!(value["item"]["title"].as_str(), Some("surface item"));
    }

    #[test]
    fn workgraph_tool_defs_have_workgraph_provenance() {
        let surface =
            WorkGraphToolSurface::new(WorkGraphService::new(Arc::new(MemoryWorkGraphStore::new())));
        assert!(surface.tools().iter().all(|tool| {
            tool.provenance
                .as_ref()
                .is_some_and(|p| p.kind == ToolSourceKind::WorkGraph)
        }));
    }
}