Skip to main content

wm_tools/expansion/
v4.rs

1//! v4 integration tools — reflex, workspace, and timescale MCP tools.
2//!
3//! Tools:
4//! - `reflex.dispatch` — dispatch a reflex by ID through the reflex table
5//! - `reflex.status` — show reflex dispatch table status
6//! - `workspace.spotlight` — query current spotlight state
7//! - `workspace.events` — query recent events from the backlog
8//! - `workspace.publish` — publish an event to the global workspace
9//! - `workspace.stats` — workspace statistics
10//! - `timescale.status` — show timescale bus status
11//! - `timescale.hooks` — list hooks per tier with stats
12
13#![forbid(unsafe_code)]
14
15use async_trait::async_trait;
16
17use std::sync::{Arc, Mutex};
18
19use serde_json::{Value, json};
20use wm_cognitive::{ReflexArgs, ReflexDispatchTable, ReflexId};
21use wm_cognitive::{Tier, TimescaleBus};
22use wm_core::{Context, EffectRow, Gana, Tool, ToolStats};
23use wm_workspace::{CoreId, EventType, GlobalWorkspace, Salience};
24
25// ── Helper: parse CoreId from string ─────────────────────────────────
26
27fn parse_core_id(s: &str) -> Result<CoreId, wm_core::CoreError> {
28    match s.to_lowercase().as_str() {
29        "citta" => Ok(CoreId::Citta),
30        "dream" => Ok(CoreId::Dream),
31        "brain_wave" | "brainwave" => Ok(CoreId::BrainWave),
32        "autonomous" => Ok(CoreId::Autonomous),
33        "dispatch" => Ok(CoreId::Dispatch),
34        "reflex" => Ok(CoreId::Reflex),
35        "self_model" | "selfmodel" => Ok(CoreId::SelfModel),
36        "drive" => Ok(CoreId::Drive),
37        "homeostasis" => Ok(CoreId::Homeostasis),
38        "sensor" => Ok(CoreId::Sensor),
39        _ => {
40            if let Some(rest) = s.strip_prefix("custom_") {
41                rest.parse::<u16>().map(CoreId::Custom).map_err(|_| {
42                    wm_core::CoreError::InvalidArgs(format!("invalid custom core ID: {s}"))
43                })
44            } else {
45                Err(wm_core::CoreError::InvalidArgs(format!(
46                    "unknown core ID: {s}"
47                )))
48            }
49        }
50    }
51}
52
53fn parse_event_type(s: &str) -> Result<EventType, wm_core::CoreError> {
54    match s.to_lowercase().as_str() {
55        "error" => Ok(EventType::Error),
56        "reward" => Ok(EventType::Reward),
57        "attention_request" | "attention" => Ok(EventType::AttentionRequest),
58        "novel_detection" | "novel" => Ok(EventType::NovelDetection),
59        "threshold_crossing" | "threshold" => Ok(EventType::ThresholdCrossing),
60        "drive_update" | "drive" => Ok(EventType::DriveUpdate),
61        "safety_alert" | "safety" => Ok(EventType::SafetyAlert),
62        _ => Err(wm_core::CoreError::InvalidArgs(format!(
63            "unknown event type: {s}"
64        ))),
65    }
66}
67
68fn parse_tier(s: &str) -> Result<Tier, wm_core::CoreError> {
69    match s.to_lowercase().as_str() {
70        "reflex" | "0" => Ok(Tier::Reflex),
71        "reactive" | "1" => Ok(Tier::Reactive),
72        "planning" | "2" => Ok(Tier::Planning),
73        "consolidation" | "3" => Ok(Tier::Consolidation),
74        "evolutionary" | "4" => Ok(Tier::Evolutionary),
75        _ => Err(wm_core::CoreError::InvalidArgs(format!(
76            "unknown tier: {s}"
77        ))),
78    }
79}
80
81const fn command_name(cmd: wm_cognitive::ReflexCommand) -> &'static str {
82    match cmd {
83        wm_cognitive::ReflexCommand::EmergencyStop => "emergency_stop",
84        wm_cognitive::ReflexCommand::ReducePower => "reduce_power",
85        wm_cognitive::ReflexCommand::ApplyCorrection => "apply_correction",
86        wm_cognitive::ReflexCommand::IssueAlert => "issue_alert",
87        wm_cognitive::ReflexCommand::Drop => "drop",
88        wm_cognitive::ReflexCommand::NoOp => "noop",
89        wm_cognitive::ReflexCommand::Custom => "custom",
90    }
91}
92
93// ── Tool: reflex.dispatch ─────────────────────────────────────────────
94
95pub struct ReflexDispatchTool {
96    table: Arc<Mutex<ReflexDispatchTable>>,
97    stats: ToolStats,
98    effects: EffectRow,
99}
100
101impl ReflexDispatchTool {
102    pub fn new(table: Arc<Mutex<ReflexDispatchTable>>) -> Self {
103        Self {
104            table,
105            stats: ToolStats::default(),
106            effects: EffectRow::pure(),
107        }
108    }
109}
110
111#[async_trait]
112impl Tool for ReflexDispatchTool {
113    fn name(&self) -> &str {
114        "reflex.dispatch"
115    }
116    fn gana(&self) -> Gana {
117        Gana::Heart
118    }
119    fn effects(&self) -> &EffectRow {
120        &self.effects
121    }
122    async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
123        let reflex_id: ReflexId = args
124            .get("reflex_id")
125            .and_then(serde_json::Value::as_u64)
126            .ok_or_else(|| {
127                wm_core::CoreError::InvalidArgs("reflex_id (number 0-255) required".into())
128            })? as ReflexId;
129
130        let sensor_id = args
131            .get("sensor_id")
132            .and_then(serde_json::Value::as_u64)
133            .unwrap_or(0) as u16;
134
135        let timestamp_ns = args
136            .get("timestamp_ns")
137            .and_then(serde_json::Value::as_u64)
138            .unwrap_or(0);
139
140        let payload_hex = args
141            .get("payload")
142            .and_then(serde_json::Value::as_str)
143            .unwrap_or("");
144
145        let mut reflex_args = ReflexArgs::new(sensor_id, timestamp_ns);
146        if !payload_hex.is_empty() {
147            let payload = hex_decode(payload_hex)?;
148            reflex_args.set_payload(&payload).map_err(|e| {
149                wm_core::CoreError::InvalidArgs(format!("reflex payload error: {e}"))
150            })?;
151        }
152
153        let mut table = self
154            .table
155            .lock()
156            .map_err(|e| wm_core::CoreError::Governance(format!("reflex table lock error: {e}")))?;
157        let result = table
158            .dispatch(reflex_id, &reflex_args)
159            .map_err(|e| wm_core::CoreError::Tool(format!("reflex dispatch error: {e}")))?;
160
161        Ok(json!({
162            "reflex_id": reflex_id,
163            "actuator_id": result.actuator_id,
164            "command": command_name(result.command),
165            "priority": result.priority,
166            "payload": hex_encode(result.payload()),
167            "dispatch_count": table.dispatch_count(),
168        }))
169    }
170    fn stats(&self) -> &ToolStats {
171        &self.stats
172    }
173}
174
175// ── Tool: reflex.status ───────────────────────────────────────────────
176
177pub struct ReflexStatusTool {
178    table: Arc<Mutex<ReflexDispatchTable>>,
179    stats: ToolStats,
180    effects: EffectRow,
181}
182
183impl ReflexStatusTool {
184    pub fn new(table: Arc<Mutex<ReflexDispatchTable>>) -> Self {
185        Self {
186            table,
187            stats: ToolStats::default(),
188            effects: EffectRow::pure(),
189        }
190    }
191}
192
193#[async_trait]
194impl Tool for ReflexStatusTool {
195    fn name(&self) -> &str {
196        "reflex.status"
197    }
198    fn gana(&self) -> Gana {
199        Gana::Heart
200    }
201    fn effects(&self) -> &EffectRow {
202        &self.effects
203    }
204    async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
205        let table = self
206            .table
207            .lock()
208            .map_err(|e| wm_core::CoreError::Governance(format!("reflex table lock error: {e}")))?;
209        let registered_handlers = table.registered_count();
210        let safety_mask = format!("{:#010x}", table.safety_mask());
211        let dispatch_count = table.dispatch_count();
212        let builtins: Vec<Value> = wm_cognitive::reflex::builtins::BUILTINS
213            .iter()
214            .map(|b| {
215                json!({
216                    "id": b.id,
217                    "name": b.name,
218                    "registered": table.is_registered(b.id),
219                })
220            })
221            .collect();
222        drop(table);
223        Ok(json!({
224            "registered_handlers": registered_handlers,
225            "safety_mask": safety_mask,
226            "dispatch_count": dispatch_count,
227            "builtins": builtins,
228        }))
229    }
230    fn stats(&self) -> &ToolStats {
231        &self.stats
232    }
233}
234
235// ── Tool: workspace.spotlight ─────────────────────────────────────────
236
237pub struct WorkspaceSpotlightTool {
238    workspace: Arc<Mutex<GlobalWorkspace>>,
239    stats: ToolStats,
240    effects: EffectRow,
241}
242
243impl WorkspaceSpotlightTool {
244    pub fn new(workspace: Arc<Mutex<GlobalWorkspace>>) -> Self {
245        Self {
246            workspace,
247            stats: ToolStats::default(),
248            effects: EffectRow::pure(),
249        }
250    }
251}
252
253#[async_trait]
254impl Tool for WorkspaceSpotlightTool {
255    fn name(&self) -> &str {
256        "workspace.spotlight"
257    }
258    fn gana(&self) -> Gana {
259        Gana::Ghost
260    }
261    fn effects(&self) -> &EffectRow {
262        &self.effects
263    }
264    async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
265        let ws = self
266            .workspace
267            .lock()
268            .map_err(|e| wm_core::CoreError::Governance(format!("workspace lock error: {e}")))?;
269        match ws.spotlight() {
270            Some(entry) => Ok(json!({
271                "core": entry.core.to_string(),
272                "event_type": entry.winning_event_type.to_string(),
273                "salience": {
274                    "urgency": entry.salience.urgency,
275                    "novelty": entry.salience.novelty,
276                    "confidence": entry.salience.confidence,
277                    "composite": entry.salience.composite(),
278                },
279                "strength": ws.spotlight_strength(),
280                "age_ms": entry.age().as_millis(),
281                "candidates": entry.candidates,
282                "transfers": ws.spotlight_transfers(),
283                "arbitration_cycles": ws.arbitration_cycles(),
284            })),
285            None => Ok(json!({
286                "spotlight": null,
287                "transfers": ws.spotlight_transfers(),
288                "arbitration_cycles": ws.arbitration_cycles(),
289            })),
290        }
291    }
292    fn stats(&self) -> &ToolStats {
293        &self.stats
294    }
295}
296
297// ── Tool: workspace.events ────────────────────────────────────────────
298
299pub struct WorkspaceEventsTool {
300    workspace: Arc<Mutex<GlobalWorkspace>>,
301    stats: ToolStats,
302    effects: EffectRow,
303}
304
305impl WorkspaceEventsTool {
306    pub fn new(workspace: Arc<Mutex<GlobalWorkspace>>) -> Self {
307        Self {
308            workspace,
309            stats: ToolStats::default(),
310            effects: EffectRow::pure(),
311        }
312    }
313}
314
315#[async_trait]
316impl Tool for WorkspaceEventsTool {
317    fn name(&self) -> &str {
318        "workspace.events"
319    }
320    fn gana(&self) -> Gana {
321        Gana::Ghost
322    }
323    fn effects(&self) -> &EffectRow {
324        &self.effects
325    }
326    async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
327        let count = args
328            .get("count")
329            .and_then(serde_json::Value::as_u64)
330            .unwrap_or(10) as usize;
331        let count = count.min(256);
332
333        let ws = self
334            .workspace
335            .lock()
336            .map_err(|e| wm_core::CoreError::Governance(format!("workspace lock error: {e}")))?;
337        let events: Vec<Value> = ws
338            .recent_events(count)
339            .iter()
340            .map(|e| {
341                json!({
342                    "core": e.core.to_string(),
343                    "event_type": e.event_type.to_string(),
344                    "salience": {
345                        "urgency": e.salience.urgency,
346                        "novelty": e.salience.novelty,
347                        "confidence": e.salience.confidence,
348                        "composite": e.composite_salience(),
349                    },
350                    "payload": e.payload,
351                    "age_ms": e.age().as_millis(),
352                })
353            })
354            .collect();
355
356        Ok(json!({
357            "events": events,
358            "total_published": ws.events_published(),
359            "backlog_len": ws.backlog().len(),
360        }))
361    }
362    fn stats(&self) -> &ToolStats {
363        &self.stats
364    }
365}
366
367// ── Tool: workspace.publish ───────────────────────────────────────────
368
369pub struct WorkspacePublishTool {
370    workspace: Arc<Mutex<GlobalWorkspace>>,
371    stats: ToolStats,
372    effects: EffectRow,
373}
374
375impl WorkspacePublishTool {
376    pub fn new(workspace: Arc<Mutex<GlobalWorkspace>>) -> Self {
377        Self {
378            workspace,
379            stats: ToolStats::default(),
380            effects: EffectRow::pure(),
381        }
382    }
383}
384
385#[async_trait]
386impl Tool for WorkspacePublishTool {
387    fn name(&self) -> &str {
388        "workspace.publish"
389    }
390    fn gana(&self) -> Gana {
391        Gana::Ghost
392    }
393    fn effects(&self) -> &EffectRow {
394        &self.effects
395    }
396    async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
397        let core_str = args
398            .get("core")
399            .and_then(serde_json::Value::as_str)
400            .ok_or_else(|| wm_core::CoreError::InvalidArgs("core (string) required".into()))?;
401        let core = parse_core_id(core_str)?;
402
403        let event_type_str = args
404            .get("event_type")
405            .and_then(serde_json::Value::as_str)
406            .ok_or_else(|| {
407                wm_core::CoreError::InvalidArgs("event_type (string) required".into())
408            })?;
409        let event_type = parse_event_type(event_type_str)?;
410
411        let urgency = args
412            .get("urgency")
413            .and_then(serde_json::Value::as_f64)
414            .unwrap_or(0.5) as f32;
415        let novelty = args
416            .get("novelty")
417            .and_then(serde_json::Value::as_f64)
418            .unwrap_or(0.5) as f32;
419        let confidence = args
420            .get("confidence")
421            .and_then(serde_json::Value::as_f64)
422            .unwrap_or(0.5) as f32;
423        let payload = args.get("payload").cloned().unwrap_or_else(|| json!({}));
424
425        let event = wm_workspace::WorkspaceEvent::new(
426            core,
427            event_type,
428            Salience::new(urgency, novelty, confidence),
429            payload,
430        );
431
432        let mut ws = self
433            .workspace
434            .lock()
435            .map_err(|e| wm_core::CoreError::Governance(format!("workspace lock error: {e}")))?;
436        let won = ws.publish(&event);
437
438        Ok(json!({
439            "won_spotlight": won,
440            "spotlight_core": ws.spotlight_core().map(|c| c.to_string()),
441            "spotlight_strength": ws.spotlight_strength(),
442            "events_published": ws.events_published(),
443        }))
444    }
445    fn stats(&self) -> &ToolStats {
446        &self.stats
447    }
448}
449
450// ── Tool: workspace.stats ─────────────────────────────────────────────
451
452pub struct WorkspaceStatsTool {
453    workspace: Arc<Mutex<GlobalWorkspace>>,
454    stats: ToolStats,
455    effects: EffectRow,
456}
457
458impl WorkspaceStatsTool {
459    pub fn new(workspace: Arc<Mutex<GlobalWorkspace>>) -> Self {
460        Self {
461            workspace,
462            stats: ToolStats::default(),
463            effects: EffectRow::pure(),
464        }
465    }
466}
467
468#[async_trait]
469impl Tool for WorkspaceStatsTool {
470    fn name(&self) -> &str {
471        "workspace.stats"
472    }
473    fn gana(&self) -> Gana {
474        Gana::Ghost
475    }
476    fn effects(&self) -> &EffectRow {
477        &self.effects
478    }
479    async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
480        let ws = self
481            .workspace
482            .lock()
483            .map_err(|e| wm_core::CoreError::Governance(format!("workspace lock error: {e}")))?;
484        let stats = ws.stats();
485        let events_published = stats.events_published;
486        let spotlight_transfers = stats.spotlight_transfers;
487        let arbitration_cycles = stats.arbitration_cycles;
488        let events_per_core: serde_json::Map<_, _> = stats
489            .events_per_core
490            .iter()
491            .map(|(c, n)| (c.to_string(), json!(n)))
492            .collect();
493        let events_per_type: serde_json::Map<_, _> = stats
494            .events_per_type
495            .iter()
496            .map(|(t, n)| (t.to_string(), json!(n)))
497            .collect();
498        drop(ws);
499        Ok(json!({
500            "events_published": events_published,
501            "spotlight_transfers": spotlight_transfers,
502            "arbitration_cycles": arbitration_cycles,
503            "events_per_core": events_per_core,
504            "events_per_type": events_per_type,
505        }))
506    }
507    fn stats(&self) -> &ToolStats {
508        &self.stats
509    }
510}
511
512// ── Tool: timescale.status ────────────────────────────────────────────
513
514pub struct TimescaleStatusTool {
515    bus: Arc<Mutex<TimescaleBus>>,
516    stats: ToolStats,
517    effects: EffectRow,
518}
519
520impl TimescaleStatusTool {
521    pub fn new(bus: Arc<Mutex<TimescaleBus>>) -> Self {
522        Self {
523            bus,
524            stats: ToolStats::default(),
525            effects: EffectRow::pure(),
526        }
527    }
528}
529
530#[async_trait]
531impl Tool for TimescaleStatusTool {
532    fn name(&self) -> &str {
533        "timescale.status"
534    }
535    fn gana(&self) -> Gana {
536        Gana::Dipper
537    }
538    fn effects(&self) -> &EffectRow {
539        &self.effects
540    }
541    async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
542        let bus = self.bus.lock().map_err(|e| {
543            wm_core::CoreError::Governance(format!("timescale bus lock error: {e}"))
544        })?;
545        let tiers: Vec<Value> = Tier::all()
546            .iter()
547            .map(|t| {
548                let config = bus.tier_config(*t);
549                json!({
550                    "name": t.name(),
551                    "index": t.index(),
552                    "active": bus.is_tier_active(*t),
553                    "hook_count": bus.hook_count(*t),
554                    "interval_ms": config.interval.as_millis(),
555                    "budget_ms": config.budget.as_millis(),
556                })
557            })
558            .collect();
559        let brain_wave = format!("{:?}", bus.brain_wave());
560        let total_hooks = bus.total_hook_count();
561        let total_ticks = bus.total_ticks();
562        let total_timeouts = bus.total_timeouts();
563        let active_tiers: Vec<_> = bus.active_tiers().iter().map(|t| t.name()).collect();
564        let inactive_tiers: Vec<_> = bus.inactive_tiers().iter().map(|t| t.name()).collect();
565        drop(bus);
566        Ok(json!({
567            "brain_wave": brain_wave,
568            "total_hooks": total_hooks,
569            "total_ticks": total_ticks,
570            "total_timeouts": total_timeouts,
571            "active_tiers": active_tiers,
572            "inactive_tiers": inactive_tiers,
573            "tiers": tiers,
574        }))
575    }
576    fn stats(&self) -> &ToolStats {
577        &self.stats
578    }
579}
580
581// ── Tool: timescale.hooks ─────────────────────────────────────────────
582
583pub struct TimescaleHooksTool {
584    bus: Arc<Mutex<TimescaleBus>>,
585    stats: ToolStats,
586    effects: EffectRow,
587}
588
589impl TimescaleHooksTool {
590    pub fn new(bus: Arc<Mutex<TimescaleBus>>) -> Self {
591        Self {
592            bus,
593            stats: ToolStats::default(),
594            effects: EffectRow::pure(),
595        }
596    }
597}
598
599#[async_trait]
600impl Tool for TimescaleHooksTool {
601    fn name(&self) -> &str {
602        "timescale.hooks"
603    }
604    fn gana(&self) -> Gana {
605        Gana::Dipper
606    }
607    fn effects(&self) -> &EffectRow {
608        &self.effects
609    }
610    async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
611        let tier_str = args
612            .get("tier")
613            .and_then(serde_json::Value::as_str)
614            .unwrap_or("reflex");
615        let tier = parse_tier(tier_str)?;
616
617        let bus = self.bus.lock().map_err(|e| {
618            wm_core::CoreError::Governance(format!("timescale bus lock error: {e}"))
619        })?;
620        let hooks: Vec<Value> = bus
621            .tier_stats(tier)
622            .iter()
623            .map(|(id, name, snap)| {
624                json!({
625                    "id": id,
626                    "name": name,
627                    "tick_count": snap.tick_count,
628                    "success_count": snap.success_count,
629                    "timeout_count": snap.timeout_count,
630                    "error_count": snap.error_count,
631                    "last_duration_us": snap.last_duration_us,
632                    "avg_duration_us": snap.avg_duration_us,
633                })
634            })
635            .collect();
636        let active = bus.is_tier_active(tier);
637        let hook_count = bus.hook_count(tier);
638        drop(bus);
639        Ok(json!({
640            "tier": tier.name(),
641            "active": active,
642            "hook_count": hook_count,
643            "hooks": hooks,
644        }))
645    }
646    fn stats(&self) -> &ToolStats {
647        &self.stats
648    }
649}
650
651// ── Hex helpers ───────────────────────────────────────────────────────
652
653fn hex_encode(data: &[u8]) -> String {
654    use std::fmt::Write;
655    let mut s = String::with_capacity(data.len() * 2);
656    for b in data {
657        let _ = write!(s, "{b:02x}");
658    }
659    s
660}
661
662fn hex_decode(s: &str) -> Result<Vec<u8>, wm_core::CoreError> {
663    if s.len() % 2 != 0 {
664        return Err(wm_core::CoreError::InvalidArgs(
665            "payload hex must have even length".into(),
666        ));
667    }
668    (0..s.len())
669        .step_by(2)
670        .map(|i| {
671            u8::from_str_radix(&s[i..i + 2], 16)
672                .map_err(|e| wm_core::CoreError::InvalidArgs(format!("invalid hex: {e}")))
673        })
674        .collect()
675}
676
677// ── Registration ──────────────────────────────────────────────────────
678
679/// Register all v4 integration tools into a registry.
680pub fn register_v4(
681    registry: &wm_dispatch::ToolRegistry,
682    reflex_table: Arc<Mutex<ReflexDispatchTable>>,
683    timescale_bus: Arc<Mutex<TimescaleBus>>,
684    workspace: Arc<Mutex<GlobalWorkspace>>,
685) -> wm_dispatch::ToolRegistry {
686    registry
687        .register(Arc::new(ReflexDispatchTool::new(Arc::clone(&reflex_table))))
688        .register(Arc::new(ReflexStatusTool::new(reflex_table)))
689        .register(Arc::new(WorkspaceSpotlightTool::new(Arc::clone(
690            &workspace,
691        ))))
692        .register(Arc::new(WorkspaceEventsTool::new(Arc::clone(&workspace))))
693        .register(Arc::new(WorkspacePublishTool::new(Arc::clone(&workspace))))
694        .register(Arc::new(WorkspaceStatsTool::new(workspace)))
695        .register(Arc::new(TimescaleStatusTool::new(Arc::clone(
696            &timescale_bus,
697        ))))
698        .register(Arc::new(TimescaleHooksTool::new(timescale_bus)))
699}
700
701// ── Tests ─────────────────────────────────────────────────────────────
702
703#[cfg(test)]
704mod tests {
705    use super::*;
706    use wm_cognitive::reflex::{ReflexDispatchTable, builtins};
707    use wm_workspace::GlobalWorkspace;
708
709    fn test_reflex_table() -> Arc<Mutex<ReflexDispatchTable>> {
710        let mut table = ReflexDispatchTable::permissive();
711        builtins::register_builtins(&mut table);
712        Arc::new(Mutex::new(table))
713    }
714
715    fn test_workspace() -> Arc<Mutex<GlobalWorkspace>> {
716        Arc::new(Mutex::new(GlobalWorkspace::new()))
717    }
718
719    fn test_timescale_bus() -> Arc<Mutex<TimescaleBus>> {
720        Arc::new(Mutex::new(TimescaleBus::default()))
721    }
722
723    #[tokio::test]
724    async fn reflex_dispatch_e_stop() {
725        let table = test_reflex_table();
726        let tool = ReflexDispatchTool::new(Arc::clone(&table));
727        let mut ctx = Context::new(wm_core::BrainWave::Gamma);
728        let result = tool.call(&mut ctx, json!({"reflex_id": 0})).await.unwrap();
729        assert_eq!(result["command"], "emergency_stop");
730        assert_eq!(result["priority"], 255);
731    }
732
733    #[tokio::test]
734    async fn reflex_dispatch_with_payload() {
735        let table = test_reflex_table();
736        let tool = ReflexDispatchTool::new(Arc::clone(&table));
737        let mut ctx = Context::new(wm_core::BrainWave::Gamma);
738        let result = tool
739            .call(&mut ctx, json!({"reflex_id": 0, "payload": "deadbeef"}))
740            .await
741            .unwrap();
742        assert_eq!(result["command"], "emergency_stop");
743    }
744
745    #[tokio::test]
746    async fn reflex_dispatch_not_registered() {
747        let table = test_reflex_table();
748        let tool = ReflexDispatchTool::new(Arc::clone(&table));
749        let mut ctx = Context::new(wm_core::BrainWave::Gamma);
750        let err = tool
751            .call(&mut ctx, json!({"reflex_id": 200}))
752            .await
753            .unwrap_err();
754        assert!(err.to_string().contains("no handler registered"));
755    }
756
757    #[tokio::test]
758    async fn reflex_dispatch_missing_id() {
759        let table = test_reflex_table();
760        let tool = ReflexDispatchTool::new(Arc::clone(&table));
761        let mut ctx = Context::new(wm_core::BrainWave::Gamma);
762        let err = tool.call(&mut ctx, json!({})).await.unwrap_err();
763        assert!(err.to_string().contains("reflex_id"));
764    }
765
766    #[tokio::test]
767    async fn reflex_status_shows_builtins() {
768        let table = test_reflex_table();
769        let tool = ReflexStatusTool::new(Arc::clone(&table));
770        let mut ctx = Context::new(wm_core::BrainWave::Gamma);
771        let result = tool.call(&mut ctx, json!({})).await.unwrap();
772        assert_eq!(result["registered_handlers"], 8);
773        assert_eq!(result["dispatch_count"].as_u64().unwrap(), 0);
774    }
775
776    #[tokio::test]
777    async fn workspace_spotlight_empty() {
778        let ws = test_workspace();
779        let tool = WorkspaceSpotlightTool::new(Arc::clone(&ws));
780        let mut ctx = Context::new(wm_core::BrainWave::Gamma);
781        let result = tool.call(&mut ctx, json!({})).await.unwrap();
782        assert!(result["spotlight"].is_null());
783    }
784
785    #[tokio::test]
786    async fn workspace_spotlight_after_publish() {
787        let ws = test_workspace();
788        let pub_tool = WorkspacePublishTool::new(Arc::clone(&ws));
789        let spot_tool = WorkspaceSpotlightTool::new(Arc::clone(&ws));
790        let mut ctx = Context::new(wm_core::BrainWave::Gamma);
791
792        pub_tool
793            .call(
794                &mut ctx,
795                json!({
796                    "core": "reflex",
797                    "event_type": "safety_alert",
798                    "urgency": 0.9,
799                    "novelty": 0.8,
800                    "confidence": 0.95,
801                }),
802            )
803            .await
804            .unwrap();
805
806        let result = spot_tool.call(&mut ctx, json!({})).await.unwrap();
807        assert_eq!(result["core"], "reflex");
808        assert_eq!(result["event_type"], "safety_alert");
809    }
810
811    #[tokio::test]
812    async fn workspace_publish_wins_spotlight() {
813        let ws = test_workspace();
814        let tool = WorkspacePublishTool::new(Arc::clone(&ws));
815        let mut ctx = Context::new(wm_core::BrainWave::Gamma);
816        let result = tool
817            .call(
818                &mut ctx,
819                json!({
820                    "core": "citta",
821                    "event_type": "attention_request",
822                    "urgency": 0.7,
823                    "novelty": 0.6,
824                    "confidence": 0.8,
825                }),
826            )
827            .await
828            .unwrap();
829        assert_eq!(result["won_spotlight"], true);
830        assert_eq!(result["spotlight_core"], "citta");
831    }
832
833    #[tokio::test]
834    async fn workspace_publish_missing_core() {
835        let ws = test_workspace();
836        let tool = WorkspacePublishTool::new(Arc::clone(&ws));
837        let mut ctx = Context::new(wm_core::BrainWave::Gamma);
838        let err = tool
839            .call(&mut ctx, json!({"event_type": "error"}))
840            .await
841            .unwrap_err();
842        assert!(err.to_string().contains("core"));
843    }
844
845    #[tokio::test]
846    async fn workspace_publish_invalid_core() {
847        let ws = test_workspace();
848        let tool = WorkspacePublishTool::new(Arc::clone(&ws));
849        let mut ctx = Context::new(wm_core::BrainWave::Gamma);
850        let err = tool
851            .call(
852                &mut ctx,
853                json!({"core": "nonexistent", "event_type": "error"}),
854            )
855            .await
856            .unwrap_err();
857        assert!(err.to_string().contains("unknown core"));
858    }
859
860    #[tokio::test]
861    async fn workspace_events_after_publish() {
862        let ws = test_workspace();
863        let pub_tool = WorkspacePublishTool::new(Arc::clone(&ws));
864        let events_tool = WorkspaceEventsTool::new(Arc::clone(&ws));
865        let mut ctx = Context::new(wm_core::BrainWave::Gamma);
866
867        for i in 0..3 {
868            pub_tool
869                .call(
870                    &mut ctx,
871                    json!({
872                        "core": "citta",
873                        "event_type": "attention_request",
874                        "urgency": f64::from(i).mul_add(0.1, 0.5),
875                        "novelty": 0.5,
876                        "confidence": 0.5,
877                    }),
878                )
879                .await
880                .unwrap();
881        }
882
883        let result = events_tool
884            .call(&mut ctx, json!({"count": 10}))
885            .await
886            .unwrap();
887        assert_eq!(result["total_published"], 3);
888        assert_eq!(result["events"].as_array().unwrap().len(), 3);
889    }
890
891    #[tokio::test]
892    async fn workspace_stats_shows_counts() {
893        let ws = test_workspace();
894        let pub_tool = WorkspacePublishTool::new(Arc::clone(&ws));
895        let stats_tool = WorkspaceStatsTool::new(Arc::clone(&ws));
896        let mut ctx = Context::new(wm_core::BrainWave::Gamma);
897
898        pub_tool
899            .call(
900                &mut ctx,
901                json!({
902                    "core": "citta",
903                    "event_type": "attention_request",
904                    "urgency": 0.7,
905                    "novelty": 0.5,
906                    "confidence": 0.8,
907                }),
908            )
909            .await
910            .unwrap();
911
912        let result = stats_tool.call(&mut ctx, json!({})).await.unwrap();
913        assert_eq!(result["events_published"], 1);
914        assert_eq!(result["spotlight_transfers"], 1);
915    }
916
917    #[tokio::test]
918    async fn timescale_status_default() {
919        let bus = test_timescale_bus();
920        let tool = TimescaleStatusTool::new(Arc::clone(&bus));
921        let mut ctx = Context::new(wm_core::BrainWave::Gamma);
922        let result = tool.call(&mut ctx, json!({})).await.unwrap();
923        assert_eq!(result["brain_wave"], "Gamma");
924        assert_eq!(result["total_hooks"], 0);
925        let tiers = result["tiers"].as_array().unwrap();
926        assert_eq!(tiers.len(), 5);
927    }
928
929    #[tokio::test]
930    async fn timescale_hooks_empty_tier() {
931        let bus = test_timescale_bus();
932        let tool = TimescaleHooksTool::new(Arc::clone(&bus));
933        let mut ctx = Context::new(wm_core::BrainWave::Gamma);
934        let result = tool
935            .call(&mut ctx, json!({"tier": "reflex"}))
936            .await
937            .unwrap();
938        assert_eq!(result["tier"], "Reflex");
939        assert_eq!(result["hook_count"], 0);
940    }
941
942    #[tokio::test]
943    async fn timescale_hooks_invalid_tier() {
944        let bus = test_timescale_bus();
945        let tool = TimescaleHooksTool::new(Arc::clone(&bus));
946        let mut ctx = Context::new(wm_core::BrainWave::Gamma);
947        let err = tool
948            .call(&mut ctx, json!({"tier": "nonexistent"}))
949            .await
950            .unwrap_err();
951        assert!(err.to_string().contains("unknown tier"));
952    }
953
954    #[tokio::test]
955    async fn parse_core_id_all_builtins() {
956        assert_eq!(parse_core_id("citta").unwrap(), CoreId::Citta);
957        assert_eq!(parse_core_id("CITTA").unwrap(), CoreId::Citta);
958        assert_eq!(parse_core_id("dream").unwrap(), CoreId::Dream);
959        assert_eq!(parse_core_id("reflex").unwrap(), CoreId::Reflex);
960        assert_eq!(parse_core_id("brain_wave").unwrap(), CoreId::BrainWave);
961        assert_eq!(parse_core_id("brainwave").unwrap(), CoreId::BrainWave);
962        assert_eq!(parse_core_id("self_model").unwrap(), CoreId::SelfModel);
963        assert_eq!(parse_core_id("custom_42").unwrap(), CoreId::Custom(42));
964    }
965
966    #[tokio::test]
967    async fn parse_event_type_all_types() {
968        assert_eq!(parse_event_type("error").unwrap(), EventType::Error);
969        assert_eq!(parse_event_type("reward").unwrap(), EventType::Reward);
970        assert_eq!(
971            parse_event_type("attention_request").unwrap(),
972            EventType::AttentionRequest
973        );
974        assert_eq!(
975            parse_event_type("attention").unwrap(),
976            EventType::AttentionRequest
977        );
978        assert_eq!(
979            parse_event_type("novel_detection").unwrap(),
980            EventType::NovelDetection
981        );
982        assert_eq!(
983            parse_event_type("safety_alert").unwrap(),
984            EventType::SafetyAlert
985        );
986    }
987
988    #[tokio::test]
989    async fn parse_tier_all_tiers() {
990        assert_eq!(parse_tier("reflex").unwrap(), Tier::Reflex);
991        assert_eq!(parse_tier("reactive").unwrap(), Tier::Reactive);
992        assert_eq!(parse_tier("planning").unwrap(), Tier::Planning);
993        assert_eq!(parse_tier("consolidation").unwrap(), Tier::Consolidation);
994        assert_eq!(parse_tier("evolutionary").unwrap(), Tier::Evolutionary);
995        assert_eq!(parse_tier("0").unwrap(), Tier::Reflex);
996        assert_eq!(parse_tier("4").unwrap(), Tier::Evolutionary);
997    }
998
999    #[tokio::test]
1000    async fn hex_roundtrip() {
1001        let data = vec![0xde, 0xad, 0xbe, 0xef];
1002        let encoded = hex_encode(&data);
1003        assert_eq!(encoded, "deadbeef");
1004        let decoded = hex_decode(&encoded).unwrap();
1005        assert_eq!(decoded, data);
1006    }
1007
1008    #[tokio::test]
1009    async fn hex_decode_odd_length_fails() {
1010        assert!(hex_decode("abc").is_err());
1011    }
1012
1013    #[tokio::test]
1014    async fn register_v4_creates_8_tools() {
1015        let table = test_reflex_table();
1016        let bus = test_timescale_bus();
1017        let ws = test_workspace();
1018        let registry = wm_dispatch::ToolRegistry::new();
1019        let reg = register_v4(&registry, table, bus, ws);
1020        assert_eq!(reg.len(), 8);
1021    }
1022}