swarm_sdk 2.0.0.pre.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. checksums.yaml +7 -0
  2. data/lib/swarm_sdk/agent/builder.rb +333 -0
  3. data/lib/swarm_sdk/agent/chat/context_tracker.rb +271 -0
  4. data/lib/swarm_sdk/agent/chat/hook_integration.rb +372 -0
  5. data/lib/swarm_sdk/agent/chat/logging_helpers.rb +99 -0
  6. data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +114 -0
  7. data/lib/swarm_sdk/agent/chat.rb +779 -0
  8. data/lib/swarm_sdk/agent/context.rb +108 -0
  9. data/lib/swarm_sdk/agent/definition.rb +335 -0
  10. data/lib/swarm_sdk/configuration.rb +251 -0
  11. data/lib/swarm_sdk/context_compactor/metrics.rb +147 -0
  12. data/lib/swarm_sdk/context_compactor/token_counter.rb +106 -0
  13. data/lib/swarm_sdk/context_compactor.rb +340 -0
  14. data/lib/swarm_sdk/hooks/adapter.rb +359 -0
  15. data/lib/swarm_sdk/hooks/context.rb +163 -0
  16. data/lib/swarm_sdk/hooks/definition.rb +80 -0
  17. data/lib/swarm_sdk/hooks/error.rb +29 -0
  18. data/lib/swarm_sdk/hooks/executor.rb +146 -0
  19. data/lib/swarm_sdk/hooks/registry.rb +143 -0
  20. data/lib/swarm_sdk/hooks/result.rb +150 -0
  21. data/lib/swarm_sdk/hooks/shell_executor.rb +254 -0
  22. data/lib/swarm_sdk/hooks/tool_call.rb +35 -0
  23. data/lib/swarm_sdk/hooks/tool_result.rb +62 -0
  24. data/lib/swarm_sdk/log_collector.rb +83 -0
  25. data/lib/swarm_sdk/log_stream.rb +69 -0
  26. data/lib/swarm_sdk/markdown_parser.rb +46 -0
  27. data/lib/swarm_sdk/permissions/config.rb +239 -0
  28. data/lib/swarm_sdk/permissions/error_formatter.rb +121 -0
  29. data/lib/swarm_sdk/permissions/path_matcher.rb +35 -0
  30. data/lib/swarm_sdk/permissions/validator.rb +173 -0
  31. data/lib/swarm_sdk/permissions_builder.rb +122 -0
  32. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +237 -0
  33. data/lib/swarm_sdk/providers/openai_with_responses.rb +582 -0
  34. data/lib/swarm_sdk/result.rb +97 -0
  35. data/lib/swarm_sdk/swarm/agent_initializer.rb +224 -0
  36. data/lib/swarm_sdk/swarm/all_agents_builder.rb +62 -0
  37. data/lib/swarm_sdk/swarm/builder.rb +240 -0
  38. data/lib/swarm_sdk/swarm/mcp_configurator.rb +151 -0
  39. data/lib/swarm_sdk/swarm/tool_configurator.rb +267 -0
  40. data/lib/swarm_sdk/swarm.rb +837 -0
  41. data/lib/swarm_sdk/tools/bash.rb +274 -0
  42. data/lib/swarm_sdk/tools/delegate.rb +152 -0
  43. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +83 -0
  44. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +99 -0
  45. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +78 -0
  46. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +194 -0
  47. data/lib/swarm_sdk/tools/edit.rb +150 -0
  48. data/lib/swarm_sdk/tools/glob.rb +158 -0
  49. data/lib/swarm_sdk/tools/grep.rb +231 -0
  50. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +43 -0
  51. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +163 -0
  52. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +65 -0
  53. data/lib/swarm_sdk/tools/multi_edit.rb +232 -0
  54. data/lib/swarm_sdk/tools/path_resolver.rb +43 -0
  55. data/lib/swarm_sdk/tools/read.rb +251 -0
  56. data/lib/swarm_sdk/tools/registry.rb +73 -0
  57. data/lib/swarm_sdk/tools/scratchpad_list.rb +88 -0
  58. data/lib/swarm_sdk/tools/scratchpad_read.rb +59 -0
  59. data/lib/swarm_sdk/tools/scratchpad_write.rb +88 -0
  60. data/lib/swarm_sdk/tools/stores/read_tracker.rb +61 -0
  61. data/lib/swarm_sdk/tools/stores/scratchpad.rb +153 -0
  62. data/lib/swarm_sdk/tools/stores/todo_manager.rb +65 -0
  63. data/lib/swarm_sdk/tools/todo_write.rb +216 -0
  64. data/lib/swarm_sdk/tools/write.rb +117 -0
  65. data/lib/swarm_sdk/utils.rb +50 -0
  66. data/lib/swarm_sdk/version.rb +5 -0
  67. data/lib/swarm_sdk.rb +69 -0
  68. metadata +169 -0
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module Hooks
5
+ # Executes hooks with proper chaining, error handling, and logging
6
+ #
7
+ # The executor:
8
+ # - Chains multiple hooks for the same event
9
+ # - Handles errors and blocking (via Error or Result.halt)
10
+ # - Respects matcher patterns (only runs matching hooks)
11
+ # - Logs execution for debugging
12
+ # - Returns Result indicating action to take
13
+ #
14
+ # @example Execute hooks
15
+ # executor = SwarmSDK::Hooks::Executor.new(registry, logger)
16
+ # context = SwarmSDK::Hooks::Context.new(...)
17
+ # result = executor.execute(event: :pre_tool_use, context: context, hooks: agent_hooks)
18
+ # if result.halt?
19
+ # # Handle halt
20
+ # elsif result.replace?
21
+ # # Use replacement value
22
+ # end
23
+ class Executor
24
+ # @param registry [Registry] Hook registry for resolving named hooks
25
+ # @param logger [Logger, nil] Logger for debugging (optional)
26
+ def initialize(registry, logger: nil)
27
+ @registry = registry
28
+ @logger = logger || Logger.new(nil) # Null logger if not provided
29
+ end
30
+
31
+ # Execute all hooks for an event
32
+ #
33
+ # Execution order:
34
+ # 1. Swarm-level defaults (from registry)
35
+ # 2. Agent-specific hooks
36
+ # 3. Within each group, by priority (highest first)
37
+ #
38
+ # Hooks must return:
39
+ # - Result - to control execution flow (halt, replace, reprompt, continue)
40
+ # - nil - treated as continue with unmodified context
41
+ #
42
+ # @param event [Symbol] Event type
43
+ # @param context [Context] Context to pass to hooks
44
+ # @param callbacks [Array<Definition>] Agent-specific hooks
45
+ # @return [Result] Result indicating action and value
46
+ # @raise [Error] If a hook raises an error
47
+ def execute(event:, context:, callbacks: [])
48
+ # Combine swarm defaults and agent hooks
49
+ all_hooks = @registry.get_defaults(event) + callbacks
50
+
51
+ # Filter by matcher (for tool events)
52
+ if context.tool_event? && context.tool_name
53
+ all_hooks = all_hooks.select { |hook| hook.matches?(context.tool_name) }
54
+ end
55
+
56
+ # Execute hooks in order
57
+ all_hooks.each do |hook_def|
58
+ result = execute_single(hook_def, context)
59
+
60
+ # Only Result controls flow - nil means continue
61
+ next unless result.is_a?(Result)
62
+
63
+ # Early return for control flow actions
64
+ return result if result.halt? || result.replace? || result.reprompt? || result.finish_agent? || result.finish_swarm?
65
+
66
+ # Update context if continue with modified context
67
+ context = result.value if result.continue? && result.value.is_a?(Context)
68
+ end
69
+
70
+ # All hooks executed successfully - continue with final context
71
+ Result.continue(context)
72
+ rescue Error => e
73
+ # Re-raise with context for better error messages
74
+ @logger.error("Hook blocked execution: #{e.message}")
75
+ raise
76
+ rescue StandardError => e
77
+ # Wrap unexpected errors
78
+ @logger.error("Hook failed unexpectedly: #{e.class} - #{e.message}")
79
+ @logger.error(e.backtrace.join("\n"))
80
+ raise Error.new(
81
+ "Hook failed: #{e.message}",
82
+ context: context,
83
+ )
84
+ end
85
+
86
+ # Execute a single hook
87
+ #
88
+ # @param hook_def [Definition] Hook to execute
89
+ # @param context [Context] Current context
90
+ # @return [Result, nil] Result from hook (Result or nil)
91
+ # @raise [Error] If hook execution fails
92
+ def execute_single(hook_def, context)
93
+ proc = hook_def.resolve_proc(@registry)
94
+
95
+ @logger.debug("Executing hook for #{context.event} (agent: #{context.agent_name})")
96
+
97
+ # Execute hook with context as parameter
98
+ # Users can access convenience methods via context parameter:
99
+ # hook(:event) { |ctx| ctx.halt("msg") }
100
+ # This preserves lexical scope and access to surrounding instance variables
101
+ proc.call(context)
102
+ rescue Error
103
+ # Pass through blocking errors
104
+ raise
105
+ rescue StandardError => e
106
+ # Wrap other errors with context and detailed debugging
107
+ hook_name = hook_def.named_hook? ? hook_def.proc : "anonymous"
108
+
109
+ # Log detailed error info for debugging
110
+ @logger.error("=" * 80)
111
+ @logger.error("HOOK EXECUTION ERROR")
112
+ @logger.error(" Hook: #{hook_name}")
113
+ @logger.error(" Event: #{context.event}")
114
+ @logger.error(" Agent: #{context.agent_name}")
115
+ @logger.error(" Proc class: #{proc.class}")
116
+ @logger.error(" Proc arity: #{proc.arity} (expected: 1 for |context|)")
117
+ @logger.error(" Error: #{e.class.name}: #{e.message}")
118
+ @logger.error(" Backtrace:")
119
+ e.backtrace.first(15).each { |line| @logger.error(" #{line}") }
120
+ @logger.error("=" * 80)
121
+
122
+ raise Error.new(
123
+ "Hook #{hook_name} failed: #{e.message}",
124
+ hook_name: hook_name,
125
+ context: context,
126
+ )
127
+ end
128
+
129
+ # Execute hooks and return result safely (without raising)
130
+ #
131
+ # This is a convenience method that catches Error and converts it
132
+ # to a halt result, making it easier to use in control flow.
133
+ #
134
+ # @param event [Symbol] Event type
135
+ # @param context [Context] Context to pass to hooks
136
+ # @param callbacks [Array<Definition>] Agent-specific hooks
137
+ # @return [Result] Result from hooks
138
+ def execute_safe(event:, context:, callbacks: [])
139
+ execute(event: event, context: context, callbacks: callbacks)
140
+ rescue Error => e
141
+ @logger.warn("Execution blocked by hook: #{e.message}")
142
+ Result.halt(e.message)
143
+ end
144
+ end
145
+ end
146
+ end
@@ -0,0 +1,143 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module Hooks
5
+ # Central registry for managing named hooks and swarm-level defaults
6
+ #
7
+ # The Registry stores:
8
+ # - Named hooks that can be referenced by symbol in YAML or code
9
+ # - Swarm-level default hooks that apply to all agents
10
+ #
11
+ # @example Register a named hook
12
+ # registry = SwarmSDK::Hooks::Registry.new
13
+ # registry.register(:validate_code) do |context|
14
+ # raise SwarmSDK::Hooks::Error, "Invalid code" unless valid?(context.tool_call)
15
+ # end
16
+ #
17
+ # @example Add swarm-level default
18
+ # registry.add_default(:pre_tool_use, matcher: "Write|Edit") do |context|
19
+ # puts "Tool #{context.tool_call.name} called by #{context.agent_name}"
20
+ # end
21
+ class Registry
22
+ # Available hook event types
23
+ VALID_EVENTS = [
24
+ # Swarm lifecycle events
25
+ :swarm_start, # When Swarm.execute is called (before first user message)
26
+ :swarm_stop, # When Swarm.execute completes (after execution)
27
+ :first_message, # When first user message is sent to swarm (Swarm.execute)
28
+
29
+ # Agent/LLM events
30
+ :user_prompt, # Before sending user message to LLM
31
+ :agent_step, # After agent makes intermediate response with tool calls
32
+ :agent_stop, # After agent completes with final response (no more tool calls)
33
+
34
+ # Tool events
35
+ :pre_tool_use, # Before tool execution (can block/modify)
36
+ :post_tool_use, # After tool execution
37
+
38
+ # Delegation events
39
+ :pre_delegation, # Before delegating to another agent
40
+ :post_delegation, # After delegation completes
41
+
42
+ # Context events
43
+ :context_warning, # When context usage crosses threshold
44
+ ].freeze
45
+
46
+ def initialize
47
+ @named_hooks = {} # { hook_name: proc }
48
+ @defaults = Hash.new { |h, k| h[k] = [] } # { event_type: [Definition, ...] }
49
+ end
50
+
51
+ # Register a named hook that can be referenced elsewhere
52
+ #
53
+ # @param name [Symbol] Unique name for this hook
54
+ # @param block [Proc] Hook implementation
55
+ # @raise [ArgumentError] if name already registered or invalid
56
+ #
57
+ # @example
58
+ # registry.register(:log_tool_use) { |ctx| puts "Tool: #{ctx.tool_call.name}" }
59
+ def register(name, &block)
60
+ raise ArgumentError, "Hook name must be a symbol" unless name.is_a?(Symbol)
61
+ raise ArgumentError, "Hook #{name} already registered" if @named_hooks.key?(name)
62
+ raise ArgumentError, "Block required" unless block
63
+
64
+ @named_hooks[name] = block
65
+ end
66
+
67
+ # Get a named hook by symbol
68
+ #
69
+ # @param name [Symbol] Hook name
70
+ # @return [Proc, nil] The hook proc or nil if not found
71
+ def get(name)
72
+ @named_hooks[name]
73
+ end
74
+
75
+ # Add a swarm-level default hook
76
+ #
77
+ # These hooks apply to all agents unless overridden at agent level.
78
+ #
79
+ # @param event [Symbol] Event type (must be in VALID_EVENTS)
80
+ # @param matcher [String, Regexp, nil] Optional regex pattern to match tool names
81
+ # @param priority [Integer] Execution priority (higher runs first)
82
+ # @param block [Proc] Hook implementation
83
+ # @raise [ArgumentError] if event invalid or block missing
84
+ #
85
+ # @example Add default logging
86
+ # registry.add_default(:pre_tool_use) { |ctx| log(ctx) }
87
+ #
88
+ # @example Add validation for specific tools
89
+ # registry.add_default(:pre_tool_use, matcher: "Write|Edit") do |ctx|
90
+ # validate_tool_call(ctx.tool_call)
91
+ # end
92
+ def add_default(event, matcher: nil, priority: 0, &block)
93
+ validate_event!(event)
94
+ raise ArgumentError, "Block required" unless block
95
+
96
+ definition = Definition.new(
97
+ event: event,
98
+ matcher: matcher,
99
+ priority: priority,
100
+ proc: block,
101
+ )
102
+
103
+ @defaults[event] << definition
104
+ @defaults[event].sort_by! { |d| -d.priority } # Higher priority first
105
+ end
106
+
107
+ # Get all default hooks for an event type
108
+ #
109
+ # @param event [Symbol] Event type
110
+ # @return [Array<Definition>] List of hook definitions
111
+ def get_defaults(event)
112
+ @defaults[event]
113
+ end
114
+
115
+ # Get all registered named hook names
116
+ #
117
+ # @return [Array<Symbol>] List of hook names
118
+ def named_hooks
119
+ @named_hooks.keys
120
+ end
121
+
122
+ # Check if a hook name is registered
123
+ #
124
+ # @param name [Symbol] Hook name
125
+ # @return [Boolean] true if registered
126
+ def registered?(name)
127
+ @named_hooks.key?(name)
128
+ end
129
+
130
+ private
131
+
132
+ # Validate event type
133
+ #
134
+ # @param event [Symbol] Event to validate
135
+ # @raise [ArgumentError] if event invalid
136
+ def validate_event!(event)
137
+ return if VALID_EVENTS.include?(event)
138
+
139
+ raise ArgumentError, "Invalid event type: #{event}. Valid types: #{VALID_EVENTS.join(", ")}"
140
+ end
141
+ end
142
+ end
143
+ end
@@ -0,0 +1,150 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ module Hooks
5
+ # Result object returned by hooks to control execution flow
6
+ #
7
+ # Hooks can return a Result to:
8
+ # - Continue normal execution (default)
9
+ # - Halt execution and return an error/message
10
+ # - Replace a value (e.g., tool result, delegation result)
11
+ # - Reprompt the agent with a new prompt
12
+ # - Finish the current agent's execution (agent scope)
13
+ # - Finish the entire swarm execution (swarm scope)
14
+ #
15
+ # @example Continue normal execution (default)
16
+ # SwarmSDK::Hooks::Result.continue
17
+ #
18
+ # @example Halt execution with error message
19
+ # SwarmSDK::Hooks::Result.halt("Validation failed: invalid input")
20
+ #
21
+ # @example Replace tool result
22
+ # SwarmSDK::Hooks::Result.replace("Custom tool result")
23
+ #
24
+ # @example Reprompt agent with modified prompt
25
+ # SwarmSDK::Hooks::Result.reprompt("Modified task: #{original_task}")
26
+ #
27
+ # @example Finish current agent with message
28
+ # SwarmSDK::Hooks::Result.finish_agent("Agent task completed")
29
+ #
30
+ # @example Finish entire swarm with message
31
+ # SwarmSDK::Hooks::Result.finish_swarm("All tasks complete!")
32
+ class Result
33
+ attr_reader :action, :value
34
+
35
+ # Valid actions that control execution flow
36
+ VALID_ACTIONS = [:continue, :halt, :replace, :reprompt, :finish_agent, :finish_swarm].freeze
37
+
38
+ # @param action [Symbol] Action to take (:continue, :halt, :replace, :reprompt)
39
+ # @param value [Object, nil] Associated value (context for :continue, message for :halt, etc.)
40
+ def initialize(action:, value: nil)
41
+ unless VALID_ACTIONS.include?(action)
42
+ raise ArgumentError, "Invalid action: #{action}. Valid actions: #{VALID_ACTIONS.join(", ")}"
43
+ end
44
+
45
+ @action = action
46
+ @value = value
47
+ end
48
+
49
+ # Check if this result indicates halting execution
50
+ #
51
+ # @return [Boolean] true if action is :halt
52
+ def halt?
53
+ @action == :halt
54
+ end
55
+
56
+ # Check if this result provides a replacement value
57
+ #
58
+ # @return [Boolean] true if action is :replace
59
+ def replace?
60
+ @action == :replace
61
+ end
62
+
63
+ # Check if this result requests reprompting
64
+ #
65
+ # @return [Boolean] true if action is :reprompt
66
+ def reprompt?
67
+ @action == :reprompt
68
+ end
69
+
70
+ # Check if this result continues normal execution
71
+ #
72
+ # @return [Boolean] true if action is :continue
73
+ def continue?
74
+ @action == :continue
75
+ end
76
+
77
+ # Check if this result finishes the current agent
78
+ #
79
+ # @return [Boolean] true if action is :finish_agent
80
+ def finish_agent?
81
+ @action == :finish_agent
82
+ end
83
+
84
+ # Check if this result finishes the entire swarm
85
+ #
86
+ # @return [Boolean] true if action is :finish_swarm
87
+ def finish_swarm?
88
+ @action == :finish_swarm
89
+ end
90
+
91
+ class << self
92
+ # Create a result that continues normal execution
93
+ #
94
+ # @param context [Context, nil] Updated context (optional)
95
+ # @return [Result] Result with continue action
96
+ def continue(context = nil)
97
+ new(action: :continue, value: context)
98
+ end
99
+
100
+ # Create a result that halts execution
101
+ #
102
+ # @param message [String] Error or halt message
103
+ # @return [Result] Result with halt action
104
+ def halt(message)
105
+ new(action: :halt, value: message)
106
+ end
107
+
108
+ # Create a result that replaces a value
109
+ #
110
+ # @param value [Object] Replacement value
111
+ # @return [Result] Result with replace action
112
+ def replace(value)
113
+ new(action: :replace, value: value)
114
+ end
115
+
116
+ # Create a result that reprompts the agent
117
+ #
118
+ # @param prompt [String] New prompt to send to agent
119
+ # @return [Result] Result with reprompt action
120
+ def reprompt(prompt)
121
+ new(action: :reprompt, value: prompt)
122
+ end
123
+
124
+ # Create a result that finishes the current agent
125
+ #
126
+ # Exits the agent's chat loop and returns the message as the agent's
127
+ # final response. If this agent was delegated to, control returns to
128
+ # the calling agent. The swarm continues if there's more work.
129
+ #
130
+ # @param message [String] Final message from the agent
131
+ # @return [Result] Result with finish_agent action
132
+ def finish_agent(message)
133
+ new(action: :finish_agent, value: message)
134
+ end
135
+
136
+ # Create a result that finishes the entire swarm
137
+ #
138
+ # Immediately exits the Swarm.execute() loop and returns the message
139
+ # as the final Result#output. All agent execution stops and the user
140
+ # receives this message.
141
+ #
142
+ # @param message [String] Final message from the swarm
143
+ # @return [Result] Result with finish_swarm action
144
+ def finish_swarm(message)
145
+ new(action: :finish_swarm, value: message)
146
+ end
147
+ end
148
+ end
149
+ end
150
+ end
@@ -0,0 +1,254 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "open3"
4
+ require "json"
5
+ require "timeout"
6
+
7
+ module SwarmSDK
8
+ module Hooks
9
+ # Executes shell command hooks with JSON I/O and exit code handling
10
+ #
11
+ # ShellExecutor runs external shell commands (defined in YAML hooks) and
12
+ # converts their exit codes to Result objects that control execution flow.
13
+ #
14
+ # ## Exit Code Behavior (following Claude Code convention)
15
+ #
16
+ # - **0**: Success - continue execution (Result.continue)
17
+ # - **2**: Block with error feedback to LLM (Result.halt)
18
+ # - **Other**: Non-blocking error - log warning and continue (Result.continue)
19
+ #
20
+ # ## JSON I/O Format
21
+ #
22
+ # **stdin (to hook script)**:
23
+ # ```json
24
+ # {
25
+ # "event": "pre_tool_use",
26
+ # "agent": "backend",
27
+ # "tool": "Write",
28
+ # "parameters": { "file_path": "api.rb", "content": "..." }
29
+ # }
30
+ # ```
31
+ #
32
+ # **stdout (from hook script)**:
33
+ # ```json
34
+ # {
35
+ # "success": false,
36
+ # "error": "Validation failed: syntax error"
37
+ # }
38
+ # ```
39
+ #
40
+ # @example Execute a validation hook
41
+ # result = SwarmSDK::Hooks::ShellExecutor.execute(
42
+ # command: "python scripts/validate.py",
43
+ # input_json: { event: "pre_tool_use", tool: "Write", parameters: {...} },
44
+ # timeout: 10,
45
+ # agent_name: :backend,
46
+ # swarm_name: "Dev Team"
47
+ # )
48
+ # # => Result (continue or halt based on exit code)
49
+ class ShellExecutor
50
+ DEFAULT_TIMEOUT = 60
51
+
52
+ class << self
53
+ # Execute a shell command hook
54
+ #
55
+ # @param command [String] Shell command to execute
56
+ # @param input_json [Hash] JSON data to provide on stdin
57
+ # @param timeout [Integer] Timeout in seconds (default: 60)
58
+ # @param agent_name [Symbol, String, nil] Agent name for environment variables
59
+ # @param swarm_name [String, nil] Swarm name for environment variables
60
+ # @param event [Symbol] Event type for context-aware behavior
61
+ # @return [Result] Result based on exit code (continue or halt)
62
+ def execute(command:, input_json:, timeout: DEFAULT_TIMEOUT, agent_name: nil, swarm_name: nil, event: nil)
63
+ # Build environment variables
64
+ env = build_environment(agent_name: agent_name, swarm_name: swarm_name)
65
+
66
+ # Execute command with JSON stdin and timeout
67
+ stdout, stderr, status = Timeout.timeout(timeout) do
68
+ Open3.capture3(
69
+ env,
70
+ command,
71
+ stdin_data: JSON.generate(input_json),
72
+ )
73
+ end
74
+
75
+ # Handle exit code per Claude Code convention (context-aware)
76
+ result = handle_exit_code(status.exitstatus, stdout, stderr, event)
77
+
78
+ # Emit log event for hook execution
79
+ case status.exitstatus
80
+ when 0
81
+ # Success - log stdout/stderr
82
+ emit_hook_log(
83
+ event: event,
84
+ agent_name: agent_name,
85
+ command: command,
86
+ exit_code: status.exitstatus,
87
+ success: true,
88
+ stdout: stdout,
89
+ stderr: stderr,
90
+ )
91
+ when 2
92
+ # Blocking error - always log stderr
93
+ emit_hook_log(
94
+ event: event,
95
+ agent_name: agent_name,
96
+ command: command,
97
+ exit_code: status.exitstatus,
98
+ success: false,
99
+ stderr: stderr,
100
+ blocked: true,
101
+ )
102
+ else
103
+ # Non-blocking error - log stderr
104
+ emit_hook_log(
105
+ event: event,
106
+ agent_name: agent_name,
107
+ command: command,
108
+ exit_code: status.exitstatus,
109
+ success: false,
110
+ stderr: stderr,
111
+ blocked: false,
112
+ )
113
+ end
114
+
115
+ result
116
+ rescue Timeout::Error
117
+ emit_hook_log(
118
+ event: event,
119
+ agent_name: agent_name,
120
+ command: command,
121
+ exit_code: nil,
122
+ success: false,
123
+ stderr: "Timeout after #{timeout}s",
124
+ )
125
+ # Don't block on timeout - log and continue
126
+ Result.continue
127
+ rescue StandardError => e
128
+ emit_hook_log(
129
+ event: event,
130
+ agent_name: agent_name,
131
+ command: command,
132
+ exit_code: nil,
133
+ success: false,
134
+ stderr: e.message,
135
+ )
136
+ # Don't block on errors - log and continue
137
+ Result.continue
138
+ end
139
+
140
+ private
141
+
142
+ # Build environment variables for hook execution
143
+ #
144
+ # @param agent_name [Symbol, String, nil] Agent name
145
+ # @param swarm_name [String, nil] Swarm name
146
+ # @return [Hash] Environment variables
147
+ def build_environment(agent_name:, swarm_name:)
148
+ {
149
+ "SWARM_SDK_PROJECT_DIR" => Dir.pwd,
150
+ "SWARM_SDK_AGENT_NAME" => agent_name.to_s,
151
+ "SWARM_SDK_SWARM_NAME" => swarm_name.to_s,
152
+ "PATH" => ENV.fetch("PATH", ""),
153
+ }
154
+ end
155
+
156
+ # Handle exit code and return appropriate Result
157
+ #
158
+ # @param exit_code [Integer] Process exit code
159
+ # @param stdout [String] Standard output
160
+ # @param stderr [String] Standard error
161
+ # @param event [Symbol] Hook event type
162
+ # @return [Result] Result based on exit code
163
+ def handle_exit_code(exit_code, stdout, stderr, event)
164
+ case exit_code
165
+ when 0
166
+ # Success - continue execution
167
+ # For user_prompt and swarm_start: return stdout to be shown to agent
168
+ if [:user_prompt, :swarm_start].include?(event) && !stdout.strip.empty?
169
+ # Return Result with stdout that will be appended to prompt
170
+ Result.replace(stdout.strip)
171
+ else
172
+ # Normal success
173
+ Result.continue
174
+ end
175
+ when 2
176
+ # Blocking error - behavior depends on event type
177
+ handle_exit_code_2(event, stdout, stderr)
178
+ else
179
+ # Non-blocking error - continue (stderr logged above)
180
+ Result.continue
181
+ end
182
+ end
183
+
184
+ # Handle exit code 2 (blocking error)
185
+ #
186
+ # @param event [Symbol] Hook event type
187
+ # @param _stdout [String] Standard output (unused)
188
+ # @param stderr [String] Standard error
189
+ # @return [Result] Result based on event type
190
+ def handle_exit_code_2(event, _stdout, stderr)
191
+ error_msg = stderr.strip
192
+
193
+ case event
194
+ when :pre_tool_use
195
+ # Block tool call, show stderr to agent (stderr already logged above)
196
+ Result.halt(error_msg)
197
+ when :post_tool_use
198
+ # Tool already ran, show stderr to agent (stderr already logged above)
199
+ Result.halt(error_msg)
200
+ when :user_prompt
201
+ # Block prompt processing, erase prompt
202
+ # stderr is logged (above) for user to see, but NOT shown to agent
203
+ # Return empty halt (prompt is erased, no message to agent)
204
+ Result.halt("")
205
+ when :agent_stop
206
+ # Block stoppage, show stderr to agent (stderr already logged above)
207
+ Result.halt(error_msg)
208
+ when :context_warning, :swarm_start, :swarm_stop
209
+ # N/A - stderr logged above, don't halt
210
+ Result.continue
211
+ else
212
+ # Default: halt with stderr
213
+ Result.halt(error_msg)
214
+ end
215
+ end
216
+
217
+ # Emit hook execution log entry
218
+ #
219
+ # @param event [Symbol] Hook event type
220
+ # @param agent_name [String, Symbol, nil] Agent name
221
+ # @param command [String] Shell command executed
222
+ # @param exit_code [Integer, nil] Process exit code
223
+ # @param success [Boolean] Whether execution succeeded
224
+ # @param stdout [String, nil] Standard output
225
+ # @param stderr [String, nil] Standard error
226
+ # @param blocked [Boolean] Whether execution was blocked (exit code 2)
227
+ def emit_hook_log(event:, agent_name:, command:, exit_code:, success:, stdout: nil, stderr: nil, blocked: false)
228
+ # Only emit if LogStream is enabled (has emitter)
229
+ return unless LogStream.enabled?
230
+
231
+ log_entry = {
232
+ type: "hook_executed",
233
+ hook_event: event&.to_s, # Which hook event triggered this (pre_tool_use, swarm_start, etc.)
234
+ agent: agent_name,
235
+ command: command,
236
+ exit_code: exit_code,
237
+ success: success,
238
+ }
239
+
240
+ # Add stdout if present (exit code 0)
241
+ log_entry[:stdout] = stdout.strip if stdout && !stdout.strip.empty?
242
+
243
+ # Add stderr if present (any exit code)
244
+ log_entry[:stderr] = stderr.strip if stderr && !stderr.strip.empty?
245
+
246
+ # Add blocked flag for exit code 2
247
+ log_entry[:blocked] = true if blocked
248
+
249
+ LogStream.emit(**log_entry)
250
+ end
251
+ end
252
+ end
253
+ end
254
+ end