Class: Clacky::HookManager

Inherits:
Object
  • Object
show all
Defined in:
lib/clacky/agent/hook_manager.rb

Constant Summary collapse

HOOK_EVENTS =
[
  :before_tool_use,
  :after_tool_use,
  :on_tool_error,
  :on_start,
  :on_complete,
  :on_iteration,
  :session_rollback
].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(agent: nil) ⇒ HookManager

Returns a new instance of HookManager.



17
18
19
20
# File 'lib/clacky/agent/hook_manager.rb', line 17

def initialize(agent: nil)
  @hooks = Hash.new { |h, k| h[k] = [] }
  @agent = agent
end

Instance Attribute Details

#agentObject

Returns the value of attribute agent.



15
16
17
# File 'lib/clacky/agent/hook_manager.rb', line 15

def agent
  @agent
end

Instance Method Details

#add(event, &block) ⇒ Object



22
23
24
25
# File 'lib/clacky/agent/hook_manager.rb', line 22

def add(event, &block)
  validate_event!(event)
  @hooks[event] << block
end

#clear(event = nil) ⇒ Object



65
66
67
68
69
70
71
72
# File 'lib/clacky/agent/hook_manager.rb', line 65

def clear(event = nil)
  if event
    validate_event!(event)
    @hooks[event].clear
  else
    @hooks.clear
  end
end

#has_hooks?(event) ⇒ Boolean

Returns:

  • (Boolean)


61
62
63
# File 'lib/clacky/agent/hook_manager.rb', line 61

def has_hooks?(event)
  @hooks[event].any?
end

#trigger(event, *args) ⇒ Hash

Extra trailing arg: the agent that owns this hook chain, so ext hooks can call agent.emit_event(...). Blocks are procs — those declaring fewer params (|call|, |call, result|) silently ignore it.

Returns:

  • (Hash)

    {action: :allow}, {action: :deny, reason:}, or {action: :handled, result:} when a hook fulfilled the call itself.



32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# File 'lib/clacky/agent/hook_manager.rb', line 32

def trigger(event, *args)
  validate_event!(event)
  result = { action: :allow }

  @hooks[event].each do |hook|
    begin
      hook_result = hook.call(*args, @agent)
      next unless hook_result.is_a?(Hash)
      # First deny wins and stops the chain: a weaker later verdict must
      # never clobber a stronger earlier one, and the first deny's reason
      # is the one that reaches the agent. Rewrite hooks mutate `call` in
      # place (chained rewrite), so for non-deny results there's nothing to
      # merge — we just keep going.
      #
      # :handled short-circuits the same way — the hook has already produced
      # the tool's result, so later hooks have nothing left to act on.
      if hook_result[:action] == :deny || hook_result[:action] == :handled
        result = hook_result
        break
      end
    rescue StandardError => e
      # Log error but don't fail
      Clacky::Logger.error("Hook error", event: event, error: e)
    end
  end

  result
end

#validate_event!(event) ⇒ Object

Raises:

  • (ArgumentError)


75
76
77
78
79
# File 'lib/clacky/agent/hook_manager.rb', line 75

def validate_event!(event)
  return if HOOK_EVENTS.include?(event)

  raise ArgumentError, "Invalid hook event: #{event}. Must be one of #{HOOK_EVENTS.join(', ')}"
end