use std::sync::{Arc, Mutex};
use cortex_store::repo::MemoryRepo;
use serde_json::{json, Value};
use crate::{GateId, ToolError, ToolHandler};
const DEFAULT_LIMIT: usize = 20;
const MAX_LIMIT: usize = 100;
#[derive(Debug)]
pub struct CortexMemoryListTool {
pool: Arc<Mutex<cortex_store::Pool>>,
}
impl CortexMemoryListTool {
#[must_use]
pub fn new(pool: Arc<Mutex<cortex_store::Pool>>) -> Self {
Self { pool }
}
}
impl ToolHandler for CortexMemoryListTool {
fn name(&self) -> &'static str {
"cortex_memory_list"
}
fn gate_set(&self) -> &'static [GateId] {
&[GateId::FtsRead]
}
fn call(&self, params: Value) -> Result<Value, ToolError> {
let domains: Vec<String> = match params.get("domains") {
None | Some(Value::Null) => Vec::new(),
Some(Value::Array(arr)) => {
let mut tags = Vec::with_capacity(arr.len());
for (i, v) in arr.iter().enumerate() {
match v.as_str() {
Some(s) => tags.push(s.to_owned()),
None => {
return Err(ToolError::InvalidParams(format!(
"domains[{i}] must be a string"
)));
}
}
}
tags
}
Some(other) => {
return Err(ToolError::InvalidParams(format!(
"domains must be an array of strings, got {other}"
)));
}
};
let limit: usize = match params.get("limit") {
None | Some(Value::Null) => DEFAULT_LIMIT,
Some(v) => {
let n = v.as_u64().ok_or_else(|| {
ToolError::InvalidParams("limit must be a non-negative integer".into())
})?;
let n = usize::try_from(n).unwrap_or(MAX_LIMIT);
n.min(MAX_LIMIT)
}
};
let offset: usize = match params.get("offset") {
None | Some(Value::Null) => 0,
Some(v) => {
let n = v.as_u64().ok_or_else(|| {
ToolError::InvalidParams("offset must be a non-negative integer".into())
})?;
usize::try_from(n).unwrap_or(0)
}
};
let pool = self
.pool
.lock()
.map_err(|err| ToolError::Internal(format!("failed to acquire store lock: {err}")))?;
let repo = MemoryRepo::new(&pool);
let all_memories = if domains.is_empty() {
repo.list_by_status("active").map_err(|err| {
ToolError::Internal(format!("failed to read active memories: {err}"))
})?
} else {
repo.list_by_status_with_tags("active", &domains)
.map_err(|err| {
ToolError::Internal(format!(
"failed to read tag-filtered active memories: {err}"
))
})?
};
let total = all_memories.len();
let page: Vec<Value> = all_memories
.into_iter()
.skip(offset)
.take(limit)
.map(|m| {
let domains_list = string_array(&m.domains_json);
json!({
"id": m.id.to_string(),
"content": m.claim,
"domains": domains_list,
"confidence": m.confidence,
"created_at": m.created_at.to_rfc3339(),
})
})
.collect();
Ok(json!({
"memories": page,
"total": total,
}))
}
}
fn string_array(value: &Value) -> Vec<String> {
value
.as_array()
.into_iter()
.flatten()
.filter_map(|v| v.as_str().map(ToOwned::to_owned))
.collect()
}