swarm_sdk 2.7.14 → 3.0.0.alpha2

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 (185) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_sdk/ruby_llm_patches/chat_callbacks_patch.rb +16 -0
  3. data/lib/swarm_sdk/ruby_llm_patches/init.rb +4 -1
  4. data/lib/swarm_sdk/v3/agent.rb +1165 -0
  5. data/lib/swarm_sdk/v3/agent_builder.rb +533 -0
  6. data/lib/swarm_sdk/v3/agent_definition.rb +330 -0
  7. data/lib/swarm_sdk/v3/configuration.rb +490 -0
  8. data/lib/swarm_sdk/v3/debug_log.rb +86 -0
  9. data/lib/swarm_sdk/v3/event_stream.rb +130 -0
  10. data/lib/swarm_sdk/v3/hooks/context.rb +112 -0
  11. data/lib/swarm_sdk/v3/hooks/result.rb +115 -0
  12. data/lib/swarm_sdk/v3/hooks/runner.rb +128 -0
  13. data/lib/swarm_sdk/v3/mcp/connector.rb +183 -0
  14. data/lib/swarm_sdk/v3/mcp/mcp_error.rb +15 -0
  15. data/lib/swarm_sdk/v3/mcp/server_definition.rb +125 -0
  16. data/lib/swarm_sdk/v3/mcp/ssl_http_transport.rb +103 -0
  17. data/lib/swarm_sdk/v3/mcp/stdio_transport.rb +135 -0
  18. data/lib/swarm_sdk/v3/mcp/tool_proxy.rb +53 -0
  19. data/lib/swarm_sdk/v3/memory/adapters/base.rb +297 -0
  20. data/lib/swarm_sdk/v3/memory/adapters/faiss_support.rb +194 -0
  21. data/lib/swarm_sdk/v3/memory/adapters/filesystem_adapter.rb +212 -0
  22. data/lib/swarm_sdk/v3/memory/adapters/sqlite_adapter.rb +507 -0
  23. data/lib/swarm_sdk/v3/memory/adapters/vector_utils.rb +88 -0
  24. data/lib/swarm_sdk/v3/memory/card.rb +206 -0
  25. data/lib/swarm_sdk/v3/memory/cluster.rb +146 -0
  26. data/lib/swarm_sdk/v3/memory/compressor.rb +496 -0
  27. data/lib/swarm_sdk/v3/memory/consolidator.rb +427 -0
  28. data/lib/swarm_sdk/v3/memory/context_builder.rb +339 -0
  29. data/lib/swarm_sdk/v3/memory/edge.rb +105 -0
  30. data/lib/swarm_sdk/v3/memory/embedder.rb +185 -0
  31. data/lib/swarm_sdk/v3/memory/exposure_tracker.rb +104 -0
  32. data/lib/swarm_sdk/v3/memory/ingestion_pipeline.rb +394 -0
  33. data/lib/swarm_sdk/v3/memory/retriever.rb +289 -0
  34. data/lib/swarm_sdk/v3/memory/store.rb +489 -0
  35. data/lib/swarm_sdk/v3/skills/loader.rb +147 -0
  36. data/lib/swarm_sdk/v3/skills/manifest.rb +45 -0
  37. data/lib/swarm_sdk/v3/sub_task_agent.rb +248 -0
  38. data/lib/swarm_sdk/v3/tools/base.rb +80 -0
  39. data/lib/swarm_sdk/v3/tools/bash.rb +174 -0
  40. data/lib/swarm_sdk/v3/tools/clock.rb +32 -0
  41. data/lib/swarm_sdk/v3/tools/document_converters/base.rb +84 -0
  42. data/lib/swarm_sdk/v3/tools/document_converters/docx_converter.rb +120 -0
  43. data/lib/swarm_sdk/v3/tools/document_converters/pdf_converter.rb +111 -0
  44. data/lib/swarm_sdk/v3/tools/document_converters/xlsx_converter.rb +128 -0
  45. data/lib/swarm_sdk/v3/tools/edit.rb +111 -0
  46. data/lib/swarm_sdk/v3/tools/glob.rb +96 -0
  47. data/lib/swarm_sdk/v3/tools/grep.rb +200 -0
  48. data/lib/swarm_sdk/v3/tools/message_teammate.rb +15 -0
  49. data/lib/swarm_sdk/v3/tools/message_user.rb +15 -0
  50. data/lib/swarm_sdk/v3/tools/read.rb +213 -0
  51. data/lib/swarm_sdk/v3/tools/read_tracker.rb +40 -0
  52. data/lib/swarm_sdk/v3/tools/registry.rb +208 -0
  53. data/lib/swarm_sdk/v3/tools/sub_task.rb +183 -0
  54. data/lib/swarm_sdk/v3/tools/think.rb +88 -0
  55. data/lib/swarm_sdk/v3/tools/write.rb +87 -0
  56. data/lib/swarm_sdk/v3.rb +145 -0
  57. metadata +88 -149
  58. data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -175
  59. data/lib/swarm_sdk/agent/builder.rb +0 -705
  60. data/lib/swarm_sdk/agent/chat.rb +0 -1438
  61. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -375
  62. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
  63. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
  64. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -85
  65. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -290
  66. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
  67. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
  68. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -134
  69. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
  70. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -146
  71. data/lib/swarm_sdk/agent/context.rb +0 -115
  72. data/lib/swarm_sdk/agent/context_manager.rb +0 -315
  73. data/lib/swarm_sdk/agent/definition.rb +0 -588
  74. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -226
  75. data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -173
  76. data/lib/swarm_sdk/agent/tool_registry.rb +0 -189
  77. data/lib/swarm_sdk/agent_registry.rb +0 -146
  78. data/lib/swarm_sdk/builders/base_builder.rb +0 -558
  79. data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
  80. data/lib/swarm_sdk/concerns/cleanupable.rb +0 -42
  81. data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
  82. data/lib/swarm_sdk/concerns/validatable.rb +0 -55
  83. data/lib/swarm_sdk/config.rb +0 -368
  84. data/lib/swarm_sdk/configuration/parser.rb +0 -397
  85. data/lib/swarm_sdk/configuration/translator.rb +0 -285
  86. data/lib/swarm_sdk/configuration.rb +0 -165
  87. data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
  88. data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -102
  89. data/lib/swarm_sdk/context_compactor.rb +0 -335
  90. data/lib/swarm_sdk/context_management/builder.rb +0 -128
  91. data/lib/swarm_sdk/context_management/context.rb +0 -328
  92. data/lib/swarm_sdk/custom_tool_registry.rb +0 -226
  93. data/lib/swarm_sdk/defaults.rb +0 -251
  94. data/lib/swarm_sdk/events_to_messages.rb +0 -199
  95. data/lib/swarm_sdk/hooks/adapter.rb +0 -359
  96. data/lib/swarm_sdk/hooks/context.rb +0 -197
  97. data/lib/swarm_sdk/hooks/definition.rb +0 -80
  98. data/lib/swarm_sdk/hooks/error.rb +0 -29
  99. data/lib/swarm_sdk/hooks/executor.rb +0 -146
  100. data/lib/swarm_sdk/hooks/registry.rb +0 -147
  101. data/lib/swarm_sdk/hooks/result.rb +0 -150
  102. data/lib/swarm_sdk/hooks/shell_executor.rb +0 -256
  103. data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
  104. data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
  105. data/lib/swarm_sdk/log_collector.rb +0 -227
  106. data/lib/swarm_sdk/log_stream.rb +0 -127
  107. data/lib/swarm_sdk/markdown_parser.rb +0 -75
  108. data/lib/swarm_sdk/model_aliases.json +0 -8
  109. data/lib/swarm_sdk/models.json +0 -44002
  110. data/lib/swarm_sdk/models.rb +0 -161
  111. data/lib/swarm_sdk/node_context.rb +0 -245
  112. data/lib/swarm_sdk/observer/builder.rb +0 -81
  113. data/lib/swarm_sdk/observer/config.rb +0 -45
  114. data/lib/swarm_sdk/observer/manager.rb +0 -248
  115. data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
  116. data/lib/swarm_sdk/permissions/config.rb +0 -239
  117. data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
  118. data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
  119. data/lib/swarm_sdk/permissions/validator.rb +0 -173
  120. data/lib/swarm_sdk/permissions_builder.rb +0 -122
  121. data/lib/swarm_sdk/plugin.rb +0 -309
  122. data/lib/swarm_sdk/plugin_registry.rb +0 -101
  123. data/lib/swarm_sdk/proc_helpers.rb +0 -53
  124. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -119
  125. data/lib/swarm_sdk/restore_result.rb +0 -65
  126. data/lib/swarm_sdk/result.rb +0 -241
  127. data/lib/swarm_sdk/snapshot.rb +0 -156
  128. data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
  129. data/lib/swarm_sdk/state_restorer.rb +0 -476
  130. data/lib/swarm_sdk/state_snapshot.rb +0 -334
  131. data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -648
  132. data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -204
  133. data/lib/swarm_sdk/swarm/builder.rb +0 -256
  134. data/lib/swarm_sdk/swarm/executor.rb +0 -446
  135. data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -162
  136. data/lib/swarm_sdk/swarm/lazy_delegate_chat.rb +0 -372
  137. data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -361
  138. data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -290
  139. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
  140. data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -392
  141. data/lib/swarm_sdk/swarm.rb +0 -973
  142. data/lib/swarm_sdk/swarm_loader.rb +0 -145
  143. data/lib/swarm_sdk/swarm_registry.rb +0 -136
  144. data/lib/swarm_sdk/tools/base.rb +0 -63
  145. data/lib/swarm_sdk/tools/bash.rb +0 -280
  146. data/lib/swarm_sdk/tools/clock.rb +0 -46
  147. data/lib/swarm_sdk/tools/delegate.rb +0 -389
  148. data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
  149. data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
  150. data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
  151. data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
  152. data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
  153. data/lib/swarm_sdk/tools/edit.rb +0 -145
  154. data/lib/swarm_sdk/tools/glob.rb +0 -166
  155. data/lib/swarm_sdk/tools/grep.rb +0 -235
  156. data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
  157. data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -167
  158. data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
  159. data/lib/swarm_sdk/tools/mcp_tool_stub.rb +0 -198
  160. data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
  161. data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
  162. data/lib/swarm_sdk/tools/read.rb +0 -261
  163. data/lib/swarm_sdk/tools/registry.rb +0 -205
  164. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
  165. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
  166. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
  167. data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
  168. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -273
  169. data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
  170. data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
  171. data/lib/swarm_sdk/tools/think.rb +0 -100
  172. data/lib/swarm_sdk/tools/todo_write.rb +0 -237
  173. data/lib/swarm_sdk/tools/web_fetch.rb +0 -264
  174. data/lib/swarm_sdk/tools/write.rb +0 -112
  175. data/lib/swarm_sdk/transcript_builder.rb +0 -278
  176. data/lib/swarm_sdk/utils.rb +0 -68
  177. data/lib/swarm_sdk/validation_result.rb +0 -33
  178. data/lib/swarm_sdk/version.rb +0 -5
  179. data/lib/swarm_sdk/workflow/agent_config.rb +0 -95
  180. data/lib/swarm_sdk/workflow/builder.rb +0 -227
  181. data/lib/swarm_sdk/workflow/executor.rb +0 -497
  182. data/lib/swarm_sdk/workflow/node_builder.rb +0 -593
  183. data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -250
  184. data/lib/swarm_sdk/workflow.rb +0 -589
  185. data/lib/swarm_sdk.rb +0 -721
@@ -1,446 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- class Swarm
5
- # Handles swarm execution orchestration
6
- #
7
- # Extracted from Swarm#execute to reduce complexity and eliminate code duplication.
8
- # The core execution loop, error handling, and cleanup logic are unified here.
9
- #
10
- # ## Stop Mechanism
11
- #
12
- # Supports hard-stop via `swarm.stop` using IO.pipe for thread-safe signaling:
13
- # 1. `swarm.stop` writes to pipe and sets `@stop_requested`
14
- # 2. A listener task reads from the pipe (async-aware I/O)
15
- # 3. Listener calls `barrier.stop` within the Async reactor
16
- # 4. All child tasks receive `Async::Stop` exception
17
- # 5. `execute_in_task` catches `Async::Stop`, sets interrupted flag, emits events
18
- class Executor
19
- def initialize(swarm)
20
- @swarm = swarm
21
- @interrupted_result = nil
22
- end
23
-
24
- # Execute the swarm with a prompt
25
- #
26
- # @param prompt [String] User prompt
27
- # @param wait [Boolean] Block until completion (true) or return task (false)
28
- # @param logs [Array] Log collection array
29
- # @param has_logging [Boolean] Whether logging is enabled
30
- # @param original_fiber_storage [Hash] Original Fiber storage values to restore
31
- # @return [Result, Async::Task] Result if wait: true, Async::Task if wait: false
32
- def run(prompt, wait:, logs:, has_logging:, original_fiber_storage:)
33
- @original_fiber_storage = original_fiber_storage
34
- if wait
35
- run_blocking(prompt, logs: logs, has_logging: has_logging)
36
- else
37
- run_async(prompt, logs: logs, has_logging: has_logging)
38
- end
39
- end
40
-
41
- private
42
-
43
- # Blocking execution using Sync
44
- #
45
- # Wraps execution in an Async::Barrier so `swarm.stop` can cancel all tasks.
46
- # A stop listener task watches the IO.pipe for stop signals.
47
- def run_blocking(prompt, logs:, has_logging:)
48
- result = nil
49
- start_time = Time.now
50
- @swarm.prepare_for_execution
51
-
52
- Sync do |task|
53
- barrier = Async::Barrier.new
54
- @swarm.register_execution_barrier(barrier)
55
- stop_listener = setup_stop_listener(task, barrier)
56
-
57
- begin
58
- result = barrier.async do
59
- if @swarm.execution_timeout
60
- execute_with_execution_timeout(task, prompt, logs, has_logging, start_time)
61
- else
62
- execute_in_task(prompt, logs: logs, has_logging: has_logging) do |lead, current_prompt|
63
- lead.ask(current_prompt)
64
- end
65
- end
66
- end.wait
67
-
68
- # barrier child .wait returns nil when stopped
69
- result = @interrupted_result if result.nil? && @swarm.stop_requested?
70
- rescue Async::Stop
71
- # Non-blocking path (rare - user called task.stop on Sync root)
72
- result = @interrupted_result
73
- ensure
74
- barrier.stop unless barrier.empty?
75
- stop_listener&.stop
76
- @swarm.clear_execution_barrier
77
- end
78
- ensure
79
- # Always wait for observer tasks, even if main execution raises
80
- # This is INSIDE Sync block, so async tasks can still complete
81
- @swarm.wait_for_observers
82
- end
83
-
84
- result
85
- ensure
86
- @interrupted_result = nil
87
- @swarm.cleanup_stop_signal
88
- restore_fiber_storage
89
- end
90
-
91
- # Non-blocking execution using parent async task
92
- #
93
- # Same barrier + stop listener pattern as run_blocking.
94
- def run_async(prompt, logs:, has_logging:)
95
- parent = Async::Task.current
96
- raise ConfigurationError, "wait: false requires an async context. Use Sync { swarm.execute(..., wait: false) }" unless parent
97
-
98
- @swarm.prepare_for_execution
99
-
100
- # NOTE: The block receives |task| as the spawned Async::Task when arity > 0
101
- parent.async(finished: false) do |task|
102
- start_time = Time.now
103
- barrier = Async::Barrier.new
104
- @swarm.register_execution_barrier(barrier)
105
- stop_listener = setup_stop_listener(task, barrier)
106
-
107
- begin
108
- result = barrier.async do
109
- if @swarm.execution_timeout
110
- execute_with_execution_timeout(task, prompt, logs, has_logging, start_time)
111
- else
112
- execute_in_task(prompt, logs: logs, has_logging: has_logging) do |lead, current_prompt|
113
- lead.ask(current_prompt)
114
- end
115
- end
116
- end.wait
117
-
118
- result = @interrupted_result if result.nil? && @swarm.stop_requested?
119
- result
120
- rescue Async::Stop
121
- @interrupted_result
122
- ensure
123
- barrier.stop unless barrier.empty?
124
- stop_listener&.stop
125
- @swarm.clear_execution_barrier
126
- @interrupted_result = nil
127
- @swarm.cleanup_stop_signal
128
- @swarm.wait_for_observers
129
- end
130
- end
131
- end
132
-
133
- # Setup a listener task that watches for stop signals via IO.pipe
134
- #
135
- # The listener reads from the pipe (async-aware I/O that yields to scheduler).
136
- # When data arrives (from `swarm.stop`), it stops the barrier to cancel all tasks.
137
- #
138
- # @param task [Async::Task] Parent task to spawn listener under
139
- # @param barrier [Async::Barrier] Execution barrier to stop
140
- # @return [Async::Task, nil] The listener task, or nil if no pipe
141
- def setup_stop_listener(task, barrier)
142
- return unless @swarm.stop_signal_read
143
-
144
- task.async do
145
- @swarm.stop_signal_read.read(1) # Async-aware I/O, yields to scheduler
146
- barrier.stop unless barrier.empty?
147
- rescue IOError, Async::Stop
148
- # Pipe closed or listener stopped - normal cleanup
149
- end
150
- end
151
-
152
- # Core execution logic (unified, no duplication)
153
- #
154
- # Handles InterruptedError and Async::Stop to properly track interruption state.
155
- # The interrupted flag drives cleanup behavior (event emission, result building).
156
- #
157
- # @param prompt [String] Initial prompt
158
- # @param logs [Array] Log collection
159
- # @param has_logging [Boolean] Whether logging is enabled
160
- # @yield [lead, current_prompt] Block to execute LLM call
161
- # @return [Result] Execution result
162
- def execute_in_task(prompt, logs:, has_logging:, &block)
163
- start_time = Time.now
164
- result = nil
165
- swarm_stop_triggered = false
166
- current_prompt = prompt
167
- interrupted = false
168
-
169
- begin
170
- # Notify plugins that swarm is starting
171
- PluginRegistry.emit_event(:on_swarm_started, swarm: @swarm)
172
-
173
- result = execution_loop(current_prompt, logs, start_time, &block)
174
- swarm_stop_triggered = true
175
- rescue ConfigurationError, AgentNotFoundError, ExecutionTimeoutError, TurnTimeoutError
176
- # Re-raise configuration errors and timeouts - these should not be caught here
177
- # Timeouts are handled by execute_with_execution_timeout wrapper
178
- raise
179
- rescue InterruptedError
180
- interrupted = true
181
- raise
182
- rescue Async::Stop
183
- interrupted = true
184
- raise # Must re-raise for Async task cleanup
185
- rescue TypeError => e
186
- result = handle_type_error(e, logs, start_time)
187
- rescue StandardError => e
188
- result = handle_standard_error(e, logs, start_time)
189
- ensure
190
- # Notify plugins that swarm is stopping (called even on error)
191
- PluginRegistry.emit_event(:on_swarm_stopped, swarm: @swarm)
192
-
193
- result = cleanup_after_execution(
194
- result,
195
- start_time,
196
- logs,
197
- swarm_stop_triggered,
198
- has_logging,
199
- interrupted: interrupted,
200
- )
201
- @interrupted_result = result if interrupted
202
- end
203
-
204
- result
205
- end
206
-
207
- # Main execution loop with reprompting support
208
- #
209
- # Checks for stop requests at the top of each iteration to prevent
210
- # unnecessary LLM calls after stop is requested.
211
- def execution_loop(initial_prompt, logs, start_time)
212
- current_prompt = initial_prompt
213
-
214
- loop do
215
- raise InterruptedError, "Swarm execution was interrupted" if @swarm.stop_requested?
216
-
217
- lead = @swarm.agents[@swarm.lead_agent]
218
- response = yield(lead, current_prompt)
219
-
220
- # Check if swarm was finished by a hook (finish_swarm)
221
- if response.is_a?(Hash) && response[:__finish_swarm__]
222
- result = build_result(response[:message], logs, start_time)
223
- @swarm.trigger_swarm_stop(result)
224
- return result
225
- end
226
-
227
- result = build_result(response.content, logs, start_time)
228
-
229
- # Trigger swarm_stop hooks (for reprompt check and event emission)
230
- hook_result = @swarm.trigger_swarm_stop(result)
231
-
232
- # Check if hook requests reprompting
233
- if hook_result&.reprompt?
234
- current_prompt = hook_result.value
235
- # Continue loop with new prompt
236
- else
237
- # Exit loop - execution complete
238
- return result
239
- end
240
- end
241
- end
242
-
243
- # Build a Result object
244
- def build_result(content, logs, start_time)
245
- Result.new(
246
- content: content,
247
- agent: @swarm.lead_agent.to_s,
248
- logs: logs,
249
- duration: Time.now - start_time,
250
- )
251
- end
252
-
253
- # Handle TypeError (e.g., "String does not have #dig method")
254
- def handle_type_error(error, logs, start_time)
255
- if error.message.include?("does not have #dig method")
256
- agent_definition = @swarm.agent_definitions[@swarm.lead_agent]
257
- error_msg = if agent_definition.base_url
258
- "LLM API request failed: The proxy/server at '#{agent_definition.base_url}' returned an invalid response. " \
259
- "This usually means the proxy is unreachable, requires authentication, or returned an error in non-JSON format. " \
260
- "Original error: #{error.message}"
261
- else
262
- "LLM API request failed with unexpected response format. Original error: #{error.message}"
263
- end
264
-
265
- Result.new(
266
- content: nil,
267
- agent: @swarm.lead_agent.to_s,
268
- error: LLMError.new(error_msg),
269
- logs: logs,
270
- duration: Time.now - start_time,
271
- )
272
- else
273
- Result.new(
274
- content: nil,
275
- agent: @swarm.lead_agent.to_s,
276
- error: error,
277
- logs: logs,
278
- duration: Time.now - start_time,
279
- )
280
- end
281
- end
282
-
283
- # Handle StandardError
284
- def handle_standard_error(error, logs, start_time)
285
- Result.new(
286
- content: nil,
287
- agent: @swarm.lead_agent&.to_s || "unknown",
288
- error: error,
289
- logs: logs,
290
- duration: Time.now - start_time,
291
- )
292
- end
293
-
294
- # Cleanup after execution (ensure block logic)
295
- #
296
- # When interrupted, emits agent_stop events for active agents, builds
297
- # an interrupted result, and triggers swarm_stop hook with interrupted context.
298
- #
299
- # @param result [Result, nil] Current execution result
300
- # @param start_time [Time] Execution start time
301
- # @param logs [Array] Collected logs
302
- # @param swarm_stop_triggered [Boolean] Whether swarm_stop hook already fired
303
- # @param has_logging [Boolean] Whether logging is enabled
304
- # @param interrupted [Boolean] Whether execution was interrupted
305
- # @return [Result] Final result (may be replaced with interrupted result)
306
- def cleanup_after_execution(result, start_time, logs, swarm_stop_triggered, has_logging, interrupted: false)
307
- if interrupted && !swarm_stop_triggered
308
- emit_interrupted_agent_events
309
- result = build_interrupted_result(logs, start_time)
310
-
311
- # Trigger swarm_stop hook with interrupted result (emits swarm_stop event)
312
- begin
313
- @swarm.trigger_swarm_stop(result)
314
- rescue StandardError => e
315
- LogStream.emit_error(e, source: "executor", context: "interrupted_swarm_stop")
316
- end
317
- swarm_stop_triggered = true
318
- end
319
-
320
- # Trigger swarm_stop if not already triggered (handles error cases)
321
- unless swarm_stop_triggered
322
- @swarm.trigger_swarm_stop_final(result, start_time, logs)
323
- end
324
-
325
- # Cleanup MCP clients after execution
326
- @swarm.cleanup
327
-
328
- # Cleanup observer subscriptions (matches MCP cleanup pattern)
329
- @swarm.cleanup_observers
330
-
331
- # Restore original Fiber storage (preserves parent context for nested swarms)
332
- restore_fiber_storage
333
-
334
- # Reset logging state for next execution if we set it up
335
- reset_logging if has_logging
336
-
337
- result
338
- end
339
-
340
- # Emit agent_stop events for all agents that were actively executing when interrupted
341
- #
342
- # @return [void]
343
- def emit_interrupted_agent_events
344
- @swarm.active_agent_chats.each do |name, _chat|
345
- LogStream.emit(
346
- type: "agent_stop",
347
- agent: name,
348
- swarm_id: @swarm.swarm_id,
349
- parent_swarm_id: @swarm.parent_swarm_id,
350
- finish_reason: "interrupted",
351
- content: nil,
352
- tool_calls: [],
353
- usage: {},
354
- metadata: { interrupted: true },
355
- )
356
- end
357
- end
358
-
359
- # Build an interrupted result
360
- #
361
- # @param logs [Array] Collected logs
362
- # @param start_time [Time] Execution start time
363
- # @return [Result] Result marked as interrupted
364
- def build_interrupted_result(logs, start_time)
365
- Result.new(
366
- content: nil,
367
- agent: @swarm.lead_agent&.to_s || "unknown",
368
- error: InterruptedError.new("Swarm execution was interrupted"),
369
- logs: logs,
370
- duration: Time.now - start_time,
371
- metadata: { interrupted: true, finish_reason: "interrupted" },
372
- )
373
- end
374
-
375
- # Restore Fiber-local storage to original values (preserves parent context)
376
- def restore_fiber_storage
377
- Fiber[:execution_id] = @original_fiber_storage[:execution_id]
378
- Fiber[:swarm_id] = @original_fiber_storage[:swarm_id]
379
- Fiber[:parent_swarm_id] = @original_fiber_storage[:parent_swarm_id]
380
- end
381
-
382
- # Reset logging state
383
- def reset_logging
384
- LogCollector.reset!
385
- LogStream.reset!
386
- end
387
-
388
- # Execute with execution timeout wrapper
389
- def execute_with_execution_timeout(task, prompt, logs, has_logging, start_time)
390
- # Use Async::Task.current to get the actual current task context
391
- current_task = Async::Task.current || task
392
-
393
- # Use barrier to track ALL child tasks spawned during execution
394
- # This includes RubyLLM's async tool execution (when max_concurrent_tools is set)
395
- barrier = Async::Barrier.new
396
-
397
- begin
398
- current_task.with_timeout(
399
- @swarm.execution_timeout,
400
- ExecutionTimeoutError,
401
- "Swarm execution timed out after #{@swarm.execution_timeout}s",
402
- ) do
403
- # Execute inside barrier to track child tasks (tool executions)
404
- barrier.async do
405
- execute_in_task(prompt, logs: logs, has_logging: has_logging) do |lead, current_prompt|
406
- lead.ask(current_prompt)
407
- end
408
- end.wait
409
- end
410
- rescue ExecutionTimeoutError => e
411
- # Stop ALL child tasks (interrupts ongoing tool executions and delegations)
412
- barrier.stop
413
-
414
- emit_execution_timeout_event(@swarm.execution_timeout)
415
- build_timeout_result(e, logs, Time.now - start_time)
416
- ensure
417
- # Cleanup barrier if not already stopped
418
- barrier.stop unless barrier.empty?
419
- end
420
- end
421
-
422
- # Emit execution timeout event
423
- def emit_execution_timeout_event(limit)
424
- LogStream.emit(
425
- type: "execution_timeout",
426
- swarm_id: @swarm.swarm_id,
427
- parent_swarm_id: @swarm.parent_swarm_id,
428
- limit: limit,
429
- message: "Swarm execution timed out after #{limit}s",
430
- )
431
- end
432
-
433
- # Build timeout result
434
- def build_timeout_result(error, logs, duration)
435
- Result.new(
436
- content: nil,
437
- agent: @swarm.lead_agent&.to_s || "unknown",
438
- error: error,
439
- logs: logs,
440
- duration: duration,
441
- metadata: { timeout: true },
442
- )
443
- end
444
- end
445
- end
446
- end
@@ -1,162 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmSDK
4
- class Swarm
5
- # Hook triggering methods for swarm lifecycle events
6
- #
7
- # Extracted from Swarm to reduce class size and centralize hook execution logic.
8
- # These methods build contexts and execute hooks via the hook registry.
9
- module HookTriggers
10
- # Add a default callback for an event
11
- #
12
- # @param event [Symbol] Event type (:pre_tool_use, :post_tool_use, etc.)
13
- # @param matcher [Hash, nil] Optional matcher to filter events
14
- # @param priority [Integer] Callback priority (higher = later)
15
- # @param block [Proc] Hook implementation
16
- # @return [self]
17
- def add_default_callback(event, matcher: nil, priority: 0, &block)
18
- @hook_registry.add_default(event, matcher: matcher, priority: priority, &block)
19
- self
20
- end
21
-
22
- # Trigger swarm_stop hooks and check for reprompt
23
- #
24
- # @param result [Result] The execution result
25
- # @return [Hooks::Result, nil] Hook result (reprompt action if applicable)
26
- def trigger_swarm_stop(result)
27
- context = build_swarm_stop_context(result)
28
- executor = Hooks::Executor.new(@hook_registry, logger: RubyLLM.logger)
29
- executor.execute_safe(event: :swarm_stop, context: context, callbacks: [])
30
- rescue StandardError => e
31
- LogStream.emit_error(e, source: "hook_triggers", context: "swarm_stop", agent: @lead_agent)
32
- RubyLLM.logger.debug("SwarmSDK: Error in swarm_stop hook: #{e.message}")
33
- nil
34
- end
35
-
36
- # Trigger swarm_stop for final event emission (called in ensure block)
37
- #
38
- # @param result [Result, nil] Execution result
39
- # @param start_time [Time] Execution start time
40
- # @param logs [Array] Collected logs
41
- # @return [void]
42
- def trigger_swarm_stop_final(result, start_time, logs)
43
- result ||= Result.new(
44
- content: nil,
45
- agent: @lead_agent&.to_s || "unknown",
46
- logs: logs,
47
- duration: Time.now - start_time,
48
- error: StandardError.new("Unknown error"),
49
- )
50
-
51
- context = build_swarm_stop_context(result)
52
- executor = Hooks::Executor.new(@hook_registry, logger: RubyLLM.logger)
53
- executor.execute_safe(event: :swarm_stop, context: context, callbacks: [])
54
- rescue StandardError => e
55
- LogStream.emit_error(e, source: "hook_triggers", context: "swarm_stop_final", agent: @lead_agent)
56
- RubyLLM.logger.debug("SwarmSDK: Error in swarm_stop final emission: #{e.message}")
57
- end
58
-
59
- private
60
-
61
- # Build swarm_stop context (DRY - used by both trigger methods)
62
- #
63
- # @param result [Result] Execution result
64
- # @return [Hooks::Context] Hook context for swarm_stop event
65
- def build_swarm_stop_context(result)
66
- finish_reason = if @stop_requested
67
- "interrupted"
68
- elsif result&.error.is_a?(ExecutionTimeoutError)
69
- "timeout"
70
- elsif result&.success?
71
- "finished"
72
- else
73
- "error"
74
- end
75
-
76
- Hooks::Context.new(
77
- event: :swarm_stop,
78
- agent_name: @lead_agent.to_s,
79
- swarm: self,
80
- metadata: {
81
- swarm_name: @name,
82
- lead_agent: @lead_agent,
83
- last_agent: result.agent,
84
- content: result.content,
85
- success: result.success?,
86
- duration: result.duration,
87
- total_cost: result.total_cost,
88
- total_tokens: result.total_tokens,
89
- agents_involved: result.agents_involved,
90
- per_agent_usage: result.per_agent_usage,
91
- result: result,
92
- finish_reason: finish_reason,
93
- timestamp: Time.now.utc.iso8601,
94
- },
95
- )
96
- end
97
-
98
- # Trigger swarm_start hooks when swarm execution begins
99
- #
100
- # @param prompt [String] The user's task prompt
101
- # @return [Hooks::Result, nil] Result with stdout to append (if exit 0) or nil
102
- # @raise [Hooks::Error] If hook halts execution
103
- def trigger_swarm_start(prompt)
104
- context = Hooks::Context.new(
105
- event: :swarm_start,
106
- agent_name: @lead_agent.to_s,
107
- swarm: self,
108
- metadata: {
109
- swarm_name: @name,
110
- lead_agent: @lead_agent,
111
- prompt: prompt,
112
- timestamp: Time.now.utc.iso8601,
113
- },
114
- )
115
-
116
- executor = Hooks::Executor.new(@hook_registry, logger: RubyLLM.logger)
117
- result = executor.execute_safe(event: :swarm_start, context: context, callbacks: [])
118
-
119
- # Halt execution if hook requests it
120
- raise Hooks::Error, "Swarm start halted by hook: #{result.value}" if result.halt?
121
-
122
- # Return result so caller can check for replace (stdout injection)
123
- result
124
- rescue StandardError => e
125
- LogStream.emit_error(e, source: "hook_triggers", context: "swarm_start", agent: @lead_agent)
126
- RubyLLM.logger.debug("SwarmSDK: Error in swarm_start hook: #{e.message}")
127
- raise
128
- end
129
-
130
- # Trigger first_message hooks when first user message is sent
131
- #
132
- # @param prompt [String] The first user message
133
- # @return [void]
134
- # @raise [Hooks::Error] If hook halts execution
135
- def trigger_first_message(prompt)
136
- return if @hook_registry.get_defaults(:first_message).empty?
137
-
138
- context = Hooks::Context.new(
139
- event: :first_message,
140
- agent_name: @lead_agent.to_s,
141
- swarm: self,
142
- metadata: {
143
- swarm_name: @name,
144
- lead_agent: @lead_agent,
145
- prompt: prompt,
146
- timestamp: Time.now.utc.iso8601,
147
- },
148
- )
149
-
150
- executor = Hooks::Executor.new(@hook_registry, logger: RubyLLM.logger)
151
- result = executor.execute_safe(event: :first_message, context: context, callbacks: [])
152
-
153
- # Halt execution if hook requests it
154
- raise Hooks::Error, "First message halted by hook: #{result.value}" if result.halt?
155
- rescue StandardError => e
156
- LogStream.emit_error(e, source: "hook_triggers", context: "first_message", agent: @lead_agent)
157
- RubyLLM.logger.debug("SwarmSDK: Error in first_message hook: #{e.message}")
158
- raise
159
- end
160
- end
161
- end
162
- end