use std::sync::Arc;
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use crate::agentic::context::{ContextProvider, SearchMode};
use crate::agentic::memory_store::MemoryStore;
use crate::agentic::tool_impl::{ToolArgs, ToolCallResult, ToolError, ToolImpl};
use crate::agentic::toolshed::ContextSearch;
use crate::types::{Args, Tool, ToolArguments, ToolDefinition};
use foundation_db::traits::DocumentStore;
use foundation_nativeapis::{VfsFileSystem, VfsSearchMatch, VfsSearcher};
pub use foundation_nativeapis::VfsSearchKind as FileSearchKind;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileMatch {
pub path: String,
pub line_number: u32,
pub content: String,
pub score: f32,
}
impl From<VfsSearchMatch> for FileMatch {
fn from(m: VfsSearchMatch) -> Self {
Self {
path: m.path,
line_number: m.line_number,
content: m.content,
score: m.score,
}
}
}
pub trait FileSearch: Send + Sync {
fn search(
&self,
query: &str,
kind: FileSearchKind,
paths: Option<&[String]>,
) -> Result<Vec<FileMatch>, ToolError>;
}
pub struct VfsSearchBackend {
searcher: Box<dyn VfsSearcher>,
root: String,
}
impl VfsSearchBackend {
#[must_use]
pub fn new(searcher: Box<dyn VfsSearcher>, root: String) -> Self {
Self { searcher, root }
}
#[must_use]
pub fn from_vfs<F: VfsFileSystem + 'static>(fs: Arc<F>, root: String) -> Self {
let searcher = foundation_nativeapis::vfs_searcher(fs);
Self { searcher, root }
}
#[cfg(not(target_family = "wasm"))]
#[must_use]
pub fn from_vfs_native<F: VfsFileSystem + 'static>(fs: Arc<F>, root: String) -> Self {
let searcher = foundation_nativeapis::native_vfs_searcher(fs);
Self { searcher, root }
}
}
impl FileSearch for VfsSearchBackend {
fn search(
&self,
query: &str,
kind: FileSearchKind,
paths: Option<&[String]>,
) -> Result<Vec<FileMatch>, ToolError> {
let search_roots: Vec<String> = match paths {
Some(ps) if !ps.is_empty() => ps
.iter()
.map(|p| {
if p.starts_with('/') {
p.clone()
} else if self.root == "/" {
format!("/{p}")
} else {
format!("{}/{p}", self.root)
}
})
.collect(),
_ => vec![self.root.clone()],
};
let vfs_matches = self
.searcher
.search(query, kind, &search_roots)
.map_err(|e| ToolError::Execution {
tool: "search_file".into(),
reason: format!("{e}"),
})?;
Ok(vfs_matches.into_iter().map(FileMatch::from).collect())
}
}
pub struct SearchContextTool {
context: Arc<dyn ContextSearch>,
}
impl SearchContextTool {
#[must_use]
pub fn new(context: impl Into<Arc<dyn ContextSearch>>) -> Self {
Self {
context: context.into(),
}
}
}
impl<D: DocumentStore + 'static, M: MemoryStore + 'static> From<ContextProvider<D, M>>
for Arc<dyn ContextSearch>
{
fn from(context: ContextProvider<D, M>) -> Self {
Arc::new(context)
}
}
#[async_trait]
impl ToolImpl for SearchContextTool {
fn definition(&self) -> Tool {
Tool::SingleCommand(ToolDefinition {
name: "search_context".into(),
description: "Search your knowledge — semantic recall over prior messages, \
distilled memory (observations/reflections), and the code-graph. \
NOT the live filesystem — use search_file for that."
.into(),
arguments: Args::new(
foundation_jsonschema::scheme::object()
.required("query", foundation_jsonschema::scheme::string().min_len(1))
.required(
"mode",
foundation_jsonschema::scheme::string()
.enum_values(vec![
"Semantic".into(),
"Memory".into(),
"Graph".into(),
"Hybrid".into(),
]),
)
.optional("k", foundation_jsonschema::scheme::integer().min(1))
.build(),
),
category: "search".into(),
returns: None,
})
}
async fn execute(&self, arguments: ToolArguments) -> Result<ToolCallResult, ToolError> {
let args = ToolArgs::new("search_context", &arguments);
let query = args.str("query")?.to_string();
let mode = match args.opt_str("mode")? {
Some(mode) => parse_search_mode(mode)?,
None => SearchMode::Hybrid,
};
let k = args.opt_usize("k")?.unwrap_or(10);
let hits = self.context.search(&query, mode, k).await;
let json = serde_json::to_string(&hits).map_err(|e| ToolError::Execution {
tool: "search_context".into(),
reason: format!("serializing the hits failed: {e}"),
})?;
Ok(ToolCallResult::text(json))
}
}
pub fn parse_search_mode(s: &str) -> Result<SearchMode, ToolError> {
match s {
"Semantic" | "semantic" => Ok(SearchMode::Semantic),
"Memory" | "memory" => Ok(SearchMode::Memory),
"Graph" | "graph" => Ok(SearchMode::Graph),
"Hybrid" | "hybrid" => Ok(SearchMode::Hybrid),
other => Err(ToolError::InvalidArguments {
tool: "search_context".into(),
reason: format!("unknown search mode: {other}"),
}),
}
}
pub struct SearchFileTool {
backend: Arc<dyn FileSearch>,
}
impl SearchFileTool {
#[must_use]
pub fn new(backend: Arc<dyn FileSearch>) -> Self {
Self { backend }
}
#[must_use]
pub fn from_vfs<F: VfsFileSystem + 'static>(fs: Arc<F>, root: String) -> Self {
Self {
backend: Arc::new(VfsSearchBackend::from_vfs(fs, root)),
}
}
#[must_use]
pub fn from_searcher(searcher: Box<dyn VfsSearcher>, root: String) -> Self {
Self {
backend: Arc::new(VfsSearchBackend::new(searcher, root)),
}
}
#[cfg(not(target_family = "wasm"))]
#[must_use]
pub fn native<F: VfsFileSystem + 'static>(fs: Arc<F>, root: String) -> Self {
Self {
backend: Arc::new(VfsSearchBackend::from_vfs_native(fs, root)),
}
}
}
#[async_trait]
impl ToolImpl for SearchFileTool {
fn definition(&self) -> Tool {
Tool::SingleCommand(ToolDefinition {
name: "search_file".into(),
description: "Search files: grep file content, find by path pattern, \
multi-file grep. NOT for memory recall — use search_context."
.into(),
arguments: Args::new(
foundation_jsonschema::scheme::object()
.required("query", foundation_jsonschema::scheme::string().min_len(1))
.optional(
"kind",
foundation_jsonschema::scheme::string()
.enum_values(vec![
"Grep".into(),
"Find".into(),
"MultiGrep".into(),
]),
)
.optional(
"paths",
foundation_jsonschema::scheme::array_of(
foundation_jsonschema::scheme::string().build_schema(),
),
)
.build(),
),
category: "search_files".into(),
returns: None,
})
}
async fn execute(&self, arguments: ToolArguments) -> Result<ToolCallResult, ToolError> {
let args = ToolArgs::new("search_file", &arguments);
let query = args.str("query")?.to_string();
let kind = match args.opt_str("kind")? {
Some(kind) => parse_file_search_kind(kind)?,
None => FileSearchKind::Grep,
};
let paths: Option<Vec<String>> = args
.opt_value("paths")
.map(serde_json::from_value)
.transpose()
.map_err(|e| ToolError::InvalidArguments {
tool: "search_file".into(),
reason: format!("`paths` must be a list of strings: {e}"),
})?;
let matches = self
.backend
.search(&query, kind, paths.as_deref())?;
let json = serde_json::to_string(&matches).map_err(|e| ToolError::Execution {
tool: "search_file".into(),
reason: format!("serializing the matches failed: {e}"),
})?;
Ok(ToolCallResult::text(json))
}
}
pub fn parse_file_search_kind(s: &str) -> Result<FileSearchKind, ToolError> {
match s {
"Grep" | "grep" => Ok(FileSearchKind::Grep),
"Find" | "find" => Ok(FileSearchKind::Find),
"MultiGrep" | "multi_grep" | "multigrep" => Ok(FileSearchKind::MultiGrep),
other => Err(ToolError::InvalidArguments {
tool: "search_file".into(),
reason: format!("unknown file search kind: {other}"),
}),
}
}