Class: Norn::PluginManager

Inherits:
Object
  • Object
show all
Defined in:
lib/norn/plugin_manager.rb

Class Attribute Summary collapse

Class Method Summary collapse

Class Attribute Details

.active_pluginsObject

Returns the value of attribute active_plugins.



14
15
16
# File 'lib/norn/plugin_manager.rb', line 14

def active_plugins
  @active_plugins
end

Class Method Details

.declare_hook(event, desc:) ⇒ Object

Phase 1: Declare a new lifecycle event or custom hook in the system



45
46
47
48
49
# File 'lib/norn/plugin_manager.rb', line 45

def declare_hook(event, desc:)
  @lock.synchronize do
    @declared_hooks[event.to_sym] = desc
  end
end

.declared_hooksObject



51
52
53
# File 'lib/norn/plugin_manager.rb', line 51

def declared_hooks
  @declared_hooks
end

.ensure_active_plugins!Object



16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/norn/plugin_manager.rb', line 16

def ensure_active_plugins!
  return unless @active_plugins.empty? && Norn::Plugin.registered_plugins.any?

  # Guard against recursive instantiation on the same thread
  return if Thread.current[:norn_instantiating_plugins]

  @lock.synchronize do
    return unless @active_plugins.empty?
  end

  Thread.current[:norn_instantiating_plugins] = true
  begin
    plugins = Norn::Plugin.registered_plugins.map(&:new)
    @lock.synchronize do
      @active_plugins = plugins if @active_plugins.empty?
    end
  ensure
    Thread.current[:norn_instantiating_plugins] = false
  end
end

.register_core_hooks!Object

Auto-wire declared standard Norn core hooks



152
153
154
155
156
157
158
159
160
161
162
163
164
# File 'lib/norn/plugin_manager.rb', line 152

def register_core_hooks!
  declare_hook :on_boot, desc: "Fires when Norn finishes loading dependencies."
  declare_hook :on_tool_register, desc: "Fires to populate the global ToolRegistry."
  declare_hook :on_cli_register, desc: "Fires to let plugins register custom CLI commands."
  declare_hook :on_mode_register, desc: "Fires to register custom execution modes."
  declare_hook :on_slash_commands_register, desc: "Fires to let plugins register custom slash commands."
  declare_hook :on_user_input, desc: "ROP middleware. Intercepts and processes raw user input before execution."
  declare_hook :before_llm_call, desc: "Fires before calling LLM. Allows inspecting/mutating messages."
  declare_hook :after_llm_call, desc: "Fires after LLM call completes. Informational."
  declare_hook :on_render_response, desc: "ROP middleware. Formats/renders response text in-place."
  declare_hook :after_tool_call, desc: "Fires after a tool execution completes. Informational."
  declare_hook :after_llm_response, desc: "Fires when an LLM response with metadata is received. Informational."
end

.reset!Object

Reset all subscribers, hooks, and active plugins (useful for testing)



143
144
145
146
147
148
149
# File 'lib/norn/plugin_manager.rb', line 143

def reset!
  @lock.synchronize do
    @subscribers.clear
    @declared_hooks.clear
    @active_plugins.clear
  end
end

.subscribe(event, &block) ⇒ Object

Subscribe a block to a specific hook event



38
39
40
41
42
# File 'lib/norn/plugin_manager.rb', line 38

def subscribe(event, &block)
  @lock.synchronize do
    @subscribers[event.to_sym] << block
  end
end

.trigger(event, *args) ⇒ Object

Trigger an informational event (parallel/notification only). Does not mutate state, and does not halt on failure.



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
# File 'lib/norn/plugin_manager.rb', line 57

def trigger(event, *args)
  # 1. Call legacy block-based subscribers
  blocks = @lock.synchronize { @subscribers[event.to_sym].dup }
  blocks.each do |block|
    begin
      block.call(*args)
    rescue => e
      warn "Norn Plugin Trigger Warning: #{event} subscriber failed - #{e.message}"
    end
  end

  # Auto-instantiate registered plugins if active_plugins is empty (crucial for spec environments)
  ensure_active_plugins!

  # 2. Call matching instance methods on registered OO plugin instances
  @active_plugins.each do |plugin|
    if plugin.respond_to?(event.to_sym)
      begin
        plugin.send(event.to_sym, *args)
      rescue => e
        warn "Norn Plugin Trigger Warning: #{event} on #{plugin.class} failed - #{e.message}"
      end
    end
  end
end

.trigger_middleware(event, initial_payload) ⇒ Object

Trigger a sequential ROP middleware pipeline. Sequentially reduces a Hash payload. If any subscriber fails or returns a Failure, the pipeline checks its recovery policy. If the policy is :recover, Norn logs a non-fatal warning and recovers the previous success payload, otherwise it derails.



87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
# File 'lib/norn/plugin_manager.rb', line 87

def trigger_middleware(event, initial_payload)
  # 1. Accumulate all subscribers (legacy blocks + OO plugin instances)
  subscribers = @lock.synchronize { @subscribers[event.to_sym].dup }

  # Auto-instantiate registered plugins if active_plugins is empty (crucial for spec environments)
  ensure_active_plugins!

  @active_plugins.each do |plugin|
    subscribers << plugin if plugin.respond_to?(event.to_sym)
  end

  # 2. Sequentially reduce payload using monadic composition (Kleisli reduce)
  subscribers.reduce(Success(initial_payload)) do |result, subscriber|
    result.bind do |payload|
      policy = resolve_policy(subscriber, event)

      begin
        output = if subscriber.respond_to?(:call)
                   subscriber.call(payload)
                 else
                   subscriber.send(event.to_sym, payload)
                 end

        # Standardize returned output to a monadic Result
        if output.is_a?(Dry::Monads::Result)
          if output.failure? && policy == :recover
            # Non-fatal Failure: Report and recover previous success payload!
            report_non_fatal_failure(output.failure, event, subscriber)
            Success(payload)
          else
            output
          end
        elsif output.is_a?(Hash)
          Success(output)
        else
          # If the subscriber returned nil/nothing, preserve input payload unchanged
          Success(payload)
        end
      rescue => e
        # Catch subscriber exceptions and handle according to policy
        failure = Norn::FailurePayload.new(e, { event: event, payload: payload })

        if policy == :recover
          # Non-fatal Exception: Report and recover previous success payload!
          report_non_fatal_failure(failure, event, subscriber)
          Success(payload)
        else
          # Fatal Exception: Derail immediately to Failure track
          Failure(failure)
        end
      end
    end
  end
end