Class: SwarmSDK::Hooks::Executor

Inherits:
Object
  • Object
show all
Defined in:
lib/swarm_sdk/hooks/executor.rb

Overview

Executes hooks with proper chaining, error handling, and logging

The executor:

  • Chains multiple hooks for the same event
  • Handles errors and blocking (via Error or Result.halt)
  • Respects matcher patterns (only runs matching hooks)
  • Logs execution for debugging
  • Returns Result indicating action to take

Examples:

Execute hooks

executor = SwarmSDK::Hooks::Executor.new(registry, logger)
context = SwarmSDK::Hooks::Context.new(...)
result = executor.execute(event: :pre_tool_use, context: context, hooks: agent_hooks)
if result.halt?
  # Handle halt
elsif result.replace?
  # Use replacement value
end

Instance Method Summary collapse

Constructor Details

#initialize(registry, logger: nil) ⇒ Executor



26
27
28
29
# File 'lib/swarm_sdk/hooks/executor.rb', line 26

def initialize(registry, logger: nil)
  @registry = registry
  @logger = logger || Logger.new(nil) # Null logger if not provided
end

Instance Method Details

#execute(event:, context:, callbacks: []) ⇒ Result

Execute all hooks for an event

Execution order:

  1. Swarm-level defaults (from registry)
  2. Agent-specific hooks
  3. Within each group, by priority (highest first)

Hooks must return:

  • Result - to control execution flow (halt, replace, reprompt, continue)
  • nil - treated as continue with unmodified context

Raises:

  • (Error)

    If a hook raises an error



47
48
49
50
51
52
53
54
55
56
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
82
83
84
# File 'lib/swarm_sdk/hooks/executor.rb', line 47

def execute(event:, context:, callbacks: [])
  # Combine swarm defaults and agent hooks
  all_hooks = @registry.get_defaults(event) + callbacks

  # Filter by matcher (for tool events)
  if context.tool_event? && context.tool_name
    all_hooks = all_hooks.select { |hook| hook.matches?(context.tool_name) }
  end

  # Execute hooks in order
  all_hooks.each do |hook_def|
    result = execute_single(hook_def, context)

    # Only Result controls flow - nil means continue
    next unless result.is_a?(Result)

    # Early return for control flow actions
    return result if result.halt? || result.replace? || result.reprompt? || result.finish_agent? || result.finish_swarm?

    # Update context if continue with modified context
    context = result.value if result.continue? && result.value.is_a?(Context)
  end

  # All hooks executed successfully - continue with final context
  Result.continue(context)
rescue Error => e
  # Re-raise with context for better error messages
  @logger.error("Hook blocked execution: #{e.message}")
  raise
rescue StandardError => e
  # Wrap unexpected errors
  @logger.error("Hook failed unexpectedly: #{e.class} - #{e.message}")
  @logger.error(e.backtrace.join("\n"))
  raise Error.new(
    "Hook failed: #{e.message}",
    context: context,
  )
end

#execute_safe(event:, context:, callbacks: []) ⇒ Result

Execute hooks and return result safely (without raising)

This is a convenience method that catches Error and converts it to a halt result, making it easier to use in control flow.



138
139
140
141
142
143
# File 'lib/swarm_sdk/hooks/executor.rb', line 138

def execute_safe(event:, context:, callbacks: [])
  execute(event: event, context: context, callbacks: callbacks)
rescue Error => e
  @logger.warn("Execution blocked by hook: #{e.message}")
  Result.halt(e.message)
end

#execute_single(hook_def, context) ⇒ Result?

Execute a single hook

Raises:

  • (Error)

    If hook execution fails



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
# File 'lib/swarm_sdk/hooks/executor.rb', line 92

def execute_single(hook_def, context)
  proc = hook_def.resolve_proc(@registry)

  @logger.debug("Executing hook for #{context.event} (agent: #{context.agent_name})")

  # Execute hook with context as parameter
  # Users can access convenience methods via context parameter:
  #   hook(:event) { |ctx| ctx.halt("msg") }
  # This preserves lexical scope and access to surrounding instance variables
  proc.call(context)
rescue Error
  # Pass through blocking errors
  raise
rescue StandardError => e
  # Wrap other errors with context and detailed debugging
  hook_name = hook_def.named_hook? ? hook_def.proc : "anonymous"

  # Log detailed error info for debugging
  @logger.error("=" * 80)
  @logger.error("HOOK EXECUTION ERROR")
  @logger.error("  Hook: #{hook_name}")
  @logger.error("  Event: #{context.event}")
  @logger.error("  Agent: #{context.agent_name}")
  @logger.error("  Proc class: #{proc.class}")
  @logger.error("  Proc arity: #{proc.arity} (expected: 1 for |context|)")
  @logger.error("  Error: #{e.class.name}: #{e.message}")
  @logger.error("  Backtrace:")
  e.backtrace.first(15).each { |line| @logger.error("    #{line}") }
  @logger.error("=" * 80)

  raise Error.new(
    "Hook #{hook_name} failed: #{e.message}",
    hook_name: hook_name,
    context: context,
  )
end