synwire_agent/tools/
meta.rs1use synwire_core::error::SynwireError;
7use synwire_core::tools::{
8 StaticToolProvider, StructuredTool, Tool, ToolOutput, ToolProvider, ToolSchema,
9};
10
11pub 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
26fn 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}