Skip to main content

synwire_agent/tools/
meta.rs

1//! `meta.*` tool provider for tool discovery and introspection.
2//!
3//! These opt-in tools allow the agent to search for and list available tools at
4//! runtime, supporting progressive disclosure of large tool suites.
5
6use synwire_core::error::SynwireError;
7use synwire_core::tools::{
8    StaticToolProvider, StructuredTool, Tool, ToolOutput, ToolProvider, ToolSchema,
9};
10
11/// Build a tool provider for `meta.*` tools (opt-in).
12///
13/// The returned provider includes:
14/// - `meta.search` (semantic tool search via `ToolSearchIndex`)
15/// - `meta.list` (list available tools with optional namespace filter)
16///
17/// # Errors
18///
19/// Returns [`SynwireError`] if any tool fails validation.
20pub fn meta_tool_provider() -> Result<Box<dyn ToolProvider>, SynwireError> {
21    let tools: Vec<Box<dyn Tool>> =
22        vec![Box::new(build_meta_search()?), Box::new(build_meta_list()?)];
23    Ok(Box::new(StaticToolProvider::new(tools)))
24}
25
26/// Create a stub tool that returns a "not configured" message.
27fn stub_response(tool_name: &str) -> ToolOutput {
28    ToolOutput {
29        content: format!(
30            "{tool_name}: not configured. This tool requires a ToolSearchIndex. \
31             Configure the search index to enable this tool."
32        ),
33        ..Default::default()
34    }
35}
36
37fn build_meta_search() -> Result<StructuredTool, SynwireError> {
38    StructuredTool::builder()
39        .name("meta.search")
40        .description(
41            "Search for available tools by intent. Uses embedding-based retrieval \
42             from the ToolSearchIndex to find the most relevant tools for a task.",
43        )
44        .schema(ToolSchema {
45            name: "meta.search".into(),
46            description: "Search for tools by intent".into(),
47            parameters: serde_json::json!({
48                "type": "object",
49                "properties": {
50                    "query": {
51                        "type": "string",
52                        "description": "Natural language description of what you want to do"
53                    },
54                    "limit": {
55                        "type": "integer",
56                        "description": "Maximum number of tools to return (default: 5)"
57                    },
58                    "namespace": {
59                        "type": "string",
60                        "description": "Restrict search to a namespace (e.g. 'code', 'fs', 'debug')"
61                    }
62                },
63                "required": ["query"],
64                "additionalProperties": false,
65            }),
66        })
67        .func(|_input| Box::pin(async { Ok(stub_response("meta.search")) }))
68        .build()
69}
70
71fn build_meta_list() -> Result<StructuredTool, SynwireError> {
72    StructuredTool::builder()
73        .name("meta.list")
74        .description(
75            "List all available tools, optionally filtered by namespace prefix. \
76             Returns tool names and short descriptions.",
77        )
78        .schema(ToolSchema {
79            name: "meta.list".into(),
80            description: "List available tools".into(),
81            parameters: serde_json::json!({
82                "type": "object",
83                "properties": {
84                    "namespace": {
85                        "type": "string",
86                        "description": "Filter by namespace prefix (e.g. 'code', 'fs')"
87                    }
88                },
89                "additionalProperties": false,
90            }),
91        })
92        .func(|_input| Box::pin(async { Ok(stub_response("meta.list")) }))
93        .build()
94}
95
96#[cfg(test)]
97#[allow(clippy::unwrap_used)]
98mod tests {
99    use super::*;
100
101    #[tokio::test]
102    async fn meta_provider_discovers_all_tools() {
103        let provider = meta_tool_provider().unwrap();
104        let tools = provider.discover_tools().await.unwrap();
105        assert_eq!(tools.len(), 2);
106    }
107
108    #[tokio::test]
109    async fn meta_provider_get_by_name() {
110        let provider = meta_tool_provider().unwrap();
111        let tool = provider.get_tool("meta.search").await.unwrap();
112        assert!(tool.is_some());
113        let tool = provider.get_tool("meta.list").await.unwrap();
114        assert!(tool.is_some());
115        let missing = provider.get_tool("meta.nonexistent").await.unwrap();
116        assert!(missing.is_none());
117    }
118
119    #[tokio::test]
120    async fn stub_tools_return_not_configured() {
121        let provider = meta_tool_provider().unwrap();
122        let tool = provider.get_tool("meta.search").await.unwrap().unwrap();
123        let output = tool
124            .invoke(serde_json::json!({"query": "find files"}))
125            .await
126            .unwrap();
127        assert!(output.content.contains("not configured"));
128    }
129}