#![forbid(unsafe_code)]
use async_trait::async_trait;
use std::sync::Arc;
use std::sync::Mutex;
use serde_json::{Value, json};
use wm_cognitive::{EventType, GanYingBus};
use wm_core::{Context, EffectRow, Gana, Resource, Tool, ToolStats};
pub struct BusStatsTool {
bus: Arc<Mutex<GanYingBus>>,
stats: ToolStats,
effects: EffectRow,
}
impl BusStatsTool {
pub fn new(bus: Arc<Mutex<GanYingBus>>) -> Self {
Self {
bus,
stats: ToolStats::default(),
effects: EffectRow::read_only(vec![Resource::Galaxy("resonance".into())]),
}
}
}
#[async_trait]
impl Tool for BusStatsTool {
fn name(&self) -> &str {
"bus.stats"
}
fn gana(&self) -> Gana {
Gana::Heart
}
fn effects(&self) -> &EffectRow {
&self.effects
}
fn description(&self) -> &str {
"Gan Ying Bus statistics (events emitted, cascades, subscriber triggers)"
}
async fn call(&self, _ctx: &mut Context, _args: Value) -> wm_core::Result<Value> {
let bus = self
.bus
.lock()
.map_err(|e| wm_core::CoreError::Tool(format!("resonance bus lock: {e}")))?;
let stats = bus.stats();
Ok(json!({
"status": "success",
"events_emitted": stats.events_emitted,
"cascade_events": stats.cascade_events,
"subscriber_triggers": stats.subscriber_triggers,
"active_subscriptions": stats.active_subscriptions,
"events_per_category": stats.events_per_category,
}))
}
fn stats(&self) -> &ToolStats {
&self.stats
}
}
pub struct BusEmitTool {
bus: Arc<Mutex<GanYingBus>>,
stats: ToolStats,
effects: EffectRow,
}
impl BusEmitTool {
pub fn new(bus: Arc<Mutex<GanYingBus>>) -> Self {
Self {
bus,
stats: ToolStats::default(),
effects: EffectRow {
writes: vec![Resource::EventBus, Resource::Filesystem],
..Default::default()
},
}
}
}
#[async_trait]
impl Tool for BusEmitTool {
fn name(&self) -> &str {
"bus.emit"
}
fn gana(&self) -> Gana {
Gana::Heart
}
fn effects(&self) -> &EffectRow {
&self.effects
}
fn description(&self) -> &str {
"Emit a custom event to the Gan Ying Bus (args: event_type, source, data)"
}
async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
let event_type_str = args
.get("event_type")
.and_then(Value::as_str)
.ok_or_else(|| wm_core::CoreError::InvalidArgs("event_type required".into()))?;
let source = args.get("source").and_then(Value::as_str).unwrap_or("user");
let data = args.get("data").cloned().unwrap_or_else(|| json!({}));
let event_type = parse_event_type(event_type_str).ok_or_else(|| {
wm_core::CoreError::InvalidArgs(format!("unknown event type: {event_type_str}"))
})?;
let mut bus = self
.bus
.lock()
.map_err(|e| wm_core::CoreError::Tool(format!("resonance bus lock: {e}")))?;
bus.emit(event_type, source, data);
Ok(json!({
"status": "success",
"event_type": event_type_str,
"source": source,
}))
}
fn stats(&self) -> &ToolStats {
&self.stats
}
}
pub struct BusRecentTool {
bus: Arc<Mutex<GanYingBus>>,
stats: ToolStats,
effects: EffectRow,
}
impl BusRecentTool {
pub fn new(bus: Arc<Mutex<GanYingBus>>) -> Self {
Self {
bus,
stats: ToolStats::default(),
effects: EffectRow::read_only(vec![Resource::Galaxy("resonance".into())]),
}
}
}
#[async_trait]
impl Tool for BusRecentTool {
fn name(&self) -> &str {
"bus.recent"
}
fn gana(&self) -> Gana {
Gana::Heart
}
fn effects(&self) -> &EffectRow {
&self.effects
}
fn description(&self) -> &str {
"Get recent events from the Gan Ying Bus (optionally filter by category)"
}
async fn call(&self, _ctx: &mut Context, args: Value) -> wm_core::Result<Value> {
let limit = args.get("limit").and_then(Value::as_u64).unwrap_or(20) as usize;
let category_filter = args.get("category").and_then(Value::as_str);
let bus = self
.bus
.lock()
.map_err(|e| wm_core::CoreError::Tool(format!("resonance bus lock: {e}")))?;
let recent = bus.recent_events(limit);
let events: Vec<Value> = recent
.iter()
.filter(|e| {
if let Some(cat) = category_filter {
e.event_type.category().as_str() == cat
} else {
true
}
})
.map(|e| {
json!({
"event_type": e.event_type.as_str(),
"category": e.event_type.category().as_str(),
"source": e.source,
"timestamp": e.timestamp.to_rfc3339(),
"salience": e.salience,
"payload": e.payload,
})
})
.collect();
Ok(json!({
"status": "success",
"count": events.len(),
"events": events,
}))
}
fn stats(&self) -> &ToolStats {
&self.stats
}
}
fn parse_event_type(s: &str) -> Option<EventType> {
EventType::all().into_iter().find(|et| et.as_str() == s)
}
pub fn register_resonance(
registry: &wm_dispatch::ToolRegistry,
bus: Arc<Mutex<GanYingBus>>,
) -> wm_dispatch::ToolRegistry {
registry
.register(Arc::new(BusStatsTool::new(bus.clone())))
.register(Arc::new(BusEmitTool::new(bus.clone())))
.register(Arc::new(BusRecentTool::new(bus)))
}
#[cfg(test)]
mod tests {
use super::*;
fn test_bus() -> Arc<Mutex<GanYingBus>> {
Arc::new(Mutex::new(GanYingBus::default()))
}
#[tokio::test]
async fn bus_stats_returns_stats() {
let bus = test_bus();
let tool = BusStatsTool::new(bus);
let mut ctx = Context::default();
let v = tool.call(&mut ctx, json!({})).await.unwrap();
assert_eq!(v["status"], "success");
assert_eq!(v["events_emitted"], 0);
}
#[tokio::test]
async fn bus_emit_emits_event() {
let bus = test_bus();
let tool = BusEmitTool::new(bus.clone());
let mut ctx = Context::default();
let v = tool
.call(
&mut ctx,
json!({"event_type": "system_heartbeat", "source": "test", "data": {"foo": 1}}),
)
.await
.unwrap();
assert_eq!(v["status"], "success");
let stats_tool = BusStatsTool::new(bus);
let v2 = stats_tool.call(&mut ctx, json!({})).await.unwrap();
assert_eq!(v2["events_emitted"], 1);
}
#[test]
fn bus_emit_declares_writes() {
let tool = BusEmitTool::new(test_bus());
assert!(
tool.effects().writes.contains(&wm_core::Resource::EventBus),
"bus.emit must declare an EventBus write"
);
}
#[tokio::test]
async fn bus_emit_unknown_type_errors() {
let bus = test_bus();
let tool = BusEmitTool::new(bus);
let mut ctx = Context::default();
let result = tool
.call(&mut ctx, json!({"event_type": "nonexistent.event"}))
.await;
assert!(result.is_err());
}
#[tokio::test]
async fn bus_emit_missing_type_errors() {
let bus = test_bus();
let tool = BusEmitTool::new(bus);
let mut ctx = Context::default();
let result = tool.call(&mut ctx, json!({})).await;
assert!(result.is_err());
}
#[tokio::test]
async fn bus_recent_returns_events() {
let bus = test_bus();
{
let mut b = bus.lock().unwrap();
b.emit(EventType::SystemHeartbeat, "test", json!({"n": 1}));
b.emit(EventType::SystemHeartbeat, "test", json!({"n": 2}));
}
let tool = BusRecentTool::new(bus);
let mut ctx = Context::default();
let v = tool.call(&mut ctx, json!({"limit": 5})).await.unwrap();
assert_eq!(v["status"], "success");
assert_eq!(v["count"], 2);
}
#[tokio::test]
async fn bus_recent_filters_by_category() {
let bus = test_bus();
{
let mut b = bus.lock().unwrap();
b.emit(EventType::SystemHeartbeat, "test", json!({}));
b.emit(EventType::MemoryCreated, "test", json!({}));
}
let tool = BusRecentTool::new(bus);
let mut ctx = Context::default();
let v = tool
.call(&mut ctx, json!({"category": "memory"}))
.await
.unwrap();
assert_eq!(v["count"], 1);
}
#[tokio::test]
async fn resonance_tools_are_heart_gana() {
let bus = test_bus();
assert_eq!(BusStatsTool::new(bus.clone()).gana(), Gana::Heart);
assert_eq!(BusEmitTool::new(bus.clone()).gana(), Gana::Heart);
assert_eq!(BusRecentTool::new(bus).gana(), Gana::Heart);
}
}