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 tag_list: Vec<String> = tags.into_iter().collect();
93        Ok(
94            json!({ "status": "success", "galaxy": galaxy_name(galaxy), "unique_tags": tag_list.len(), "tags": tag_list }),
95        )
96    }
97    fn stats(&self) -> &ToolStats {
98        &self.stats
99    }
100}
101
102/// `session.list` — list all sessions.
103pub struct SessionListTool {
104    store: Arc<MemoryStore>,
105    stats: ToolStats,
106    effects: EffectRow,
107}
108
109impl SessionListTool {
110    pub fn new(store: Arc<MemoryStore>) -> Self {
111        Self {
112            store,
113            stats: ToolStats::default(),
114            effects: EffectRow::read_only(vec![Resource::Galaxy("sessions".into())]),
115        }
116    }
117}
118
119#[async_trait]
120impl Tool for SessionListTool {
121    fn name(&self) -> &str {
122        "session.list"
123    }
124    fn gana(&self) -> Gana {
125        Gana::StraddlingLegs
126    }
127    fn effects(&self) -> &EffectRow {
128        &self.effects
129    }
130    fn description(&self) -> &str {
131        "List session summaries in the Sessions galaxy (turns grouped by session)"
132    }
133    async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
134        let memories = self.store.scan_all(Galaxy::Sessions)?;
135
136        // Group turns and start markers into session summaries.
137        #[derive(Default)]
138        struct Summary {
139            title: Option<String>,
140            turns: u64,
141            earliest: Option<chrono::DateTime<chrono::Utc>>,
142            latest: Option<chrono::DateTime<chrono::Utc>>,
143        }
144        let mut summaries: std::collections::HashMap<String, Summary> =
145            std::collections::HashMap::new();
146
147        for m in &memories {
148            if let Ok(v) = serde_json::from_str::<Value>(&m.content) {
149                // session_start: session id lives in the tag.
150                if v.get("type").and_then(Value::as_str) == Some("session_start") {
151                    if let Some(sid) = m
152                        .metadata
153                        .tags
154                        .iter()
155                        .find_map(|t| t.strip_prefix("session:"))
156                    {
157                        let entry = summaries.entry(sid.to_string()).or_default();
158                        entry.title = v.get("title").and_then(Value::as_str).map(String::from);
159                    }
160                    continue;
161                }
162                // session_turn (and other session memories): session id in content.
163                if let Some(sid) = v.get("session_id").and_then(Value::as_str) {
164                    let entry = summaries.entry(sid.to_string()).or_default();
165                    let ts = m.metadata.created_at;
166                    entry.earliest = Some(entry.earliest.map_or(ts, |e| e.min(ts)));
167                    entry.latest = Some(entry.latest.map_or(ts, |l| l.max(ts)));
168                    if v.get("sequence").and_then(Value::as_u64).is_some() {
169                        entry.turns += 1;
170                    }
171                }
172            }
173        }
174
175        let mut sessions: Vec<(String, Summary)> = summaries.into_iter().collect();
176        // Most recently active sessions first.
177        sessions.sort_by_key(|(_, s)| {
178            std::cmp::Reverse(
179                s.latest
180                    .unwrap_or(chrono::DateTime::<chrono::Utc>::UNIX_EPOCH),
181            )
182        });
183        let total = sessions.len();
184        sessions.truncate(100);
185
186        let sessions: Vec<Value> = sessions
187            .into_iter()
188            .map(|(sid, s)| {
189                json!({
190                    "session_id": sid,
191                    "title": s.title.unwrap_or_else(|| format!("Session {}", &sid[..sid.len().min(8)])),
192                    "turns": s.turns,
193                    "first_activity": s.earliest.map(|t| t.to_rfc3339()),
194                    "last_activity": s.latest.map(|t| t.to_rfc3339()),
195                })
196            })
197            .collect();
198
199        Ok(json!({
200            "status": "success",
201            "count": total,
202            "sessions": sessions,
203        }))
204    }
205    fn stats(&self) -> &ToolStats {
206        &self.stats
207    }
208}
209
210/// `citta.coherence` — check coherence threshold.
211pub struct CittaCoherenceTool {
212    stats: ToolStats,
213    effects: EffectRow,
214}
215
216impl CittaCoherenceTool {
217    #[must_use]
218    pub fn new() -> Self {
219        Self {
220            stats: ToolStats::default(),
221            effects: EffectRow::pure(),
222        }
223    }
224}
225
226impl Default for CittaCoherenceTool {
227    fn default() -> Self {
228        Self::new()
229    }
230}
231
232#[async_trait]
233impl Tool for CittaCoherenceTool {
234    fn name(&self) -> &str {
235        "citta.coherence"
236    }
237    fn gana(&self) -> Gana {
238        Gana::Ghost
239    }
240    fn effects(&self) -> &EffectRow {
241        &self.effects
242    }
243    fn description(&self) -> &str {
244        "Check citta coherence level and whether writes are permitted"
245    }
246    async fn call(&self, ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
247        let threshold = 0.3f32;
248        let can_write = ctx.citta_coherence >= threshold;
249        Ok(json!({
250            "status": "success",
251            "coherence": ctx.citta_coherence,
252            "valence": ctx.citta_valence,
253            "write_threshold": threshold,
254            "can_write": can_write,
255        }))
256    }
257    fn stats(&self) -> &ToolStats {
258        &self.stats
259    }
260}
261
262/// `dharma.profiles` — list available dharma profiles.
263pub struct DharmaProfilesTool {
264    stats: ToolStats,
265    effects: EffectRow,
266}
267
268impl DharmaProfilesTool {
269    #[must_use]
270    pub fn new() -> Self {
271        Self {
272            stats: ToolStats::default(),
273            effects: EffectRow::pure(),
274        }
275    }
276}
277
278impl Default for DharmaProfilesTool {
279    fn default() -> Self {
280        Self::new()
281    }
282}
283
284#[async_trait]
285impl Tool for DharmaProfilesTool {
286    fn name(&self) -> &str {
287        "dharma.profiles"
288    }
289    fn gana(&self) -> Gana {
290        Gana::ExtendedNet
291    }
292    fn effects(&self) -> &EffectRow {
293        &self.effects
294    }
295    fn description(&self) -> &str {
296        "List available dharma governance profiles"
297    }
298    async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
299        Ok(json!({
300            "status": "success",
301            "profiles": [
302                { "name": "default", "description": "Standard governance — observe and advise" },
303                { "name": "strict", "description": "Strict governance — intervene on writes in low coherence" },
304                { "name": "research", "description": "Lenient governance — allow experimental tools" },
305                { "name": "production", "description": "Hardened governance — panic on dharma violations" },
306            ],
307        }))
308    }
309    fn stats(&self) -> &ToolStats {
310        &self.stats
311    }
312}
313
314/// `memory.nearby` — find memories spatially near a query using 5D coordinates.
315pub struct MemoryNearbyTool {
316    store: Arc<MemoryStore>,
317    stats: ToolStats,
318    effects: EffectRow,
319}
320
321impl MemoryNearbyTool {
322    pub fn new(store: Arc<MemoryStore>) -> Self {
323        Self {
324            store,
325            stats: ToolStats::default(),
326            effects: EffectRow::read_only(vec![Resource::Galaxy("codex".into())]),
327        }
328    }
329}
330
331#[async_trait]
332impl Tool for MemoryNearbyTool {
333    fn name(&self) -> &str {
334        "memory.nearby"
335    }
336    fn gana(&self) -> Gana {
337        Gana::Star
338    }
339    fn effects(&self) -> &EffectRow {
340        &self.effects
341    }
342    fn description(&self) -> &str {
343        "Find memories spatially near a query text using 5D holographic coordinates"
344    }
345    async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
346        let query = args.get("query").and_then(|v| v.as_str()).unwrap_or("");
347        if query.is_empty() {
348            return Err(wm_core::CoreError::InvalidArgs(
349                "Missing 'query' parameter".into(),
350            ));
351        }
352        let galaxy_name_str = args
353            .get("galaxy")
354            .and_then(|v| v.as_str())
355            .unwrap_or("codex");
356        let galaxy = parse_galaxy(galaxy_name_str)?;
357        let radius = args
358            .get("radius")
359            .and_then(serde_json::Value::as_f64)
360            .unwrap_or(0.5) as f32;
361        let limit = args
362            .get("limit")
363            .and_then(serde_json::Value::as_u64)
364            .unwrap_or(20) as usize;
365
366        let center = wm_core::Coordinate5D::encode(query);
367        let memories = self.store.scan(galaxy, 1000)?;
368
369        let candidates: Vec<(usize, wm_core::Coordinate5D)> = memories
370            .iter()
371            .enumerate()
372            .map(|(i, m)| (i, m.metadata.coord5d.clone()))
373            .collect();
374
375        let nearby = wm_core::find_nearby(&center, &candidates, radius);
376
377        let results: Vec<Value> = nearby
378            .iter()
379            .filter(|(idx, _)| crate::expansion::common::mcp_visible(&memories[*idx]))
380            .filter(|(idx, _)| crate::expansion::common::validity_visible(&memories[*idx]))
381            .take(limit)
382            .map(|(idx, dist)| {
383                let mem = &memories[*idx];
384                json!({
385                    "id": mem.metadata.id,
386                    "content": mem.content.chars().take(100).collect::<String>(),
387                    "distance": dist,
388                    "zone": mem.metadata.coord5d.zone().name(),
389                    "importance": mem.metadata.importance,
390                    "tags": mem.metadata.tags,
391                })
392            })
393            .collect();
394
395        Ok(json!({
396            "status": "success",
397            "query": query,
398            "galaxy": galaxy_name_str,
399            "radius": radius,
400            "center": {
401                "x": center.x,
402                "y": center.y,
403                "z": center.z,
404                "w": center.w,
405                "v": center.v,
406            },
407            "found": results.len(),
408            "scanned": memories.len(),
409            "nearby": results,
410        }))
411    }
412    fn stats(&self) -> &ToolStats {
413        &self.stats
414    }
415}