Skip to main content

codetether_agent/tool/
search_router.rs

1//! `search` router tool — LLM picks a backend, runs it, returns JSON.
2//!
3//! This is a thin `Tool` wrapper over [`crate::search::run_router_search`]
4//! so agents can call the same pipeline the CLI uses.
5
6use std::sync::Arc;
7
8use anyhow::Result;
9use async_trait::async_trait;
10use serde_json::{Value, json};
11
12use super::{Tool, ToolResult};
13use crate::provider::ProviderRegistry;
14use crate::search::{model::DEFAULT_ROUTER_MODEL, run_router_search};
15
16/// Search-router tool. Requires a [`ProviderRegistry`] so the LLM router
17/// can pick the backend.
18pub struct SearchTool {
19    registry: Arc<ProviderRegistry>,
20}
21
22impl SearchTool {
23    pub fn new(registry: Arc<ProviderRegistry>) -> Self {
24        Self { registry }
25    }
26}
27
28#[async_trait]
29impl Tool for SearchTool {
30    fn id(&self) -> &str {
31        "search"
32    }
33    fn name(&self) -> &str {
34        "Search Router"
35    }
36    fn description(&self) -> &str {
37        "search(query: string, top_n?: int, router_model?: string) — LLM-routed search. Picks grep/glob/websearch/webfetch/memory/rlm based on the query and returns normalized JSON."
38    }
39    fn parameters(&self) -> Value {
40        json!({
41            "type": "object",
42            "properties": {
43                "query": {"type": "string", "description": "Natural-language search query"},
44                "top_n": {"type": "integer", "description": "Max backends to run (default 1)"},
45                "router_model": {"type": "string", "description": "Override router model (default zai/glm-5.1)"}
46            },
47            "required": ["query"]
48        })
49    }
50    async fn execute(&self, args: Value) -> Result<ToolResult> {
51        let query = match args["query"].as_str() {
52            Some(q) if !q.is_empty() => q,
53            _ => {
54                return Ok(ToolResult::structured_error(
55                    "INVALID_ARGUMENT",
56                    "search",
57                    "query is required",
58                    Some(vec!["query"]),
59                    Some(json!({"query": "where is fn main"})),
60                ));
61            }
62        };
63        let top_n = args["top_n"].as_u64().unwrap_or(1).max(1) as usize;
64        let router_model = args["router_model"]
65            .as_str()
66            .unwrap_or(DEFAULT_ROUTER_MODEL);
67        match run_router_search(Arc::clone(&self.registry), router_model, query, top_n).await {
68            Ok(result) => {
69                let payload = serde_json::to_string_pretty(&result)?;
70                Ok(ToolResult::success(payload)
71                    .with_metadata("backends", json!(result.runs.len()))
72                    .with_metadata("router_model", json!(result.router_model)))
73            }
74            Err(err) => Ok(ToolResult::error(format!("search router failed: {err}"))),
75        }
76    }
77}