1#![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
54pub 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
103pub 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 #[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 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 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 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
211pub 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
263pub 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
315pub 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(¢er, &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}