Skip to main content

wm_tools/expansion/
additional.rs

1//! Additional tools — count, tags, session_list, citta_coherence, dharma_profiles, nearby.
2
3#![forbid(unsafe_code)]
4
5use async_trait::async_trait;
6
7use serde_json::{Value, json};
8use std::sync::Arc;
9use wm_core::{Context, EffectRow, Galaxy, Gana, Resource, Tool, ToolStats};
10use wm_memory::MemoryStore;
11
12use super::common::{galaxy_name, parse_galaxy, parse_galaxy_or};
13
14pub struct MemoryCountTool {
15    store: Arc<MemoryStore>,
16    stats: ToolStats,
17    effects: EffectRow,
18}
19
20impl MemoryCountTool {
21    pub fn new(store: Arc<MemoryStore>) -> Self {
22        Self {
23            store,
24            stats: ToolStats::default(),
25            effects: EffectRow::read_only(vec![Resource::Galaxy("codex".into())]),
26        }
27    }
28}
29
30#[async_trait]
31impl Tool for MemoryCountTool {
32    fn name(&self) -> &str {
33        "memory.count"
34    }
35    fn gana(&self) -> Gana {
36        Gana::WinnowingBasket
37    }
38    fn effects(&self) -> &EffectRow {
39        &self.effects
40    }
41    fn description(&self) -> &str {
42        "Count memories in a galaxy"
43    }
44    async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
45        let galaxy = parse_galaxy_or(args.get("galaxy").and_then(|v| v.as_str()), Galaxy::Codex)?;
46        let count = self.store.count(galaxy)?;
47        Ok(json!({ "status": "success", "galaxy": galaxy_name(galaxy), "count": count }))
48    }
49    fn stats(&self) -> &ToolStats {
50        &self.stats
51    }
52}
53
54/// `memory.tags` — list all unique tags in a galaxy.
55pub struct MemoryTagsTool {
56    store: Arc<MemoryStore>,
57    stats: ToolStats,
58    effects: EffectRow,
59}
60
61impl MemoryTagsTool {
62    pub fn new(store: Arc<MemoryStore>) -> Self {
63        Self {
64            store,
65            stats: ToolStats::default(),
66            effects: EffectRow::read_only(vec![Resource::Galaxy("codex".into())]),
67        }
68    }
69}
70
71#[async_trait]
72impl Tool for MemoryTagsTool {
73    fn name(&self) -> &str {
74        "memory.tags"
75    }
76    fn gana(&self) -> Gana {
77        Gana::Net
78    }
79    fn effects(&self) -> &EffectRow {
80        &self.effects
81    }
82    fn description(&self) -> &str {
83        "List all unique tags in a galaxy"
84    }
85    async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
86        let galaxy = parse_galaxy_or(args.get("galaxy").and_then(|v| v.as_str()), Galaxy::Codex)?;
87        let memories = self.store.scan(galaxy, 10_000)?;
88        let tags: std::collections::HashSet<String> = memories
89            .iter()
90            .flat_map(|m| m.metadata.tags.iter().cloned())
91            .collect();
92        let mut tag_list: Vec<String> = tags.into_iter().collect();
93        tag_list.sort_unstable();
94        Ok(
95            json!({ "status": "success", "galaxy": galaxy_name(galaxy), "unique_tags": tag_list.len(), "tags": tag_list }),
96        )
97    }
98    fn stats(&self) -> &ToolStats {
99        &self.stats
100    }
101}
102
103/// `session.list` — list all sessions.
104pub struct SessionListTool {
105    store: Arc<MemoryStore>,
106    stats: ToolStats,
107    effects: EffectRow,
108}
109
110impl SessionListTool {
111    pub fn new(store: Arc<MemoryStore>) -> Self {
112        Self {
113            store,
114            stats: ToolStats::default(),
115            effects: EffectRow::read_only(vec![Resource::Galaxy("sessions".into())]),
116        }
117    }
118}
119
120#[async_trait]
121impl Tool for SessionListTool {
122    fn name(&self) -> &str {
123        "session.list"
124    }
125    fn gana(&self) -> Gana {
126        Gana::StraddlingLegs
127    }
128    fn effects(&self) -> &EffectRow {
129        &self.effects
130    }
131    fn description(&self) -> &str {
132        "List session summaries in the Sessions galaxy (turns grouped by session)"
133    }
134    async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
135        let memories = self.store.scan_all(Galaxy::Sessions)?;
136
137        // Group turns and start markers into session summaries.
138        #[derive(Default)]
139        struct Summary {
140            title: Option<String>,
141            turns: u64,
142            earliest: Option<chrono::DateTime<chrono::Utc>>,
143            latest: Option<chrono::DateTime<chrono::Utc>>,
144        }
145        let mut summaries: std::collections::HashMap<String, Summary> =
146            std::collections::HashMap::new();
147
148        for m in &memories {
149            if let Ok(v) = serde_json::from_str::<Value>(&m.content) {
150                // session_start: session id lives in the tag.
151                if v.get("type").and_then(Value::as_str) == Some("session_start") {
152                    if let Some(sid) = m
153                        .metadata
154                        .tags
155                        .iter()
156                        .find_map(|t| t.strip_prefix("session:"))
157                    {
158                        let entry = summaries.entry(sid.to_string()).or_default();
159                        entry.title = v.get("title").and_then(Value::as_str).map(String::from);
160                    }
161                    continue;
162                }
163                // session_turn (and other session memories): session id in content.
164                if let Some(sid) = v.get("session_id").and_then(Value::as_str) {
165                    let entry = summaries.entry(sid.to_string()).or_default();
166                    let ts = m.metadata.created_at;
167                    entry.earliest = Some(entry.earliest.map_or(ts, |e| e.min(ts)));
168                    entry.latest = Some(entry.latest.map_or(ts, |l| l.max(ts)));
169                    if v.get("sequence").and_then(Value::as_u64).is_some() {
170                        entry.turns += 1;
171                    }
172                }
173            }
174        }
175
176        let mut sessions: Vec<(String, Summary)> = summaries.into_iter().collect();
177        // Most recently active sessions first.
178        sessions.sort_by_key(|(_, s)| {
179            std::cmp::Reverse(
180                s.latest
181                    .unwrap_or(chrono::DateTime::<chrono::Utc>::UNIX_EPOCH),
182            )
183        });
184        let total = sessions.len();
185        sessions.truncate(100);
186
187        let sessions: Vec<Value> = sessions
188            .into_iter()
189            .map(|(sid, s)| {
190                json!({
191                    "session_id": sid,
192                    "title": s.title.unwrap_or_else(|| format!("Session {}", &sid[..sid.len().min(8)])),
193                    "turns": s.turns,
194                    "first_activity": s.earliest.map(|t| t.to_rfc3339()),
195                    "last_activity": s.latest.map(|t| t.to_rfc3339()),
196                })
197            })
198            .collect();
199
200        Ok(json!({
201            "status": "success",
202            "count": total,
203            "sessions": sessions,
204        }))
205    }
206    fn stats(&self) -> &ToolStats {
207        &self.stats
208    }
209}
210
211/// `citta.coherence` — check coherence threshold.
212pub struct CittaCoherenceTool {
213    stats: ToolStats,
214    effects: EffectRow,
215}
216
217impl CittaCoherenceTool {
218    #[must_use]
219    pub fn new() -> Self {
220        Self {
221            stats: ToolStats::default(),
222            effects: EffectRow::pure(),
223        }
224    }
225}
226
227impl Default for CittaCoherenceTool {
228    fn default() -> Self {
229        Self::new()
230    }
231}
232
233#[async_trait]
234impl Tool for CittaCoherenceTool {
235    fn name(&self) -> &str {
236        "citta.coherence"
237    }
238    fn gana(&self) -> Gana {
239        Gana::Ghost
240    }
241    fn effects(&self) -> &EffectRow {
242        &self.effects
243    }
244    fn description(&self) -> &str {
245        "Check citta coherence level and whether writes are permitted"
246    }
247    async fn call(&self, ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
248        let threshold = 0.3f32;
249        let can_write = ctx.citta_coherence >= threshold;
250        Ok(json!({
251            "status": "success",
252            "coherence": ctx.citta_coherence,
253            "valence": ctx.citta_valence,
254            "write_threshold": threshold,
255            "can_write": can_write,
256        }))
257    }
258    fn stats(&self) -> &ToolStats {
259        &self.stats
260    }
261}
262
263/// `dharma.profiles` — list available dharma profiles.
264pub struct DharmaProfilesTool {
265    stats: ToolStats,
266    effects: EffectRow,
267}
268
269impl DharmaProfilesTool {
270    #[must_use]
271    pub fn new() -> Self {
272        Self {
273            stats: ToolStats::default(),
274            effects: EffectRow::pure(),
275        }
276    }
277}
278
279impl Default for DharmaProfilesTool {
280    fn default() -> Self {
281        Self::new()
282    }
283}
284
285#[async_trait]
286impl Tool for DharmaProfilesTool {
287    fn name(&self) -> &str {
288        "dharma.profiles"
289    }
290    fn gana(&self) -> Gana {
291        Gana::ExtendedNet
292    }
293    fn effects(&self) -> &EffectRow {
294        &self.effects
295    }
296    fn description(&self) -> &str {
297        "List available dharma governance profiles"
298    }
299    async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
300        Ok(json!({
301            "status": "success",
302            "profiles": [
303                { "name": "default", "description": "Standard governance — observe and advise" },
304                { "name": "strict", "description": "Strict governance — intervene on writes in low coherence" },
305                { "name": "research", "description": "Lenient governance — allow experimental tools" },
306                { "name": "production", "description": "Hardened governance — panic on dharma violations" },
307            ],
308        }))
309    }
310    fn stats(&self) -> &ToolStats {
311        &self.stats
312    }
313}
314
315/// `memory.nearby` — find memories spatially near a query using 5D coordinates.
316pub struct MemoryNearbyTool {
317    store: Arc<MemoryStore>,
318    stats: ToolStats,
319    effects: EffectRow,
320}
321
322impl MemoryNearbyTool {
323    pub fn new(store: Arc<MemoryStore>) -> Self {
324        Self {
325            store,
326            stats: ToolStats::default(),
327            effects: EffectRow::read_only(vec![Resource::Galaxy("codex".into())]),
328        }
329    }
330}
331
332#[async_trait]
333impl Tool for MemoryNearbyTool {
334    fn name(&self) -> &str {
335        "memory.nearby"
336    }
337    fn gana(&self) -> Gana {
338        Gana::Star
339    }
340    fn effects(&self) -> &EffectRow {
341        &self.effects
342    }
343    fn description(&self) -> &str {
344        "Find memories spatially near a query text using 5D holographic coordinates"
345    }
346    async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
347        let query = args.get("query").and_then(|v| v.as_str()).unwrap_or("");
348        if query.is_empty() {
349            return Err(wm_core::CoreError::InvalidArgs(
350                "Missing 'query' parameter".into(),
351            ));
352        }
353        let galaxy_name_str = args
354            .get("galaxy")
355            .and_then(|v| v.as_str())
356            .unwrap_or("codex");
357        let galaxy = parse_galaxy(galaxy_name_str)?;
358        let radius = args
359            .get("radius")
360            .and_then(serde_json::Value::as_f64)
361            .unwrap_or(0.5) as f32;
362        let limit = args
363            .get("limit")
364            .and_then(serde_json::Value::as_u64)
365            .unwrap_or(20) as usize;
366
367        let center = wm_core::Coordinate5D::encode(query);
368        let memories = self.store.scan(galaxy, 1000)?;
369
370        let candidates: Vec<(usize, wm_core::Coordinate5D)> = memories
371            .iter()
372            .enumerate()
373            .map(|(i, m)| (i, m.metadata.coord5d.clone()))
374            .collect();
375
376        let nearby = wm_core::find_nearby(&center, &candidates, radius);
377
378        let results: Vec<Value> = nearby
379            .iter()
380            .filter(|(idx, _)| crate::expansion::common::mcp_visible(&memories[*idx]))
381            .filter(|(idx, _)| crate::expansion::common::validity_visible(&memories[*idx]))
382            .take(limit)
383            .map(|(idx, dist)| {
384                let mem = &memories[*idx];
385                json!({
386                    "id": mem.metadata.id,
387                    "content": mem.content.chars().take(100).collect::<String>(),
388                    "distance": dist,
389                    "zone": mem.metadata.coord5d.zone().name(),
390                    "importance": mem.metadata.importance,
391                    "tags": mem.metadata.tags,
392                })
393            })
394            .collect();
395
396        Ok(json!({
397            "status": "success",
398            "query": query,
399            "galaxy": galaxy_name_str,
400            "radius": radius,
401            "center": {
402                "x": center.x,
403                "y": center.y,
404                "z": center.z,
405                "w": center.w,
406                "v": center.v,
407            },
408            "found": results.len(),
409            "scanned": memories.len(),
410            "nearby": results,
411        }))
412    }
413    fn stats(&self) -> &ToolStats {
414        &self.stats
415    }
416}
417
418#[cfg(test)]
419mod tests {
420    use super::*;
421    use wm_memory::Memory;
422
423    fn open_store() -> (tempfile::TempDir, Arc<MemoryStore>) {
424        let tmp = tempfile::tempdir().unwrap();
425        let store = Arc::new(MemoryStore::open_default(tmp.path()).unwrap());
426        (tmp, store)
427    }
428
429    #[tokio::test]
430    async fn memory_tags_returns_unique_sorted_tags() {
431        let (_tmp, store) = open_store();
432        for tags in [["zeta", "alpha"], ["beta", "alpha"]] {
433            let mut memory = Memory::new(Galaxy::Codex, "invented tag fixture".into());
434            memory.metadata.tags = tags.into_iter().map(String::from).collect();
435            store.put(Galaxy::Codex, &memory).unwrap();
436        }
437        let result = MemoryTagsTool::new(store)
438            .call(&mut Context::default(), json!({"galaxy": "codex"}))
439            .await
440            .unwrap();
441        assert_eq!(result["unique_tags"], 3);
442        assert_eq!(result["tags"], json!(["alpha", "beta", "zeta"]));
443    }
444
445    #[tokio::test]
446    async fn citta_coherence_reflects_context_without_claiming_global_state() {
447        let mut context = Context {
448            citta_coherence: 0.29,
449            citta_valence: -0.2,
450            ..Context::default()
451        };
452        let result = CittaCoherenceTool::new()
453            .call(&mut context, json!({}))
454            .await
455            .unwrap();
456        assert!((result["coherence"].as_f64().unwrap() - 0.29).abs() < 1e-6);
457        assert_eq!(result["can_write"], false);
458        assert!((result["write_threshold"].as_f64().unwrap() - 0.3).abs() < 1e-6);
459    }
460
461    #[tokio::test]
462    async fn dharma_profiles_reports_static_descriptive_catalog() {
463        let result = DharmaProfilesTool::new()
464            .call(&mut Context::default(), json!({}))
465            .await
466            .unwrap();
467        assert_eq!(result["profiles"].as_array().unwrap().len(), 4);
468        assert_eq!(result["profiles"][0]["name"], "default");
469    }
470}