Skip to content

Feature: Event-driven Hooks System #1796

@yinwm

Description

@yinwm

Feature: Event-driven Hooks System

Background

OpenClaw has a powerful Hooks system that automatically triggers user-defined code when specific events occur (e.g., message sent, command executed).

Problem: PicoClaw currently only supports active invocation (Skills) and scheduled triggers (Cron), but not event-driven automation.

Design Goals

  • Decouple core code from extension functionality
  • Support user-defined hooks
  • Lightweight, following PicoClaw's simplicity principle
  • Pluggable (enable/disable)

Minimal Design

1. Core Interface

// pkg/hooks/hooks.go
package hooks

import "time"

// Event represents a hook event
type Event struct {
    Type      string                 // Event type, e.g., "message:sent"
    Action    string                 // Specific action, e.g., "sent"
    Timestamp time.Time              // When the event occurred
    Context   map[string]interface{} // Event context
}

// Handler is the hook handler function
type Handler func(event Event) error

// Manager manages hooks
type Manager struct {
    handlers map[string][]Handler
    enabled  map[string]bool
}

// NewManager creates a hook manager
func NewManager() *Manager {
    return &Manager{
        handlers: make(map[string][]Handler),
        enabled:  make(map[string]bool),
    }
}

// On registers a hook handler
func (m *Manager) On(eventType string, handler Handler) {
    m.handlers[eventType] = append(m.handlers[eventType], handler)
}

// Emit triggers an event
func (m *Manager) Emit(event Event) {
    handlers, ok := m.handlers[event.Type]
    if !ok {
        return
    }
    
    for _, h := range handlers {
        go func(handler Handler) {
            // Execute asynchronously, don't block main flow
            if err := handler(event); err != nil {
                log.Printf("[hooks] handler error: %v", err)
            }
        }(h)
    }
}

// Enable enables a hook
func (m *Manager) Enable(eventType string) {
    m.enabled[eventType] = true
}

// Disable disables a hook
func (m *Manager) Disable(eventType string) {
    m.enabled[eventType] = false
}

2. Predefined Event Types

// pkg/hooks/events.go
package hooks

const (
    // Message events
    EventMessageReceived = "message:received" // Message received
    EventMessageSent     = "message:sent"     // Message sent
    
    // Command events
    EventCommandNew    = "command:new"    // /new command
    EventCommandReset  = "command:reset"  // /reset command
    EventCommandStop   = "command:stop"   // /stop command
    
    // Task events
    EventTaskCreated   = "task:created"   // Task created
    EventTaskCompleted = "task:completed" // Task completed
    
    // Skill events
    EventSkillLoaded   = "skill:loaded"   // Skill loaded
    EventSkillExecuted = "skill:executed" // Skill executed
    
    // Lifecycle events
    EventGatewayStartup = "gateway:startup" // Gateway started
    EventSessionStart   = "session:start"   // Session started
)

3. Integration Example

// Trigger hook when Agent sends a message
func (a *Agent) SendMessage(content string) error {
    // 1. Send message
    err := a.channel.Send(content)
    if err != nil {
        return err
    }
    
    // 2. Trigger post-response hook
    a.hooks.Emit(hooks.Event{
        Type:      hooks.EventMessageSent,
        Action:    "sent",
        Timestamp: time.Now(),
        Context: map[string]interface{}{
            "content":   content,
            "channel":   a.channel.Name(),
            "recipient": a.recipient,
        },
    })
    
    return nil
}

4. User-defined Hook

// hooks/log-messages/hook.go
package main

import (
    "encoding/json"
    "os"
    
    "github.com/sipeed/picoclaw/pkg/hooks"
)

func init() {
    // Auto-register hook
    hooks.Register("message-logger", hooks.EventMessageSent, logMessage)
}

func logMessage(event hooks.Event) error {
    entry := map[string]interface{}{
        "timestamp": event.Timestamp,
        "content":   event.Context["content"],
        "channel":   event.Context["channel"],
    }
    
    data, _ := json.Marshal(entry)
    f, _ := os.OpenFile("messages.log", os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
    defer f.Close()
    f.WriteString(string(data) + "\n")
    
    return nil
}

Configuration

{
  "hooks": {
    "enabled": true,
    "entries": {
      "message-logger": {
        "enabled": true,
        "events": ["message:sent"]
      },
      "auto-backup": {
        "enabled": false,
        "events": ["task:completed"]
      }
    }
  }
}

Implementation Priority

Phase Content Effort
P0 Core interface + 2-3 events 1-2 days
P1 Configuration file support 1 day
P2 CLI commands (list/enable/disable) 1 day
P3 Hook package discovery and loading 2-3 days

Discussion

Question 1: Do we need Hooks?

PicoClaw is positioned as a lightweight assistant. Hooks add complexity.

Pros:

  • Decouple core code from extensions
  • Support user-defined automation
  • Competitive with OpenClaw, etc.

Cons:

  • Increases code complexity
  • Might not be needed (YAGNI principle)
  • Skills system might be sufficient

Question 2: If implemented, what scope?

Minimal: Only support 3-5 core events, configuration file management
Full: Reference OpenClaw's complete implementation (auto-discovery, hook packages, CLI management)

Question 3: Relationship with Skills?

  • Skills: Active invocation, user explicitly triggers
  • Hooks: Passive response, system automatically triggers

Is this needed? Or is Skills + Cron enough?

References


Please vote:

  • 👍 Support implementing Hooks
  • 👎 Not needed for now
  • 🤔 Need more discussion

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions