Skip to main content

Overview

FastSkill implements an event-driven architecture using a publish-subscribe pattern. The event bus enables decoupled components to react to skill lifecycle changes, facilitating features like hot-reload, cache invalidation, and logging.
The event system is defined in src/events/event_bus.rs and provides both synchronous and asynchronous event handling capabilities.

Event Types

FastSkill defines the following event types for skill lifecycle operations:
EventDescriptionWhen TriggeredSource Location
SkillRegisteredA skill was successfully registeredAfter fastskill add completessrc/cli/commands/add.rs
SkillUpdatedA skill’s metadata or configuration was updatedAfter fastskill update completessrc/cli/commands/update.rs
SkillUnregisteredA skill was removed from the systemAfter fastskill remove completessrc/cli/commands/remove.rs
SkillReloadedA skill was reloaded (e.g., after hot-reload)After file change in hot-reloadsrc/events/hot_reload.rs
SkillEnabledA skill was enabledAfter fastskill disable or fastskill addsrc/core/skill_manager.rs
SkillDisabledA skill was disabledAfter fastskill disablesrc/core/skill_manager.rs
SkillValidationFailedSkill validation failed with errorsDuring skill registration or updatesrc/validation/skill_validator.rs
HotReloadEnabledHot-reload feature was enabledOn service startup with hot-reload featuresrc/events/hot_reload.rs
HotReloadDisabledHot-reload feature was disabledOn service shutdown or explicit disablesrc/events/hot_reload.rs
CustomCustom user-defined eventsWhen explicitly publishedUser code

Event Data Structures

SkillRegistered

SkillEvent::SkillRegistered {
    skill_id: String,
    skill: Box<SkillDefinition>,
}
Example:
{
  "skill_id": "web-scraper",
  "skill": {
    "id": "web-scraper",
    "name": "Web Scraper",
    "version": "1.2.3",
    "description": "Web scraping utilities",
    // ... other skill fields
  }
}

SkillUpdated

SkillEvent::SkillUpdated {
    skill_id: String,
    changes: SkillUpdate,
}
SkillUpdate fields:
struct SkillUpdate {
    pub name: Option<String>,
    pub description: Option<String>,
    pub version: Option<String>,
    pub enabled: Option<bool>,
}
Example:
{
  "skill_id": "web-scraper",
  "changes": {
    "version": "1.2.4",
    "enabled": true
  }
}

SkillReloaded

SkillEvent::SkillReloaded {
    skill_id: String,
    success: bool,
    error_message: Option<String>,
}
Example:
{
  "skill_id": "web-scraper",
  "success": true,
  "error_message": null
}
Or on failure:
{
  "skill_id": "web-scraper",
  "success": false,
  "error_message": "Failed to parse SKILL.md: Invalid YAML"
}

HotReloadEnabled

SkillEvent::HotReloadEnabled {
    config: HotReloadConfig,
}
HotReloadConfig fields:
struct HotReloadConfig {
    pub watch_paths: Vec<String>,
    pub debounce_ms: u64,
    pub auto_reload: bool,
    pub max_concurrent_reloads: usize,
}
Example:
{
  "config": {
    "watch_paths": ["/home/user/.claude/skills"],
    "debounce_ms": 500,
    "auto_reload": true,
    "max_concurrent_reloads": 5
  }
}

EventBus Architecture

The EventBus struct provides the core event distribution mechanism:

Core Components

pub struct EventBus {
    /// Broadcast sender for events
    sender: broadcast::Sender<SkillEvent>,

    /// Event handlers registry
    handlers: Arc<RwLock<EventHandlersMap>>,

    /// Event history for debugging
    event_history: Arc<RwLock<Vec<(SkillEvent, std::time::Instant)>>,

    /// Maximum history size
    max_history_size: usize,
}
Key Features:
  • Broadcast Channel: Uses Tokio’s broadcast channel for fan-out event distribution
  • Handler Registry: Maps event types to registered handlers
  • Event History: Stores recent events for debugging (default: 100 events)
  • Thread-Safe: Uses Arc<RwLock<>> for concurrent access

Event Handler Interface

EventHandler Trait

Custom event handlers must implement the EventHandler trait:
#[async_trait]
pub trait EventHandler: Send + Sync {
    async fn handle_event(&self, event: SkillEvent) -> Result<(), ServiceError>;
}
Example Implementation:
use fastskill::events::{EventHandler, SkillEvent, SkillUpdate};
use fastskill::ServiceError;

pub struct CacheInvalidator;

#[async_trait]
impl EventHandler for CacheInvalidator {
    async fn handle_event(&self, event: SkillEvent) -> Result<(), ServiceError> {
        match event {
            SkillEvent::SkillUpdated { skill_id, changes } => {
                if changes.version.is_some() {
                    self.invalidate_cache(&skill_id).await?;
                }
            }
            _ => {}
        }
        Ok(())
    }
}

impl CacheInvalidator {
    async fn invalidate_cache(&self, skill_id: &str) -> Result<(), ServiceError> {
        // Invalidation logic here
        Ok(())
    }
}

Built-in Event Handlers

LoggingEventHandler

Logs all events to the console using tracing:
pub struct LoggingEventHandler;

#[async_trait]
impl EventHandler for LoggingEventHandler {
    async fn handle_event(&self, event: SkillEvent) -> Result<(), ServiceError> {
        match event {
            SkillEvent::SkillRegistered { skill_id, skill } => {
                info!("[OK] Skill registered: {} ({})", skill.name, skill_id);
            }
            SkillEvent::SkillUpdated { skill_id, .. } => {
                info!("Skill updated: {}", skill_id);
            }
            SkillEvent::SkillUnregistered { skill_id } => {
                info!("Skill unregistered: {}", skill_id);
            }
            SkillEvent::SkillReloaded { skill_id, success, error_message } => {
                if success {
                    info!("Skill reloaded successfully: {}", skill_id);
                } else {
                    warn!("[ERROR] Skill reload failed: {} - {:?}", skill_id, error_message);
                }
            }
            SkillEvent::SkillEnabled { skill_id } => {
                info!("[OK] Skill enabled: {}", skill_id);
            }
            SkillEvent::SkillDisabled { skill_id } => {
                info!("Skill disabled: {}", skill_id);
            }
            SkillEvent::SkillValidationFailed { skill_id, errors } => {
                warn!("[ERROR] Skill validation failed: {} - {} errors", skill_id, errors.len());
            }
            SkillEvent::HotReloadEnabled { config } => {
                info!("[INFO] Hot reload enabled for {} paths", config.watch_paths.len());
            }
            SkillEvent::HotReloadDisabled => {
                info!("Hot reload disabled");
            }
            _ => {}
        }
        Ok(())
    }
}

MetricsEventHandler

Tracks event statistics and counts:
pub struct MetricsEventHandler {
    event_counts: Arc<RwLock<HashMap<String, usize>>>,
}

#[async_trait]
impl EventHandler for MetricsEventHandler {
    async fn handle_event(&self, event: SkillEvent) -> Result<(), ServiceError> {
        let event_type = // ... determine event type string ...

        let mut counts = self.event_counts.write().await;
        *counts.entry(event_type).or_insert(0) += 1;
        Ok(())
    }
}

Publishing Events

Using Convenience Methods

FastSkill provides convenience methods for publishing common events:
use fastskill::events::EventBus;
use fastskill::core::skill_manager::SkillDefinition;

// Skill registered
event_bus.publish_skill_registered(
    skill_id: "web-scraper".to_string(),
    skill: skill_definition,
).await?;

// Skill updated
event_bus.publish_skill_updated(
    skill_id: "web-scraper".to_string(),
    changes: SkillUpdate {
        version: Some("1.2.4".to_string()),
        description: None,
        name: None,
        enabled: Some(true),
    },
).await?;

// Skill unregistered
event_bus.publish_skill_unregistered(
    skill_id: "web-scraper".to_string(),
).await?;

// Skill reloaded
event_bus.publish_skill_reloaded(
    skill_id: "web-scraper".to_string(),
    success: true,
    error_message: None,
).await?;

// Skill enabled
event_bus.publish_skill_enabled(
    skill_id: "web-scraper".to_string(),
).await?;

// Skill disabled
event_bus.publish_skill_disabled(
    skill_id: "web-scraper".to_string(),
).await?;

Publishing Custom Events

use serde_json::json;

// Create custom event
let custom_event = SkillEvent::Custom {
    event_type: "custom:event".to_string(),
    data: json!({
        "key": "value",
        "timestamp": "2026-02-02T12:00:00Z"
    }),
};

// Publish event
event_bus.publish_event(custom_event).await?;

Subscribing to Events

Basic Subscription

use fastskill::events::EventBus;

// Create event bus
let event_bus = EventBus::new();

// Subscribe to all events
let mut receiver = event_bus.subscribe();

// Handle events in a task
tokio::spawn(async move {
    while let Ok(event) = receiver.recv().await {
        println!("Received event: {:?}", event);
    }
});

Registering Event Handlers

use fastskill::events::{EventHandler, LoggingEventHandler, MetricsEventHandler};

// Create event bus
let event_bus = EventBus::new();

// Register logging handler for all events
event_bus.register_handler("logging", LoggingEventHandler::new()).await?;

// Register metrics handler
let metrics_handler = MetricsEventHandler::new();
event_bus.register_handler("metrics", metrics_handler).await?;

// Register custom handler for specific event type
event_bus.register_handler("cache-invalidator", CacheInvalidator::new()).await?;

Event Type Strings

For registering handlers, use these event type strings:
Event TypeString
SkillRegistered"skill:registered"
SkillUpdated"skill:updated"
SkillUnregistered"skill:unregistered"
SkillReloaded"skill:reloaded"
SkillValidationFailed"skill:validation:failed"
HotReloadEnabled"hot-reload:enabled"
HotReloadDisabled"hot-reload:disabled"
SkillEnabled"skill:enabled"
SkillDisabled"skill:disabled"

Event History

The event bus maintains a configurable history of recent events for debugging:
use fastskill::events::EventBus;

let event_bus = EventBus::new(); // Default max_history_size: 100

// Get event history
let history = event_bus.get_event_history().await;

for (event, timestamp) in history {
    println!("[{}] Event: {:?}", timestamp, event);
}

// Clear history
event_bus.clear_event_history().await;
Event history is circular and automatically trimmed when it exceeds max_history_size. This prevents memory leaks in long-running services.

Use Cases

Hot-Reload Integration

The event system enables hot-reload by triggering skill reloads when files change:
use notify::{Watcher, RecursiveMode, watcher};
use fastskill::events::EventBus;

async fn setup_hot_reload(event_bus: &EventBus) -> Result<(), ServiceError> {
    let mut watcher = watcher(
        move |res| {
            match res {
                Ok(event) => {
                    // Publish skill reloaded event on file changes
                    let skill_id = extract_skill_id(&event.path);
                    tokio::spawn({
                        let event_bus = event_bus.clone();
                        async move {
                            event_bus.publish_skill_reloaded(
                                skill_id,
                                success: true,
                                error_message: None,
                            ).await;
                        }
                    });
                }
                Err(e) => {
                    eprintln!("Watch error: {:?}", e);
                }
            }
        },
    )?;

    // Watch skills directory
    watcher.watch(".claude/skills/", RecursiveMode::Recursive)?;

    Ok(())
}

Cache Invalidation

Automatically invalidate caches when skills are updated:
struct Cache {
    data: HashMap<String, SkillMetadata>,
}

struct CacheInvalidator {
    cache: Arc<RwLock<Cache>>,
}

#[async_trait]
impl EventHandler for CacheInvalidator {
    async fn handle_event(&self, event: SkillEvent) -> Result<(), ServiceError> {
        match event {
            SkillEvent::SkillUpdated { skill_id, .. } |
            SkillEvent::SkillUnregistered { skill_id } |
            SkillEvent::SkillReloaded { skill_id, .. } => {
                let mut cache = self.cache.write().await;
                cache.data.remove(&skill_id);
                println!("Invalidated cache for skill: {}", skill_id);
            }
            _ => {}
        }
        Ok(())
    }
}

Metrics Collection

Track skill lifecycle events for observability:
struct MetricsCollector {
    event_bus: Arc<EventBus>,
    metrics: Arc<RwLock<HashMap<String, usize>>>,
}

impl MetricsCollector {
    pub async fn start(&self) {
        let metrics_handler = MetricsEventHandler::new();
        self.event_bus.register_handler("metrics", metrics_handler).await?;

        // Periodically report metrics
        let mut interval = tokio::time::interval(Duration::from_secs(60));
        loop {
            interval.tick().await;
            let counts = metrics_handler.get_event_counts().await;
            println!("Event Metrics: {:?}", counts);
        }
    }
}

Event-Driven UI Updates

Update UI components in response to skill changes:
struct UIUpdater {
    ui_sender: mpsc::Sender<UIMessage>,
}

#[async_trait]
impl EventHandler for UIUpdater {
    async fn handle_event(&self, event: SkillEvent) -> Result<(), ServiceError> {
        let message = match event {
            SkillEvent::SkillRegistered { skill_id, skill } => {
                UIMessage::SkillAdded {
                    id: skill_id,
                    name: skill.name.clone(),
                }
            }
            SkillEvent::SkillUnregistered { skill_id } => {
                UIMessage::SkillRemoved { id: skill_id }
            }
            _ => return Ok(()),
        };

        self.ui_sender.send(message).await?;
        Ok(())
    }
}

Event Flow Diagram

Configuration

Buffer Size

The event bus uses a broadcast channel with configurable buffer size:
// Default: 1000 events
let (sender, _) = broadcast::channel(1000);
If event handlers are slow, the buffer may fill up. Monitor using get_registered_handlers() to ensure handlers are processing events efficiently.

History Size

Event history is limited to prevent memory leaks:
let event_bus = EventBus {
    max_history_size: 100, // Default
    ..Default::default()
};

Debugging

Inspecting Event History

let event_bus = EventBus::new();

// Perform operations
// ...

// Inspect event history
let history = event_bus.get_event_history().await;
for (event, timestamp) in history {
    println!("[{}] {:?}", timestamp, event);
}

Monitoring Event Handler Registration

let handlers = event_bus.get_registered_handlers().await;
for (event_type, count) in handlers {
    println!("Event type '{}': {} handler(s) registered", event_type, count);
}

Logging All Events

The LoggingEventHandler provides built-in event logging:
use fastskill::events::LoggingEventHandler;

let event_bus = EventBus::new();
let logging_handler = LoggingEventHandler::new();
event_bus.register_handler("logging", logging_handler).await?;

// All events are now logged via tracing
Enable logging:
RUST_LOG=fastskill=debug cargo run --bin fastskill -- <command>

Performance Considerations

Event Handler Efficiency

#[async_trait]
impl EventHandler for MyHandler {
    async fn handle_event(&self, event: SkillEvent) -> Result<(), ServiceError> {
        // Pattern match early to avoid unnecessary work
        match event {
            SkillEvent::SkillUpdated { skill_id, .. } => {
                // Only process relevant events
                return self.handle_update(skill_id).await;
            }
            _ => return Ok(()),
        }
    }
}

Concurrency

Event handlers are executed concurrently:
// Multiple handlers can process the same event simultaneously
event_bus.publish_skill_registered(skill_id, skill).await?;

// Handlers run in parallel
// Handler 1: processes event
// Handler 2: processes event (at same time)
Ensure event handlers are thread-safe using Send + Sync bounds and proper synchronization primitives like Arc<RwLock<>>.

Error Handling

Event handlers should handle errors gracefully:
#[async_trait]
impl EventHandler for RobustHandler {
    async fn handle_event(&self, event: SkillEvent) -> Result<(), ServiceError> {
        match self.process_event(event).await {
            Ok(_) => Ok(()),
            Err(e) => {
                // Log but don't fail event bus
                warn!("Handler error: {:?}, continuing...", e);
                Ok(())
            }
        }
    }
}

Troubleshooting

Handler not registered: Verify you called register_handler for your event type.
Check registration: Use get_registered_handlers() to verify handler registration.
let handlers = event_bus.get_registered_handlers().await;
println!("Registered handlers: {:?}", handlers);
Wrong event type string: Ensure event type string matches exactly.
Valid types: skill:registered, skill:updated, skill:unregistered, skill:reloaded, skill:validation:failed, hot-reload:enabled, hot-reload:disabled, skill:enabled, skill:disabled
History limit reached: Events older than max_history_size are dropped.
Increase limit: Create EventBus with larger history.
let event_bus = EventBus {
    max_history_size: 1000, // Increased from default 100
    ..Default::default()
};
Blocking handlers: Event handlers must be async and return quickly.
Spawn async tasks: For long-running operations, spawn tasks instead of blocking.
async fn handle_event(&self, event: SkillEvent) -> Result<(), ServiceError> {
    // Spawn long-running task
    tokio::spawn(async move {
        // Long operation here
    });
    Ok(())
}

Best Practices

1

Use specific event handlers

Register handlers for specific event types rather than catching all events. This improves performance and maintainability.
2

Handle errors gracefully

Event handlers should return errors for logging but not crash the event bus. Use non-panicking error handling.
3

Keep handlers lightweight

Event handlers should be fast. Offload heavy work to async tasks spawned from the handler.
4

Use event history for debugging

Query get_event_history() when investigating issues. Event history provides a chronological log of all events.
5

Clean up handlers

Unregister handlers when they’re no longer needed to prevent memory leaks.
6

Test event flows

Write tests that publish events and verify handlers receive them correctly. Use the event bus in tests.

See Also