use super::error::Result;
use super::r#trait::{Tool, ToolCapability, ToolExecutionContext, ToolResult};
use async_trait::async_trait;
use serde_json::Value;
pub struct MemorySearchTool;
#[async_trait]
impl Tool for MemorySearchTool {
fn name(&self) -> &str {
"memory_search"
}
fn description(&self) -> &str {
"Search your memory. Returns ranked excerpts, cheap enough to run before \
you write anything. \
\
`scope` picks the corpus, and picking wrong is the usual reason a search \
comes back with nothing useful: \
- \"memory\" (default) — daily logs. History: what happened, when, what was \
decided in a past session. \
- \"brain\" — your brain files (SOUL, USER, AGENTS, TOOLS, CODE, SECURITY, \
MEMORY, BOOT, HEARTBEAT). Rules and policy: does a rule about this ALREADY \
exist, and which file owns it. Use this before appending a rule. \
- \"all\" — both, for \"have I ever written about this anywhere\". \
\
Searching \"memory\" for a rule usually fails: there are far more daily notes \
than brain files, and they reuse the same words for unrelated things, so \
history outranks policy and you get three confident irrelevant hits. \
\
Once a hit tells you WHICH file holds a rule, use `load_brain_file` with a \
`query` to read the whole section — this returns snippets, which are enough \
to locate a rule but not always to judge it."
}
fn input_schema(&self) -> Value {
serde_json::json!({
"type": "object",
"properties": {
"query": {
"type": "string",
"description": "Natural language search query for past memories"
},
"n": {
"type": "integer",
"description": "Number of results to return (default: 5)",
"default": 5
},
"scope": {
"type": "string",
"enum": ["memory", "brain", "all"],
"description": "Which corpus to search: \"memory\" (daily logs, the default) for history, \"brain\" for rules and policy in your brain files, \"all\" for both.",
"default": "memory"
}
},
"required": ["query"]
})
}
fn capabilities(&self) -> Vec<ToolCapability> {
vec![ToolCapability::ReadFiles]
}
fn requires_approval(&self) -> bool {
false
}
async fn execute(&self, input: Value, _context: &ToolExecutionContext) -> Result<ToolResult> {
let query = input
.get("query")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
if query.is_empty() {
return Ok(ToolResult::error("query parameter is required".to_string()));
}
let n = input.get("n").and_then(|v| v.as_u64()).unwrap_or(5) as usize;
let scope = input
.get("scope")
.and_then(|v| v.as_str())
.unwrap_or("memory");
let store = match crate::memory::get_store() {
Ok(s) => s,
Err(e) => {
tracing::warn!("Memory store init failed: {}", e);
return Ok(ToolResult::error(format!(
"Memory search unavailable: {e}. \
Daily memory logs are still saved to your `memory/` dir as markdown files \
that you can read directly with the read_file tool."
)));
}
};
let searched = match scope {
"brain" => crate::memory::search_brain(store, &query, n).await,
"all" => match crate::memory::search_brain(store, &query, n).await {
Ok(mut brain) => match crate::memory::search(store, &query, n).await {
Ok(mem) => {
brain.extend(mem);
Ok(brain)
}
Err(e) => Err(e),
},
Err(e) => Err(e),
},
_ => crate::memory::search(store, &query, n).await,
};
match searched {
Ok(results) if results.is_empty() => Ok(ToolResult::success(format!(
"No matches in scope \"{scope}\".{}",
if scope == "memory" {
" If you were checking whether a RULE already exists, search again \
with scope=\"brain\" — rules live in brain files, not daily logs."
} else {
""
}
))),
Ok(results) => {
let mut output = String::new();
for (i, r) in results.iter().enumerate() {
output.push_str(&format!("{}. **{}**\n {}\n\n", i + 1, r.path, r.snippet));
}
Ok(ToolResult::success(output))
}
Err(e) => Ok(ToolResult::error(format!("Memory search failed: {e}"))),
}
}
}