#![forbid(unsafe_code)]
use async_trait::async_trait;
use serde_json::{Value, json};
use std::sync::Arc;
use wm_core::{Context, EffectRow, Galaxy, Gana, Resource, Tool, ToolStats};
use wm_memory::{AssociationStore, MemoryStore};
use super::common::{galaxy_name, parse_galaxy};
pub struct PatternSearchTool {
store: Arc<MemoryStore>,
stats: ToolStats,
effects: EffectRow,
}
impl PatternSearchTool {
pub fn new(store: Arc<MemoryStore>) -> Self {
Self {
store,
stats: ToolStats::default(),
effects: EffectRow::read_only(vec![Resource::Galaxy("universal".into())]),
}
}
}
#[async_trait]
impl Tool for PatternSearchTool {
fn name(&self) -> &str {
"pattern.search"
}
fn gana(&self) -> Gana {
Gana::Ox
}
fn effects(&self) -> &EffectRow {
&self.effects
}
fn description(&self) -> &str {
"Search for patterns in memory content across galaxies"
}
async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
let pattern = args.get("pattern").and_then(|v| v.as_str()).unwrap_or("");
let galaxies = args.get("galaxies").and_then(|v| v.as_array());
let limit = args
.get("limit")
.and_then(serde_json::Value::as_u64)
.unwrap_or(20) as usize;
let galaxies_to_search: Vec<Galaxy> = match galaxies {
Some(arr) => {
let mut parsed = Vec::new();
for g in arr {
if let Some(name) = g.as_str() {
parsed.push(parse_galaxy(name)?);
}
}
parsed
}
None => Galaxy::memory_galaxies().to_vec(),
};
let mut matches = Vec::new();
for galaxy in &galaxies_to_search {
let memories = self.store.scan(*galaxy, 500)?;
for mem in memories {
if mem.content.to_lowercase().contains(&pattern.to_lowercase()) {
matches.push(json!({
"galaxy": galaxy_name(*galaxy),
"id": mem.metadata.id,
"content_preview": mem.content.chars().take(100).collect::<String>(),
}));
if matches.len() >= limit {
break;
}
}
}
if matches.len() >= limit {
break;
}
}
Ok(json!({
"status": "success",
"pattern": pattern,
"matches": matches.len(),
"results": matches,
}))
}
fn stats(&self) -> &ToolStats {
&self.stats
}
}
pub struct SalienceSpotlightTool {
store: Arc<MemoryStore>,
stats: ToolStats,
effects: EffectRow,
}
impl SalienceSpotlightTool {
pub fn new(store: Arc<MemoryStore>) -> Self {
Self {
store,
stats: ToolStats::default(),
effects: EffectRow::read_only(vec![Resource::Galaxy("universal".into())]),
}
}
}
#[async_trait]
impl Tool for SalienceSpotlightTool {
fn name(&self) -> &str {
"salience.spotlight"
}
fn gana(&self) -> Gana {
Gana::Ox
}
fn effects(&self) -> &EffectRow {
&self.effects
}
fn description(&self) -> &str {
"Find high-importance memories across all galaxies"
}
async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
let min_importance = args
.get("min_importance")
.and_then(serde_json::Value::as_f64)
.unwrap_or(0.8) as f32;
let limit = args
.get("limit")
.and_then(serde_json::Value::as_u64)
.unwrap_or(20) as usize;
let mut spotlighted = Vec::new();
for galaxy in Galaxy::memory_galaxies() {
let memories = self.store.scan(galaxy, 200)?;
for mem in memories {
if mem.metadata.importance >= min_importance {
spotlighted.push(json!({
"galaxy": galaxy_name(galaxy),
"id": mem.metadata.id,
"importance": mem.metadata.importance,
"content_preview": mem.content.chars().take(80).collect::<String>(),
}));
}
}
}
spotlighted.sort_by(|a, b| {
b["importance"]
.as_f64()
.unwrap_or(0.0)
.partial_cmp(&a["importance"].as_f64().unwrap_or(0.0))
.unwrap_or(std::cmp::Ordering::Equal)
});
spotlighted.truncate(limit);
Ok(json!({
"status": "success",
"min_importance": min_importance,
"count": spotlighted.len(),
"spotlight": spotlighted,
}))
}
fn stats(&self) -> &ToolStats {
&self.stats
}
}
pub struct SerendipitySurfaceTool {
store: Arc<MemoryStore>,
stats: ToolStats,
effects: EffectRow,
}
impl SerendipitySurfaceTool {
pub fn new(store: Arc<MemoryStore>) -> Self {
Self {
store,
stats: ToolStats::default(),
effects: EffectRow::read_only(vec![Resource::Galaxy("associations".into())]),
}
}
}
#[async_trait]
impl Tool for SerendipitySurfaceTool {
fn name(&self) -> &str {
"serendipity.surface"
}
fn gana(&self) -> Gana {
Gana::Star
}
fn effects(&self) -> &EffectRow {
&self.effects
}
fn description(&self) -> &str {
"Surface unexpected cross-galaxy connections from associations"
}
async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
let env = self.store.env();
let assoc_store = AssociationStore::open(env)?;
let total = assoc_store.count(env)?;
let mut cross_galaxy: Vec<Value> = Vec::new();
for galaxy in Galaxy::memory_galaxies() {
let memories = self.store.scan(galaxy, 50)?;
for mem in &memories {
let assocs = assoc_store.find_from(env, mem.metadata.id)?;
for assoc in &assocs {
for other_galaxy in Galaxy::memory_galaxies() {
if other_galaxy != galaxy {
if let Ok(Some(_)) = self.store.get(other_galaxy, assoc.target) {
cross_galaxy.push(json!({
"source_galaxy": galaxy_name(galaxy),
"target_galaxy": galaxy_name(other_galaxy),
"weight": assoc.weight,
"link_type": assoc.link_type.as_str(),
"association_type": assoc.association_type,
}));
}
}
}
}
}
}
Ok(json!({
"status": "success",
"total_associations": total,
"cross_galaxy_links": cross_galaxy.len(),
"serendipities": cross_galaxy.into_iter().take(20).collect::<Vec<_>>(),
}))
}
fn stats(&self) -> &ToolStats {
&self.stats
}
}
#[cfg(test)]
mod tests {
use super::*;
use wm_memory::{Association, LinkType, Memory};
fn open_store() -> (tempfile::TempDir, Arc<MemoryStore>) {
let tmp = tempfile::tempdir().unwrap();
let store = Arc::new(MemoryStore::open_default(tmp.path()).unwrap());
(tmp, store)
}
#[tokio::test]
async fn pattern_search_is_case_insensitive_and_galaxy_scoped() {
let (_tmp, store) = open_store();
let mut codex = Memory::new(Galaxy::Codex, "Needle in the codex haystack".into());
codex.metadata.id = uuid::Uuid::from_u128(0x701);
store.put(Galaxy::Codex, &codex).unwrap();
let mut research = Memory::new(Galaxy::Research, "needle elsewhere entirely".into());
research.metadata.id = uuid::Uuid::from_u128(0x702);
store.put(Galaxy::Research, &research).unwrap();
let scoped = PatternSearchTool::new(store.clone())
.call(
&mut Context::default(),
json!({"pattern": "NEEDLE", "galaxies": ["codex"]}),
)
.await
.unwrap();
assert_eq!(scoped["matches"], 1);
assert_eq!(
scoped["results"][0]["id"].as_str().unwrap(),
codex.metadata.id.to_string()
);
let absent = PatternSearchTool::new(store)
.call(&mut Context::default(), json!({"pattern": "zzz-absent"}))
.await
.unwrap();
assert_eq!(absent["matches"], 0);
}
#[tokio::test]
async fn salience_spotlight_filters_then_sorts_by_importance() {
let (_tmp, store) = open_store();
for (id, importance) in [(0x711u128, 0.9f32), (0x712, 0.85), (0x713, 0.2)] {
let mut memory = Memory::new(Galaxy::Codex, format!("importance fixture {id}"));
memory.metadata.id = uuid::Uuid::from_u128(id);
memory.metadata.importance = importance;
store.put(Galaxy::Codex, &memory).unwrap();
}
let result = SalienceSpotlightTool::new(store)
.call(
&mut Context::default(),
json!({"min_importance": 0.8, "limit": 1}),
)
.await
.unwrap();
assert_eq!(result["count"], 1);
assert_eq!(
result["spotlight"][0]["id"].as_str().unwrap(),
uuid::Uuid::from_u128(0x711).to_string()
);
assert!((result["spotlight"][0]["importance"].as_f64().unwrap() - 0.9).abs() < 1e-6);
}
#[tokio::test]
async fn serendipity_surfaces_only_cross_galaxy_links() {
let (_tmp, store) = open_store();
let mut source = Memory::new(Galaxy::Codex, "origin".into());
source.metadata.id = uuid::Uuid::from_u128(0x721);
let mut cross = Memory::new(Galaxy::Research, "cross galaxy target".into());
cross.metadata.id = uuid::Uuid::from_u128(0x722);
let mut same = Memory::new(Galaxy::Codex, "same galaxy target".into());
same.metadata.id = uuid::Uuid::from_u128(0x723);
store.put(Galaxy::Codex, &source).unwrap();
store.put(Galaxy::Research, &cross).unwrap();
store.put(Galaxy::Codex, &same).unwrap();
let env = store.env();
let assoc_store = AssociationStore::open(env).unwrap();
assoc_store
.put(
env,
&Association::new(
source.metadata.id,
cross.metadata.id,
LinkType::Related,
0.9,
),
)
.unwrap();
assoc_store
.put(
env,
&Association::new(source.metadata.id, same.metadata.id, LinkType::Related, 0.5),
)
.unwrap();
let result = SerendipitySurfaceTool::new(store)
.call(&mut Context::default(), json!({}))
.await
.unwrap();
assert_eq!(result["total_associations"], 2);
assert_eq!(result["cross_galaxy_links"], 1);
assert_eq!(result["serendipities"][0]["source_galaxy"], "codex");
assert_eq!(result["serendipities"][0]["target_galaxy"], "research");
}
}