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 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
102pub 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 #[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 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 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 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
210pub 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
262pub 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
314pub 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(¢er, &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}