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.
- checksums.yaml +4 -4
- data/lib/swarm_sdk/ruby_llm_patches/chat_callbacks_patch.rb +16 -0
- data/lib/swarm_sdk/ruby_llm_patches/init.rb +4 -1
- data/lib/swarm_sdk/v3/agent.rb +1165 -0
- data/lib/swarm_sdk/v3/agent_builder.rb +533 -0
- data/lib/swarm_sdk/v3/agent_definition.rb +330 -0
- data/lib/swarm_sdk/v3/configuration.rb +490 -0
- data/lib/swarm_sdk/v3/debug_log.rb +86 -0
- data/lib/swarm_sdk/v3/event_stream.rb +130 -0
- data/lib/swarm_sdk/v3/hooks/context.rb +112 -0
- data/lib/swarm_sdk/v3/hooks/result.rb +115 -0
- data/lib/swarm_sdk/v3/hooks/runner.rb +128 -0
- data/lib/swarm_sdk/v3/mcp/connector.rb +183 -0
- data/lib/swarm_sdk/v3/mcp/mcp_error.rb +15 -0
- data/lib/swarm_sdk/v3/mcp/server_definition.rb +125 -0
- data/lib/swarm_sdk/v3/mcp/ssl_http_transport.rb +103 -0
- data/lib/swarm_sdk/v3/mcp/stdio_transport.rb +135 -0
- data/lib/swarm_sdk/v3/mcp/tool_proxy.rb +53 -0
- data/lib/swarm_sdk/v3/memory/adapters/base.rb +297 -0
- data/lib/swarm_sdk/v3/memory/adapters/faiss_support.rb +194 -0
- data/lib/swarm_sdk/v3/memory/adapters/filesystem_adapter.rb +212 -0
- data/lib/swarm_sdk/v3/memory/adapters/sqlite_adapter.rb +507 -0
- data/lib/swarm_sdk/v3/memory/adapters/vector_utils.rb +88 -0
- data/lib/swarm_sdk/v3/memory/card.rb +206 -0
- data/lib/swarm_sdk/v3/memory/cluster.rb +146 -0
- data/lib/swarm_sdk/v3/memory/compressor.rb +496 -0
- data/lib/swarm_sdk/v3/memory/consolidator.rb +427 -0
- data/lib/swarm_sdk/v3/memory/context_builder.rb +339 -0
- data/lib/swarm_sdk/v3/memory/edge.rb +105 -0
- data/lib/swarm_sdk/v3/memory/embedder.rb +185 -0
- data/lib/swarm_sdk/v3/memory/exposure_tracker.rb +104 -0
- data/lib/swarm_sdk/v3/memory/ingestion_pipeline.rb +394 -0
- data/lib/swarm_sdk/v3/memory/retriever.rb +289 -0
- data/lib/swarm_sdk/v3/memory/store.rb +489 -0
- data/lib/swarm_sdk/v3/skills/loader.rb +147 -0
- data/lib/swarm_sdk/v3/skills/manifest.rb +45 -0
- data/lib/swarm_sdk/v3/sub_task_agent.rb +248 -0
- data/lib/swarm_sdk/v3/tools/base.rb +80 -0
- data/lib/swarm_sdk/v3/tools/bash.rb +174 -0
- data/lib/swarm_sdk/v3/tools/clock.rb +32 -0
- data/lib/swarm_sdk/v3/tools/document_converters/base.rb +84 -0
- data/lib/swarm_sdk/v3/tools/document_converters/docx_converter.rb +120 -0
- data/lib/swarm_sdk/v3/tools/document_converters/pdf_converter.rb +111 -0
- data/lib/swarm_sdk/v3/tools/document_converters/xlsx_converter.rb +128 -0
- data/lib/swarm_sdk/v3/tools/edit.rb +111 -0
- data/lib/swarm_sdk/v3/tools/glob.rb +96 -0
- data/lib/swarm_sdk/v3/tools/grep.rb +200 -0
- data/lib/swarm_sdk/v3/tools/message_teammate.rb +15 -0
- data/lib/swarm_sdk/v3/tools/message_user.rb +15 -0
- data/lib/swarm_sdk/v3/tools/read.rb +213 -0
- data/lib/swarm_sdk/v3/tools/read_tracker.rb +40 -0
- data/lib/swarm_sdk/v3/tools/registry.rb +208 -0
- data/lib/swarm_sdk/v3/tools/sub_task.rb +183 -0
- data/lib/swarm_sdk/v3/tools/think.rb +88 -0
- data/lib/swarm_sdk/v3/tools/write.rb +87 -0
- data/lib/swarm_sdk/v3.rb +145 -0
- metadata +88 -149
- data/lib/swarm_sdk/agent/RETRY_LOGIC.md +0 -175
- data/lib/swarm_sdk/agent/builder.rb +0 -705
- data/lib/swarm_sdk/agent/chat.rb +0 -1438
- data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +0 -375
- data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +0 -204
- data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +0 -480
- data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +0 -85
- data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +0 -290
- data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +0 -116
- data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +0 -83
- data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +0 -134
- data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +0 -79
- data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +0 -146
- data/lib/swarm_sdk/agent/context.rb +0 -115
- data/lib/swarm_sdk/agent/context_manager.rb +0 -315
- data/lib/swarm_sdk/agent/definition.rb +0 -588
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +0 -226
- data/lib/swarm_sdk/agent/system_prompt_builder.rb +0 -173
- data/lib/swarm_sdk/agent/tool_registry.rb +0 -189
- data/lib/swarm_sdk/agent_registry.rb +0 -146
- data/lib/swarm_sdk/builders/base_builder.rb +0 -558
- data/lib/swarm_sdk/claude_code_agent_adapter.rb +0 -205
- data/lib/swarm_sdk/concerns/cleanupable.rb +0 -42
- data/lib/swarm_sdk/concerns/snapshotable.rb +0 -67
- data/lib/swarm_sdk/concerns/validatable.rb +0 -55
- data/lib/swarm_sdk/config.rb +0 -368
- data/lib/swarm_sdk/configuration/parser.rb +0 -397
- data/lib/swarm_sdk/configuration/translator.rb +0 -285
- data/lib/swarm_sdk/configuration.rb +0 -165
- data/lib/swarm_sdk/context_compactor/metrics.rb +0 -147
- data/lib/swarm_sdk/context_compactor/token_counter.rb +0 -102
- data/lib/swarm_sdk/context_compactor.rb +0 -335
- data/lib/swarm_sdk/context_management/builder.rb +0 -128
- data/lib/swarm_sdk/context_management/context.rb +0 -328
- data/lib/swarm_sdk/custom_tool_registry.rb +0 -226
- data/lib/swarm_sdk/defaults.rb +0 -251
- data/lib/swarm_sdk/events_to_messages.rb +0 -199
- data/lib/swarm_sdk/hooks/adapter.rb +0 -359
- data/lib/swarm_sdk/hooks/context.rb +0 -197
- data/lib/swarm_sdk/hooks/definition.rb +0 -80
- data/lib/swarm_sdk/hooks/error.rb +0 -29
- data/lib/swarm_sdk/hooks/executor.rb +0 -146
- data/lib/swarm_sdk/hooks/registry.rb +0 -147
- data/lib/swarm_sdk/hooks/result.rb +0 -150
- data/lib/swarm_sdk/hooks/shell_executor.rb +0 -256
- data/lib/swarm_sdk/hooks/tool_call.rb +0 -35
- data/lib/swarm_sdk/hooks/tool_result.rb +0 -62
- data/lib/swarm_sdk/log_collector.rb +0 -227
- data/lib/swarm_sdk/log_stream.rb +0 -127
- data/lib/swarm_sdk/markdown_parser.rb +0 -75
- data/lib/swarm_sdk/model_aliases.json +0 -8
- data/lib/swarm_sdk/models.json +0 -44002
- data/lib/swarm_sdk/models.rb +0 -161
- data/lib/swarm_sdk/node_context.rb +0 -245
- data/lib/swarm_sdk/observer/builder.rb +0 -81
- data/lib/swarm_sdk/observer/config.rb +0 -45
- data/lib/swarm_sdk/observer/manager.rb +0 -248
- data/lib/swarm_sdk/patterns/agent_observer.rb +0 -160
- data/lib/swarm_sdk/permissions/config.rb +0 -239
- data/lib/swarm_sdk/permissions/error_formatter.rb +0 -121
- data/lib/swarm_sdk/permissions/path_matcher.rb +0 -35
- data/lib/swarm_sdk/permissions/validator.rb +0 -173
- data/lib/swarm_sdk/permissions_builder.rb +0 -122
- data/lib/swarm_sdk/plugin.rb +0 -309
- data/lib/swarm_sdk/plugin_registry.rb +0 -101
- data/lib/swarm_sdk/proc_helpers.rb +0 -53
- data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -119
- data/lib/swarm_sdk/restore_result.rb +0 -65
- data/lib/swarm_sdk/result.rb +0 -241
- data/lib/swarm_sdk/snapshot.rb +0 -156
- data/lib/swarm_sdk/snapshot_from_events.rb +0 -397
- data/lib/swarm_sdk/state_restorer.rb +0 -476
- data/lib/swarm_sdk/state_snapshot.rb +0 -334
- data/lib/swarm_sdk/swarm/agent_initializer.rb +0 -648
- data/lib/swarm_sdk/swarm/all_agents_builder.rb +0 -204
- data/lib/swarm_sdk/swarm/builder.rb +0 -256
- data/lib/swarm_sdk/swarm/executor.rb +0 -446
- data/lib/swarm_sdk/swarm/hook_triggers.rb +0 -162
- data/lib/swarm_sdk/swarm/lazy_delegate_chat.rb +0 -372
- data/lib/swarm_sdk/swarm/logging_callbacks.rb +0 -361
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +0 -290
- data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +0 -67
- data/lib/swarm_sdk/swarm/tool_configurator.rb +0 -392
- data/lib/swarm_sdk/swarm.rb +0 -973
- data/lib/swarm_sdk/swarm_loader.rb +0 -145
- data/lib/swarm_sdk/swarm_registry.rb +0 -136
- data/lib/swarm_sdk/tools/base.rb +0 -63
- data/lib/swarm_sdk/tools/bash.rb +0 -280
- data/lib/swarm_sdk/tools/clock.rb +0 -46
- data/lib/swarm_sdk/tools/delegate.rb +0 -389
- data/lib/swarm_sdk/tools/document_converters/base_converter.rb +0 -83
- data/lib/swarm_sdk/tools/document_converters/docx_converter.rb +0 -99
- data/lib/swarm_sdk/tools/document_converters/html_converter.rb +0 -101
- data/lib/swarm_sdk/tools/document_converters/pdf_converter.rb +0 -78
- data/lib/swarm_sdk/tools/document_converters/xlsx_converter.rb +0 -194
- data/lib/swarm_sdk/tools/edit.rb +0 -145
- data/lib/swarm_sdk/tools/glob.rb +0 -166
- data/lib/swarm_sdk/tools/grep.rb +0 -235
- data/lib/swarm_sdk/tools/image_extractors/docx_image_extractor.rb +0 -43
- data/lib/swarm_sdk/tools/image_extractors/pdf_image_extractor.rb +0 -167
- data/lib/swarm_sdk/tools/image_formats/tiff_builder.rb +0 -65
- data/lib/swarm_sdk/tools/mcp_tool_stub.rb +0 -198
- data/lib/swarm_sdk/tools/multi_edit.rb +0 -236
- data/lib/swarm_sdk/tools/path_resolver.rb +0 -92
- data/lib/swarm_sdk/tools/read.rb +0 -261
- data/lib/swarm_sdk/tools/registry.rb +0 -205
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +0 -117
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +0 -97
- data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +0 -108
- data/lib/swarm_sdk/tools/stores/read_tracker.rb +0 -96
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +0 -273
- data/lib/swarm_sdk/tools/stores/storage.rb +0 -142
- data/lib/swarm_sdk/tools/stores/todo_manager.rb +0 -65
- data/lib/swarm_sdk/tools/think.rb +0 -100
- data/lib/swarm_sdk/tools/todo_write.rb +0 -237
- data/lib/swarm_sdk/tools/web_fetch.rb +0 -264
- data/lib/swarm_sdk/tools/write.rb +0 -112
- data/lib/swarm_sdk/transcript_builder.rb +0 -278
- data/lib/swarm_sdk/utils.rb +0 -68
- data/lib/swarm_sdk/validation_result.rb +0 -33
- data/lib/swarm_sdk/version.rb +0 -5
- data/lib/swarm_sdk/workflow/agent_config.rb +0 -95
- data/lib/swarm_sdk/workflow/builder.rb +0 -227
- data/lib/swarm_sdk/workflow/executor.rb +0 -497
- data/lib/swarm_sdk/workflow/node_builder.rb +0 -593
- data/lib/swarm_sdk/workflow/transformer_executor.rb +0 -250
- data/lib/swarm_sdk/workflow.rb +0 -589
- 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
|