swarm_sdk 2.2.0 → 2.4.0
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/agent/builder.rb +58 -0
- data/lib/swarm_sdk/agent/chat.rb +527 -1059
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/context_tracker.rb +9 -88
- data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +204 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/hook_integration.rb +111 -44
- data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +78 -0
- data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +262 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/logging_helpers.rb +1 -1
- data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +83 -0
- data/lib/swarm_sdk/agent/{chat → chat_helpers}/system_reminder_injector.rb +11 -13
- data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +79 -0
- data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +98 -0
- data/lib/swarm_sdk/agent/context.rb +1 -2
- data/lib/swarm_sdk/agent/definition.rb +66 -154
- data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +4 -2
- data/lib/swarm_sdk/agent/system_prompt_builder.rb +161 -0
- data/lib/swarm_sdk/builders/base_builder.rb +409 -0
- data/lib/swarm_sdk/concerns/cleanupable.rb +39 -0
- data/lib/swarm_sdk/concerns/snapshotable.rb +67 -0
- data/lib/swarm_sdk/concerns/validatable.rb +55 -0
- data/lib/swarm_sdk/config.rb +301 -0
- data/lib/swarm_sdk/configuration/parser.rb +353 -0
- data/lib/swarm_sdk/configuration/translator.rb +255 -0
- data/lib/swarm_sdk/configuration.rb +65 -543
- data/lib/swarm_sdk/context_compactor/token_counter.rb +2 -6
- data/lib/swarm_sdk/context_compactor.rb +6 -11
- data/lib/swarm_sdk/context_management/builder.rb +128 -0
- data/lib/swarm_sdk/context_management/context.rb +328 -0
- data/lib/swarm_sdk/defaults.rb +196 -0
- data/lib/swarm_sdk/events_to_messages.rb +18 -0
- data/lib/swarm_sdk/hooks/adapter.rb +3 -3
- data/lib/swarm_sdk/hooks/shell_executor.rb +4 -2
- data/lib/swarm_sdk/log_collector.rb +179 -29
- data/lib/swarm_sdk/log_stream.rb +29 -0
- data/lib/swarm_sdk/models.json +4333 -1
- data/lib/swarm_sdk/node_context.rb +1 -1
- data/lib/swarm_sdk/observer/builder.rb +81 -0
- data/lib/swarm_sdk/observer/config.rb +45 -0
- data/lib/swarm_sdk/observer/manager.rb +236 -0
- data/lib/swarm_sdk/patterns/agent_observer.rb +160 -0
- data/lib/swarm_sdk/plugin.rb +93 -3
- data/lib/swarm_sdk/snapshot.rb +6 -6
- data/lib/swarm_sdk/snapshot_from_events.rb +13 -2
- data/lib/swarm_sdk/state_restorer.rb +136 -151
- data/lib/swarm_sdk/state_snapshot.rb +65 -100
- data/lib/swarm_sdk/swarm/agent_initializer.rb +180 -136
- data/lib/swarm_sdk/swarm/builder.rb +44 -578
- data/lib/swarm_sdk/swarm/executor.rb +213 -0
- data/lib/swarm_sdk/swarm/hook_triggers.rb +150 -0
- data/lib/swarm_sdk/swarm/logging_callbacks.rb +340 -0
- data/lib/swarm_sdk/swarm/mcp_configurator.rb +7 -4
- data/lib/swarm_sdk/swarm/tool_configurator.rb +44 -140
- data/lib/swarm_sdk/swarm.rb +146 -689
- data/lib/swarm_sdk/tools/bash.rb +14 -8
- data/lib/swarm_sdk/tools/delegate.rb +61 -43
- data/lib/swarm_sdk/tools/edit.rb +8 -13
- data/lib/swarm_sdk/tools/glob.rb +12 -4
- data/lib/swarm_sdk/tools/grep.rb +7 -0
- data/lib/swarm_sdk/tools/multi_edit.rb +15 -11
- data/lib/swarm_sdk/tools/path_resolver.rb +51 -2
- data/lib/swarm_sdk/tools/read.rb +16 -18
- data/lib/swarm_sdk/tools/registry.rb +122 -10
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +9 -5
- data/lib/swarm_sdk/tools/stores/storage.rb +0 -6
- data/lib/swarm_sdk/tools/todo_write.rb +7 -0
- data/lib/swarm_sdk/tools/web_fetch.rb +20 -17
- data/lib/swarm_sdk/tools/write.rb +8 -13
- data/lib/swarm_sdk/version.rb +1 -1
- data/lib/swarm_sdk/{node → workflow}/agent_config.rb +1 -1
- data/lib/swarm_sdk/workflow/builder.rb +143 -0
- data/lib/swarm_sdk/workflow/executor.rb +497 -0
- data/lib/swarm_sdk/{node/builder.rb → workflow/node_builder.rb} +7 -5
- data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +5 -3
- data/lib/swarm_sdk/{node_orchestrator.rb → workflow.rb} +152 -456
- data/lib/swarm_sdk.rb +64 -104
- metadata +68 -15
- data/lib/swarm_sdk/providers/openai_with_responses.rb +0 -589
data/lib/swarm_sdk/swarm.rb
CHANGED
|
@@ -61,14 +61,18 @@ module SwarmSDK
|
|
|
61
61
|
# - McpConfigurator: MCP client management (via AgentInitializer)
|
|
62
62
|
#
|
|
63
63
|
class Swarm
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
64
|
+
include Concerns::Snapshotable
|
|
65
|
+
include Concerns::Validatable
|
|
66
|
+
include Concerns::Cleanupable
|
|
67
|
+
include LoggingCallbacks
|
|
68
|
+
include HookTriggers
|
|
69
|
+
|
|
70
|
+
# NOTE: MCP log level now accessed via SwarmSDK.config.mcp_log_level
|
|
67
71
|
|
|
68
72
|
# Default tools available to all agents
|
|
69
73
|
DEFAULT_TOOLS = ToolConfigurator::DEFAULT_TOOLS
|
|
70
74
|
|
|
71
|
-
attr_reader :name, :agents, :lead_agent, :mcp_clients, :delegation_instances, :agent_definitions, :swarm_id, :parent_swarm_id, :swarm_registry, :scratchpad_storage, :allow_filesystem_tools
|
|
75
|
+
attr_reader :name, :agents, :lead_agent, :mcp_clients, :delegation_instances, :agent_definitions, :swarm_id, :parent_swarm_id, :swarm_registry, :scratchpad_storage, :allow_filesystem_tools, :hook_registry, :global_semaphore, :plugin_storages, :config_for_hooks, :observer_configs
|
|
72
76
|
attr_accessor :delegation_call_stack
|
|
73
77
|
|
|
74
78
|
# Check if scratchpad tools are enabled
|
|
@@ -93,7 +97,7 @@ module SwarmSDK
|
|
|
93
97
|
attr_writer :first_message_sent
|
|
94
98
|
|
|
95
99
|
# Class-level MCP log level configuration
|
|
96
|
-
@mcp_log_level =
|
|
100
|
+
@mcp_log_level = nil
|
|
97
101
|
@mcp_logging_configured = false
|
|
98
102
|
|
|
99
103
|
class << self
|
|
@@ -106,8 +110,8 @@ module SwarmSDK
|
|
|
106
110
|
#
|
|
107
111
|
# @param level [Integer] Log level (Logger::DEBUG, Logger::INFO, Logger::WARN, Logger::ERROR, Logger::FATAL)
|
|
108
112
|
# @return [void]
|
|
109
|
-
def configure_mcp_logging(level =
|
|
110
|
-
@mcp_log_level = level
|
|
113
|
+
def configure_mcp_logging(level = nil)
|
|
114
|
+
@mcp_log_level = level || SwarmSDK.config.mcp_log_level
|
|
111
115
|
apply_mcp_logging_configuration
|
|
112
116
|
end
|
|
113
117
|
|
|
@@ -118,7 +122,7 @@ module SwarmSDK
|
|
|
118
122
|
return if @mcp_logging_configured
|
|
119
123
|
|
|
120
124
|
RubyLLM::MCP.configure do |config|
|
|
121
|
-
config.log_level = @mcp_log_level
|
|
125
|
+
config.log_level = @mcp_log_level || SwarmSDK.config.mcp_log_level
|
|
122
126
|
end
|
|
123
127
|
|
|
124
128
|
@mcp_logging_configured = true
|
|
@@ -130,17 +134,17 @@ module SwarmSDK
|
|
|
130
134
|
# @param name [String] Human-readable swarm name
|
|
131
135
|
# @param swarm_id [String, nil] Optional swarm ID (auto-generated if not provided)
|
|
132
136
|
# @param parent_swarm_id [String, nil] Optional parent swarm ID (nil for root swarms)
|
|
133
|
-
# @param global_concurrency [Integer] Max concurrent LLM calls across entire swarm
|
|
134
|
-
# @param default_local_concurrency [Integer] Default max concurrent tool calls per agent
|
|
137
|
+
# @param global_concurrency [Integer, nil] Max concurrent LLM calls across entire swarm (nil uses config default)
|
|
138
|
+
# @param default_local_concurrency [Integer, nil] Default max concurrent tool calls per agent (nil uses config default)
|
|
135
139
|
# @param scratchpad [Tools::Stores::Scratchpad, nil] Optional scratchpad instance (for testing/internal use)
|
|
136
140
|
# @param scratchpad_mode [Symbol, String] Scratchpad mode (:enabled or :disabled). :per_node not allowed for non-node swarms.
|
|
137
141
|
# @param allow_filesystem_tools [Boolean, nil] Whether to allow filesystem tools (nil uses global setting)
|
|
138
|
-
def initialize(name:, swarm_id: nil, parent_swarm_id: nil, global_concurrency:
|
|
142
|
+
def initialize(name:, swarm_id: nil, parent_swarm_id: nil, global_concurrency: nil, default_local_concurrency: nil, scratchpad: nil, scratchpad_mode: :enabled, allow_filesystem_tools: nil)
|
|
139
143
|
@name = name
|
|
140
144
|
@swarm_id = swarm_id || generate_swarm_id(name)
|
|
141
145
|
@parent_swarm_id = parent_swarm_id
|
|
142
|
-
@global_concurrency = global_concurrency
|
|
143
|
-
@default_local_concurrency = default_local_concurrency
|
|
146
|
+
@global_concurrency = global_concurrency || SwarmSDK.config.global_concurrency_limit
|
|
147
|
+
@default_local_concurrency = default_local_concurrency || SwarmSDK.config.local_concurrency_limit
|
|
144
148
|
|
|
145
149
|
# Handle scratchpad_mode parameter
|
|
146
150
|
# For Swarm: :enabled or :disabled (not :per_node - that's for nodes)
|
|
@@ -148,9 +152,9 @@ module SwarmSDK
|
|
|
148
152
|
|
|
149
153
|
# Resolve allow_filesystem_tools with priority:
|
|
150
154
|
# 1. Explicit parameter (if not nil)
|
|
151
|
-
# 2. Global
|
|
155
|
+
# 2. Global config
|
|
152
156
|
@allow_filesystem_tools = if allow_filesystem_tools.nil?
|
|
153
|
-
SwarmSDK.
|
|
157
|
+
SwarmSDK.config.allow_filesystem_tools
|
|
154
158
|
else
|
|
155
159
|
allow_filesystem_tools
|
|
156
160
|
end
|
|
@@ -201,6 +205,10 @@ module SwarmSDK
|
|
|
201
205
|
# Track if agent_start events have been emitted
|
|
202
206
|
# This prevents duplicate emissions and ensures events are emitted when logging is ready
|
|
203
207
|
@agent_start_events_emitted = false
|
|
208
|
+
|
|
209
|
+
# Observer agent configurations
|
|
210
|
+
@observer_configs = []
|
|
211
|
+
@observer_manager = nil
|
|
204
212
|
end
|
|
205
213
|
|
|
206
214
|
# Add an agent to the swarm
|
|
@@ -256,53 +264,53 @@ module SwarmSDK
|
|
|
256
264
|
# and the entire swarm coordinates with shared rate limiting.
|
|
257
265
|
# Supports reprompting via swarm_stop hooks.
|
|
258
266
|
#
|
|
267
|
+
# By default, this method blocks until execution completes. Set wait: false
|
|
268
|
+
# to return an Async::Task immediately, enabling cancellation via task.stop.
|
|
269
|
+
#
|
|
259
270
|
# @param prompt [String] Task to execute
|
|
271
|
+
# @param wait [Boolean] If true (default), blocks until execution completes.
|
|
272
|
+
# If false, returns Async::Task immediately for non-blocking execution.
|
|
260
273
|
# @yield [Hash] Log entry if block given (for streaming)
|
|
261
|
-
# @return [Result]
|
|
262
|
-
|
|
274
|
+
# @return [Result, Async::Task] Result if wait: true, Async::Task if wait: false
|
|
275
|
+
#
|
|
276
|
+
# @example Blocking execution (default)
|
|
277
|
+
# result = swarm.execute("Build auth")
|
|
278
|
+
# puts result.content
|
|
279
|
+
#
|
|
280
|
+
# @example Non-blocking execution with cancellation
|
|
281
|
+
# task = swarm.execute("Build auth", wait: false) { |event| puts event }
|
|
282
|
+
# # ... do other work ...
|
|
283
|
+
# task.stop # Cancel anytime
|
|
284
|
+
# result = task.wait # Returns nil for cancelled tasks
|
|
285
|
+
def execute(prompt, wait: true, &block)
|
|
263
286
|
raise ConfigurationError, "No lead agent set. Set lead= first." unless @lead_agent
|
|
264
287
|
|
|
265
|
-
start_time = Time.now
|
|
266
288
|
logs = []
|
|
267
289
|
current_prompt = prompt
|
|
290
|
+
has_logging = block_given?
|
|
268
291
|
|
|
269
|
-
#
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
Fiber
|
|
274
|
-
|
|
292
|
+
# Save original Fiber storage for restoration (preserves parent context for nested swarms)
|
|
293
|
+
original_fiber_storage = {
|
|
294
|
+
execution_id: Fiber[:execution_id],
|
|
295
|
+
swarm_id: Fiber[:swarm_id],
|
|
296
|
+
parent_swarm_id: Fiber[:parent_swarm_id],
|
|
297
|
+
}
|
|
275
298
|
|
|
276
299
|
# Set fiber-local execution context
|
|
277
300
|
# Use ||= to inherit parent's execution_id if one exists (for mini-swarms)
|
|
278
|
-
# Child fibers (tools, delegations) inherit automatically
|
|
279
301
|
Fiber[:execution_id] ||= generate_execution_id
|
|
280
302
|
Fiber[:swarm_id] = @swarm_id
|
|
281
303
|
Fiber[:parent_swarm_id] = @parent_swarm_id
|
|
282
304
|
|
|
283
305
|
# Setup logging FIRST if block given (so swarm_start event can be emitted)
|
|
284
|
-
if
|
|
285
|
-
# Force fresh callback array for this execution
|
|
286
|
-
Fiber[:log_callbacks] = []
|
|
287
|
-
|
|
288
|
-
# Register callback to collect logs and forward to user's block
|
|
289
|
-
LogCollector.on_log do |entry|
|
|
290
|
-
logs << entry
|
|
291
|
-
block.call(entry)
|
|
292
|
-
end
|
|
306
|
+
setup_logging(logs, &block) if has_logging
|
|
293
307
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
308
|
+
# Setup observer execution if any observers configured
|
|
309
|
+
# MUST happen AFTER setup_logging (which clears Fiber[:log_subscriptions])
|
|
310
|
+
setup_observer_execution if @observer_configs.any?
|
|
297
311
|
|
|
298
312
|
# Trigger swarm_start hooks (before any execution)
|
|
299
|
-
|
|
300
|
-
# Default callback emits swarm_start event to LogStream
|
|
301
|
-
swarm_start_result = trigger_swarm_start(current_prompt)
|
|
302
|
-
if swarm_start_result&.replace?
|
|
303
|
-
# Hook provided stdout to append to prompt
|
|
304
|
-
current_prompt = "#{current_prompt}\n\n<hook-context>\n#{swarm_start_result.value}\n</hook-context>"
|
|
305
|
-
end
|
|
313
|
+
current_prompt = apply_swarm_start_hooks(current_prompt)
|
|
306
314
|
|
|
307
315
|
# Trigger first_message hooks on first execution
|
|
308
316
|
unless @first_message_sent
|
|
@@ -313,147 +321,18 @@ module SwarmSDK
|
|
|
313
321
|
# Lazy initialization of agents (with optional logging)
|
|
314
322
|
initialize_agents unless @agents_initialized
|
|
315
323
|
|
|
316
|
-
#
|
|
317
|
-
|
|
318
|
-
if block_given? && @agents_initialized && !@agent_start_events_emitted
|
|
319
|
-
# Setup logging callbacks for all agents (they were skipped during initialization)
|
|
320
|
-
setup_logging_for_all_agents
|
|
321
|
-
|
|
322
|
-
# Emit agent_start events now that logging is ready
|
|
323
|
-
emit_agent_start_events
|
|
324
|
-
@agent_start_events_emitted = true
|
|
325
|
-
end
|
|
326
|
-
|
|
327
|
-
# Execution loop (supports reprompting)
|
|
328
|
-
result = nil
|
|
329
|
-
swarm_stop_triggered = false
|
|
330
|
-
|
|
331
|
-
loop do
|
|
332
|
-
# Execute within Async reactor to enable fiber scheduler for parallel execution
|
|
333
|
-
# This sets Fiber.scheduler, making Faraday fiber-aware so HTTP requests yield during I/O
|
|
334
|
-
# Use finished: false to suppress warnings for expected task failures
|
|
335
|
-
lead = @agents[@lead_agent]
|
|
336
|
-
response = Async(finished: false) do
|
|
337
|
-
lead.ask(current_prompt)
|
|
338
|
-
end.wait
|
|
339
|
-
|
|
340
|
-
# Check if swarm was finished by a hook (finish_swarm)
|
|
341
|
-
if response.is_a?(Hash) && response[:__finish_swarm__]
|
|
342
|
-
result = Result.new(
|
|
343
|
-
content: response[:message],
|
|
344
|
-
agent: @lead_agent.to_s,
|
|
345
|
-
logs: logs,
|
|
346
|
-
duration: Time.now - start_time,
|
|
347
|
-
)
|
|
348
|
-
|
|
349
|
-
# Trigger swarm_stop hooks for event emission
|
|
350
|
-
trigger_swarm_stop(result)
|
|
351
|
-
swarm_stop_triggered = true
|
|
352
|
-
|
|
353
|
-
# Break immediately - don't allow reprompting when swarm is finished by hook
|
|
354
|
-
break
|
|
355
|
-
end
|
|
356
|
-
|
|
357
|
-
result = Result.new(
|
|
358
|
-
content: response.content,
|
|
359
|
-
agent: @lead_agent.to_s,
|
|
360
|
-
logs: logs,
|
|
361
|
-
duration: Time.now - start_time,
|
|
362
|
-
)
|
|
363
|
-
|
|
364
|
-
# Trigger swarm_stop hooks (for reprompt check and event emission)
|
|
365
|
-
hook_result = trigger_swarm_stop(result)
|
|
366
|
-
swarm_stop_triggered = true
|
|
367
|
-
|
|
368
|
-
# Check if hook requests reprompting
|
|
369
|
-
if hook_result&.reprompt?
|
|
370
|
-
current_prompt = hook_result.value
|
|
371
|
-
swarm_stop_triggered = false # Will trigger again in next iteration
|
|
372
|
-
# Continue loop with new prompt
|
|
373
|
-
else
|
|
374
|
-
# Exit loop - execution complete
|
|
375
|
-
break
|
|
376
|
-
end
|
|
377
|
-
end
|
|
324
|
+
# Emit agent_start events if agents were initialized before logging was set up
|
|
325
|
+
emit_retroactive_agent_start_events if has_logging
|
|
378
326
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
# Catch the specific "String does not have #dig method" error
|
|
385
|
-
if e.message.include?("does not have #dig method")
|
|
386
|
-
agent_definition = @agent_definitions[@lead_agent]
|
|
387
|
-
error_msg = if agent_definition.base_url
|
|
388
|
-
"LLM API request failed: The proxy/server at '#{agent_definition.base_url}' returned an invalid response. " \
|
|
389
|
-
"This usually means the proxy is unreachable, requires authentication, or returned an error in non-JSON format. " \
|
|
390
|
-
"Original error: #{e.message}"
|
|
391
|
-
else
|
|
392
|
-
"LLM API request failed with unexpected response format. Original error: #{e.message}"
|
|
393
|
-
end
|
|
394
|
-
|
|
395
|
-
result = Result.new(
|
|
396
|
-
content: nil,
|
|
397
|
-
agent: @lead_agent.to_s,
|
|
398
|
-
error: LLMError.new(error_msg),
|
|
399
|
-
logs: logs,
|
|
400
|
-
duration: Time.now - start_time,
|
|
401
|
-
)
|
|
402
|
-
else
|
|
403
|
-
result = Result.new(
|
|
404
|
-
content: nil,
|
|
405
|
-
agent: @lead_agent.to_s,
|
|
406
|
-
error: e,
|
|
407
|
-
logs: logs,
|
|
408
|
-
duration: Time.now - start_time,
|
|
409
|
-
)
|
|
410
|
-
end
|
|
411
|
-
result
|
|
412
|
-
rescue StandardError => e
|
|
413
|
-
result = Result.new(
|
|
414
|
-
content: nil,
|
|
415
|
-
agent: @lead_agent&.to_s || "unknown",
|
|
416
|
-
error: e,
|
|
327
|
+
# Delegate to Executor for actual execution
|
|
328
|
+
executor = Executor.new(self)
|
|
329
|
+
@current_task = executor.run(
|
|
330
|
+
current_prompt,
|
|
331
|
+
wait: wait,
|
|
417
332
|
logs: logs,
|
|
418
|
-
|
|
333
|
+
has_logging: has_logging,
|
|
334
|
+
original_fiber_storage: original_fiber_storage,
|
|
419
335
|
)
|
|
420
|
-
result
|
|
421
|
-
ensure
|
|
422
|
-
# Trigger swarm_stop if not already triggered (handles error cases)
|
|
423
|
-
unless swarm_stop_triggered
|
|
424
|
-
trigger_swarm_stop_final(result, start_time, logs)
|
|
425
|
-
end
|
|
426
|
-
|
|
427
|
-
# Cleanup MCP clients after execution
|
|
428
|
-
cleanup
|
|
429
|
-
|
|
430
|
-
# Only clear Fiber storage if we set up logging (same pattern as LogCollector)
|
|
431
|
-
# Mini-swarms are called without block, so they don't clear
|
|
432
|
-
if block_given?
|
|
433
|
-
Fiber[:execution_id] = nil
|
|
434
|
-
Fiber[:swarm_id] = nil
|
|
435
|
-
Fiber[:parent_swarm_id] = nil
|
|
436
|
-
end
|
|
437
|
-
|
|
438
|
-
# Reset logging state for next execution if we set it up
|
|
439
|
-
#
|
|
440
|
-
# IMPORTANT: Only reset if we set up logging (block_given? == true).
|
|
441
|
-
# When this swarm is a mini-swarm within a NodeOrchestrator workflow,
|
|
442
|
-
# the orchestrator manages LogCollector and we don't set up logging.
|
|
443
|
-
#
|
|
444
|
-
# Flow in NodeOrchestrator:
|
|
445
|
-
# 1. NodeOrchestrator sets up LogCollector + LogStream (no block given to mini-swarms)
|
|
446
|
-
# 2. Each mini-swarm executes without logging block (block_given? == false)
|
|
447
|
-
# 3. Each mini-swarm skips reset (didn't set up logging)
|
|
448
|
-
# 4. NodeOrchestrator resets once at the very end
|
|
449
|
-
#
|
|
450
|
-
# Flow in standalone swarm / interactive REPL:
|
|
451
|
-
# 1. Swarm.execute sets up LogCollector + LogStream (block given)
|
|
452
|
-
# 2. Swarm.execute resets in ensure block (cleanup for next call)
|
|
453
|
-
if block_given?
|
|
454
|
-
LogCollector.reset!
|
|
455
|
-
LogStream.reset!
|
|
456
|
-
end
|
|
457
336
|
end
|
|
458
337
|
|
|
459
338
|
# Get an agent chat instance by name
|
|
@@ -487,86 +366,17 @@ module SwarmSDK
|
|
|
487
366
|
@agent_definitions.keys
|
|
488
367
|
end
|
|
489
368
|
|
|
490
|
-
#
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
# Useful for displaying configuration warnings before execution.
|
|
494
|
-
#
|
|
495
|
-
# @return [Array<Hash>] Array of warning hashes from all agent definitions
|
|
496
|
-
#
|
|
497
|
-
# @example
|
|
498
|
-
# swarm = SwarmSDK.load_file("config.yml")
|
|
499
|
-
# warnings = swarm.validate
|
|
500
|
-
# warnings.each do |warning|
|
|
501
|
-
# puts "⚠️ #{warning[:agent]}: #{warning[:model]} not found"
|
|
502
|
-
# end
|
|
503
|
-
def validate
|
|
504
|
-
@agent_definitions.flat_map { |_name, definition| definition.validate }
|
|
369
|
+
# Implement Snapshotable interface
|
|
370
|
+
def primary_agents
|
|
371
|
+
@agents
|
|
505
372
|
end
|
|
506
373
|
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
# This validates all agent definitions and emits any warnings as
|
|
510
|
-
# model_lookup_warning events through LogStream. Useful for emitting
|
|
511
|
-
# warnings before execution starts (e.g., in REPL after welcome screen).
|
|
512
|
-
#
|
|
513
|
-
# Requires LogStream.emitter to be set.
|
|
514
|
-
#
|
|
515
|
-
# @return [Array<Hash>] The validation warnings that were emitted
|
|
516
|
-
#
|
|
517
|
-
# @example
|
|
518
|
-
# LogCollector.on_log { |event| puts event }
|
|
519
|
-
# LogStream.emitter = LogCollector
|
|
520
|
-
# swarm.emit_validation_warnings
|
|
521
|
-
def emit_validation_warnings
|
|
522
|
-
warnings = validate
|
|
523
|
-
|
|
524
|
-
warnings.each do |warning|
|
|
525
|
-
case warning[:type]
|
|
526
|
-
when :model_not_found
|
|
527
|
-
LogStream.emit(
|
|
528
|
-
type: "model_lookup_warning",
|
|
529
|
-
agent: warning[:agent],
|
|
530
|
-
swarm_id: @swarm_id,
|
|
531
|
-
parent_swarm_id: @parent_swarm_id,
|
|
532
|
-
model: warning[:model],
|
|
533
|
-
error_message: warning[:error_message],
|
|
534
|
-
suggestions: warning[:suggestions],
|
|
535
|
-
timestamp: Time.now.utc.iso8601,
|
|
536
|
-
)
|
|
537
|
-
end
|
|
538
|
-
end
|
|
539
|
-
|
|
540
|
-
warnings
|
|
374
|
+
def delegation_instances_hash
|
|
375
|
+
@delegation_instances
|
|
541
376
|
end
|
|
542
377
|
|
|
543
|
-
#
|
|
544
|
-
#
|
|
545
|
-
# Stops all MCP client connections gracefully.
|
|
546
|
-
# Should be called when the swarm is no longer needed.
|
|
547
|
-
#
|
|
548
|
-
# @return [void]
|
|
549
|
-
def cleanup
|
|
550
|
-
# Check if there's anything to clean up
|
|
551
|
-
return if @mcp_clients.empty? && (!@delegation_instances || @delegation_instances.empty?)
|
|
552
|
-
|
|
553
|
-
# Stop MCP clients for all agents (primaries + delegations tracked by instance name)
|
|
554
|
-
@mcp_clients.each do |agent_name, clients|
|
|
555
|
-
clients.each do |client|
|
|
556
|
-
# Always call stop - this sets @running = false and stops background threads
|
|
557
|
-
client.stop
|
|
558
|
-
RubyLLM.logger.debug("SwarmSDK: Stopped MCP client '#{client.name}' for agent #{agent_name}")
|
|
559
|
-
rescue StandardError => e
|
|
560
|
-
# Don't fail cleanup if stopping one client fails
|
|
561
|
-
RubyLLM.logger.debug("SwarmSDK: Error stopping MCP client '#{client.name}': #{e.message}")
|
|
562
|
-
end
|
|
563
|
-
end
|
|
564
|
-
|
|
565
|
-
@mcp_clients.clear
|
|
566
|
-
|
|
567
|
-
# Clear delegation instances (V7.0: Added for completeness)
|
|
568
|
-
@delegation_instances&.clear
|
|
569
|
-
end
|
|
378
|
+
# NOTE: validate() and emit_validation_warnings() are provided by Concerns::Validatable
|
|
379
|
+
# Note: cleanup() is provided by Concerns::Cleanupable
|
|
570
380
|
|
|
571
381
|
# Register a named hook that can be referenced in agent configurations
|
|
572
382
|
#
|
|
@@ -586,26 +396,6 @@ module SwarmSDK
|
|
|
586
396
|
self
|
|
587
397
|
end
|
|
588
398
|
|
|
589
|
-
# Add a swarm-level default hook that applies to all agents
|
|
590
|
-
#
|
|
591
|
-
# Default hooks are inherited by all agents unless overridden at agent level.
|
|
592
|
-
# Useful for swarm-wide policies like logging, validation, or monitoring.
|
|
593
|
-
#
|
|
594
|
-
# @param event [Symbol] Event type (e.g., :pre_tool_use, :post_tool_use)
|
|
595
|
-
# @param matcher [String, Regexp, nil] Optional regex pattern for tool names
|
|
596
|
-
# @param priority [Integer] Execution priority (higher = earlier)
|
|
597
|
-
# @param block [Proc] Hook implementation
|
|
598
|
-
# @return [self]
|
|
599
|
-
#
|
|
600
|
-
# @example Add logging for all tool calls
|
|
601
|
-
# swarm.add_default_callback(:pre_tool_use) do |context|
|
|
602
|
-
# puts "[#{context.agent_name}] Calling #{context.tool_call.name}"
|
|
603
|
-
# end
|
|
604
|
-
def add_default_callback(event, matcher: nil, priority: 0, &block)
|
|
605
|
-
@hook_registry.add_default(event, matcher: matcher, priority: priority, &block)
|
|
606
|
-
self
|
|
607
|
-
end
|
|
608
|
-
|
|
609
399
|
# Reset context for all agents
|
|
610
400
|
#
|
|
611
401
|
# Clears conversation history for all agents. This is used by composable swarms
|
|
@@ -618,6 +408,39 @@ module SwarmSDK
|
|
|
618
408
|
end
|
|
619
409
|
end
|
|
620
410
|
|
|
411
|
+
# Add observer configuration
|
|
412
|
+
#
|
|
413
|
+
# Called by Swarm::Builder to register observer agent configurations.
|
|
414
|
+
# Validates that the referenced agent exists.
|
|
415
|
+
#
|
|
416
|
+
# @param config [Observer::Config] Observer configuration
|
|
417
|
+
# @return [void]
|
|
418
|
+
def add_observer_config(config)
|
|
419
|
+
validate_observer_agent(config.agent_name)
|
|
420
|
+
@observer_configs << config
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
# Wait for all observer tasks to complete
|
|
424
|
+
#
|
|
425
|
+
# Called by Executor to wait for observer agents before cleanup.
|
|
426
|
+
# Safe to call even if no observers are configured.
|
|
427
|
+
#
|
|
428
|
+
# @return [void]
|
|
429
|
+
def wait_for_observers
|
|
430
|
+
@observer_manager&.wait_for_completion
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
# Cleanup observer subscriptions
|
|
434
|
+
#
|
|
435
|
+
# Called by Executor.cleanup_after_execution to unsubscribe observers.
|
|
436
|
+
# Matches the MCP cleanup pattern.
|
|
437
|
+
#
|
|
438
|
+
# @return [void]
|
|
439
|
+
def cleanup_observers
|
|
440
|
+
@observer_manager&.cleanup
|
|
441
|
+
@observer_manager = nil
|
|
442
|
+
end
|
|
443
|
+
|
|
621
444
|
# Create snapshot of current conversation state
|
|
622
445
|
#
|
|
623
446
|
# Returns a Snapshot object containing:
|
|
@@ -705,12 +528,53 @@ module SwarmSDK
|
|
|
705
528
|
# @return [void]
|
|
706
529
|
attr_writer :swarm_registry
|
|
707
530
|
|
|
531
|
+
# --- Internal API (for Executor use only) ---
|
|
532
|
+
# Hook triggers for swarm lifecycle events are provided by HookTriggers module
|
|
533
|
+
|
|
708
534
|
private
|
|
709
535
|
|
|
536
|
+
# Apply swarm_start hooks to prompt
|
|
537
|
+
#
|
|
538
|
+
# @param prompt [String] Original prompt
|
|
539
|
+
# @return [String] Modified prompt (possibly with hook context appended)
|
|
540
|
+
def apply_swarm_start_hooks(prompt)
|
|
541
|
+
swarm_start_result = trigger_swarm_start(prompt)
|
|
542
|
+
if swarm_start_result&.replace?
|
|
543
|
+
"#{prompt}\n\n<hook-context>\n#{swarm_start_result.value}\n</hook-context>"
|
|
544
|
+
else
|
|
545
|
+
prompt
|
|
546
|
+
end
|
|
547
|
+
end
|
|
548
|
+
|
|
549
|
+
# Validate that observer agent exists
|
|
550
|
+
#
|
|
551
|
+
# @param agent_name [Symbol] Name of the observer agent
|
|
552
|
+
# @raise [ConfigurationError] If agent not found
|
|
553
|
+
# @return [void]
|
|
554
|
+
def validate_observer_agent(agent_name)
|
|
555
|
+
return if @agent_definitions.key?(agent_name)
|
|
556
|
+
|
|
557
|
+
raise ConfigurationError,
|
|
558
|
+
"Observer agent '#{agent_name}' not found. " \
|
|
559
|
+
"Define the agent first with `agent :#{agent_name} do ... end`"
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
# Setup observer manager and subscriptions
|
|
563
|
+
#
|
|
564
|
+
# Creates Observer::Manager and registers event subscriptions.
|
|
565
|
+
# Must be called AFTER setup_logging (which clears Fiber[:log_subscriptions]).
|
|
566
|
+
#
|
|
567
|
+
# @return [void]
|
|
568
|
+
def setup_observer_execution
|
|
569
|
+
@observer_manager = Observer::Manager.new(self)
|
|
570
|
+
@observer_configs.each { |c| @observer_manager.add_config(c) }
|
|
571
|
+
@observer_manager.setup
|
|
572
|
+
end
|
|
573
|
+
|
|
710
574
|
# Validate and normalize scratchpad mode for Swarm
|
|
711
575
|
#
|
|
712
576
|
# Regular Swarms support :enabled or :disabled.
|
|
713
|
-
# Rejects :per_node since it only makes sense for
|
|
577
|
+
# Rejects :per_node since it only makes sense for Workflow with multiple nodes.
|
|
714
578
|
#
|
|
715
579
|
# @param value [Symbol, String] Scratchpad mode (strings from YAML converted to symbols)
|
|
716
580
|
# @return [Symbol] :enabled or :disabled
|
|
@@ -724,7 +588,7 @@ module SwarmSDK
|
|
|
724
588
|
value
|
|
725
589
|
when :per_node
|
|
726
590
|
raise ArgumentError,
|
|
727
|
-
"scratchpad: :per_node is only valid for
|
|
591
|
+
"scratchpad: :per_node is only valid for Workflow with nodes. " \
|
|
728
592
|
"For regular Swarms, use :enabled or :disabled."
|
|
729
593
|
else
|
|
730
594
|
raise ArgumentError,
|
|
@@ -764,15 +628,7 @@ module SwarmSDK
|
|
|
764
628
|
def initialize_agents
|
|
765
629
|
return if @agents_initialized
|
|
766
630
|
|
|
767
|
-
initializer = AgentInitializer.new(
|
|
768
|
-
self,
|
|
769
|
-
@agent_definitions,
|
|
770
|
-
@global_semaphore,
|
|
771
|
-
@hook_registry,
|
|
772
|
-
@scratchpad_storage,
|
|
773
|
-
@plugin_storages,
|
|
774
|
-
config_for_hooks: @config_for_hooks,
|
|
775
|
-
)
|
|
631
|
+
initializer = AgentInitializer.new(self)
|
|
776
632
|
|
|
777
633
|
@agents = initializer.initialize_all
|
|
778
634
|
@agent_contexts = initializer.agent_contexts
|
|
@@ -782,85 +638,6 @@ module SwarmSDK
|
|
|
782
638
|
# This ensures events are never lost, even if agents are initialized early (e.g., by restore())
|
|
783
639
|
end
|
|
784
640
|
|
|
785
|
-
# Setup logging callbacks for all agents
|
|
786
|
-
#
|
|
787
|
-
# Called when agents were initialized before logging was set up (e.g., via restore()).
|
|
788
|
-
# Retroactively registers RubyLLM callbacks (on_tool_call, on_end_message, etc.)
|
|
789
|
-
# so events are properly emitted during execution.
|
|
790
|
-
# Safe to call multiple times - RubyLLM callbacks are replaced, not appended.
|
|
791
|
-
#
|
|
792
|
-
# @return [void]
|
|
793
|
-
def setup_logging_for_all_agents
|
|
794
|
-
# Setup for PRIMARY agents
|
|
795
|
-
@agents.each_value do |chat|
|
|
796
|
-
chat.setup_logging if chat.respond_to?(:setup_logging)
|
|
797
|
-
end
|
|
798
|
-
|
|
799
|
-
# Setup for DELEGATION instances
|
|
800
|
-
@delegation_instances.each_value do |chat|
|
|
801
|
-
chat.setup_logging if chat.respond_to?(:setup_logging)
|
|
802
|
-
end
|
|
803
|
-
end
|
|
804
|
-
|
|
805
|
-
# Emit agent_start events for all initialized agents
|
|
806
|
-
def emit_agent_start_events
|
|
807
|
-
return unless LogStream.emitter
|
|
808
|
-
|
|
809
|
-
# Emit for PRIMARY agents
|
|
810
|
-
@agents.each do |agent_name, chat|
|
|
811
|
-
emit_agent_start_for(agent_name, chat, is_delegation: false)
|
|
812
|
-
end
|
|
813
|
-
|
|
814
|
-
# Emit for DELEGATION instances
|
|
815
|
-
@delegation_instances.each do |instance_name, chat|
|
|
816
|
-
base_name = extract_base_name(instance_name)
|
|
817
|
-
emit_agent_start_for(instance_name.to_sym, chat, is_delegation: true, base_name: base_name)
|
|
818
|
-
end
|
|
819
|
-
|
|
820
|
-
# Mark as emitted to prevent duplicate emissions
|
|
821
|
-
@agent_start_events_emitted = true
|
|
822
|
-
end
|
|
823
|
-
|
|
824
|
-
# Helper for emitting agent_start event
|
|
825
|
-
def emit_agent_start_for(agent_name, chat, is_delegation:, base_name: nil)
|
|
826
|
-
base_name ||= agent_name
|
|
827
|
-
agent_def = @agent_definitions[base_name]
|
|
828
|
-
|
|
829
|
-
# Build plugin storage info using base name
|
|
830
|
-
plugin_storage_info = {}
|
|
831
|
-
@plugin_storages.each do |plugin_name, agent_storages|
|
|
832
|
-
next unless agent_storages.key?(base_name)
|
|
833
|
-
|
|
834
|
-
plugin_storage_info[plugin_name] = {
|
|
835
|
-
enabled: true,
|
|
836
|
-
config: agent_def.respond_to?(plugin_name) ? extract_plugin_config_info(agent_def.public_send(plugin_name)) : nil,
|
|
837
|
-
}
|
|
838
|
-
end
|
|
839
|
-
|
|
840
|
-
LogStream.emit(
|
|
841
|
-
type: "agent_start",
|
|
842
|
-
agent: agent_name,
|
|
843
|
-
swarm_id: @swarm_id,
|
|
844
|
-
parent_swarm_id: @parent_swarm_id,
|
|
845
|
-
swarm_name: @name,
|
|
846
|
-
model: agent_def.model,
|
|
847
|
-
provider: agent_def.provider || "openai",
|
|
848
|
-
directory: agent_def.directory,
|
|
849
|
-
system_prompt: agent_def.system_prompt,
|
|
850
|
-
tools: chat.tools.keys,
|
|
851
|
-
delegates_to: agent_def.delegates_to,
|
|
852
|
-
plugin_storages: plugin_storage_info,
|
|
853
|
-
is_delegation_instance: is_delegation,
|
|
854
|
-
base_agent: (base_name if is_delegation),
|
|
855
|
-
timestamp: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ"),
|
|
856
|
-
)
|
|
857
|
-
end
|
|
858
|
-
|
|
859
|
-
# Extract base name from instance name
|
|
860
|
-
def extract_base_name(instance_name)
|
|
861
|
-
instance_name.to_s.split("@").first.to_sym
|
|
862
|
-
end
|
|
863
|
-
|
|
864
641
|
# Normalize tools to internal format (kept for add_agent)
|
|
865
642
|
#
|
|
866
643
|
# Handles both Ruby API (simple symbols) and YAML API (already parsed configs)
|
|
@@ -908,7 +685,7 @@ module SwarmSDK
|
|
|
908
685
|
|
|
909
686
|
# Create delegation tool (delegates to AgentInitializer)
|
|
910
687
|
def create_delegation_tool(name:, description:, delegate_chat:, agent_name:)
|
|
911
|
-
AgentInitializer.new(self
|
|
688
|
+
AgentInitializer.new(self)
|
|
912
689
|
.create_delegation_tool(name: name, description: description, delegate_chat: delegate_chat, agent_name: agent_name)
|
|
913
690
|
end
|
|
914
691
|
|
|
@@ -935,325 +712,5 @@ module SwarmSDK
|
|
|
935
712
|
# Unknown config type
|
|
936
713
|
nil
|
|
937
714
|
end
|
|
938
|
-
|
|
939
|
-
# Register default logging hooks that emit LogStream events
|
|
940
|
-
#
|
|
941
|
-
# These hooks implement the standard SwarmSDK logging behavior.
|
|
942
|
-
# Users can override or extend them by registering their own hooks.
|
|
943
|
-
#
|
|
944
|
-
# @return [void]
|
|
945
|
-
def register_default_logging_callbacks
|
|
946
|
-
# Log swarm start
|
|
947
|
-
add_default_callback(:swarm_start, priority: -100) do |context|
|
|
948
|
-
# Only log if LogStream emitter is set (logging enabled)
|
|
949
|
-
next unless LogStream.emitter
|
|
950
|
-
|
|
951
|
-
LogStream.emit(
|
|
952
|
-
type: "swarm_start",
|
|
953
|
-
agent: context.metadata[:lead_agent], # Include agent for consistency
|
|
954
|
-
swarm_id: @swarm_id,
|
|
955
|
-
parent_swarm_id: @parent_swarm_id,
|
|
956
|
-
swarm_name: context.metadata[:swarm_name],
|
|
957
|
-
lead_agent: context.metadata[:lead_agent],
|
|
958
|
-
prompt: context.metadata[:prompt],
|
|
959
|
-
timestamp: context.metadata[:timestamp],
|
|
960
|
-
)
|
|
961
|
-
end
|
|
962
|
-
|
|
963
|
-
# Log swarm stop
|
|
964
|
-
add_default_callback(:swarm_stop, priority: -100) do |context|
|
|
965
|
-
# Only log if LogStream emitter is set (logging enabled)
|
|
966
|
-
next unless LogStream.emitter
|
|
967
|
-
|
|
968
|
-
LogStream.emit(
|
|
969
|
-
type: "swarm_stop",
|
|
970
|
-
swarm_id: @swarm_id,
|
|
971
|
-
parent_swarm_id: @parent_swarm_id,
|
|
972
|
-
swarm_name: context.metadata[:swarm_name],
|
|
973
|
-
lead_agent: context.metadata[:lead_agent],
|
|
974
|
-
last_agent: context.metadata[:last_agent], # Agent that produced final response
|
|
975
|
-
content: context.metadata[:content], # Final response content
|
|
976
|
-
success: context.metadata[:success],
|
|
977
|
-
duration: context.metadata[:duration],
|
|
978
|
-
total_cost: context.metadata[:total_cost],
|
|
979
|
-
total_tokens: context.metadata[:total_tokens],
|
|
980
|
-
agents_involved: context.metadata[:agents_involved],
|
|
981
|
-
timestamp: context.metadata[:timestamp],
|
|
982
|
-
)
|
|
983
|
-
end
|
|
984
|
-
|
|
985
|
-
# Log user requests
|
|
986
|
-
add_default_callback(:user_prompt, priority: -100) do |context|
|
|
987
|
-
# Only log if LogStream emitter is set (logging enabled)
|
|
988
|
-
next unless LogStream.emitter
|
|
989
|
-
|
|
990
|
-
LogStream.emit(
|
|
991
|
-
type: "user_prompt",
|
|
992
|
-
agent: context.agent_name,
|
|
993
|
-
swarm_id: @swarm_id,
|
|
994
|
-
parent_swarm_id: @parent_swarm_id,
|
|
995
|
-
model: context.metadata[:model] || "unknown",
|
|
996
|
-
provider: context.metadata[:provider] || "unknown",
|
|
997
|
-
message_count: context.metadata[:message_count] || 0,
|
|
998
|
-
tools: context.metadata[:tools] || [],
|
|
999
|
-
delegates_to: context.metadata[:delegates_to] || [],
|
|
1000
|
-
metadata: context.metadata,
|
|
1001
|
-
)
|
|
1002
|
-
end
|
|
1003
|
-
|
|
1004
|
-
# Log intermediate agent responses with tool calls
|
|
1005
|
-
add_default_callback(:agent_step, priority: -100) do |context|
|
|
1006
|
-
# Only log if LogStream emitter is set (logging enabled)
|
|
1007
|
-
next unless LogStream.emitter
|
|
1008
|
-
|
|
1009
|
-
# Extract top-level fields and remove from metadata to avoid duplication
|
|
1010
|
-
metadata_without_duplicates = context.metadata.except(:model, :content, :tool_calls, :finish_reason, :usage, :tool_executions)
|
|
1011
|
-
|
|
1012
|
-
LogStream.emit(
|
|
1013
|
-
type: "agent_step",
|
|
1014
|
-
agent: context.agent_name,
|
|
1015
|
-
swarm_id: @swarm_id,
|
|
1016
|
-
parent_swarm_id: @parent_swarm_id,
|
|
1017
|
-
model: context.metadata[:model],
|
|
1018
|
-
content: context.metadata[:content],
|
|
1019
|
-
tool_calls: context.metadata[:tool_calls],
|
|
1020
|
-
finish_reason: context.metadata[:finish_reason],
|
|
1021
|
-
usage: context.metadata[:usage],
|
|
1022
|
-
tool_executions: context.metadata[:tool_executions],
|
|
1023
|
-
metadata: metadata_without_duplicates,
|
|
1024
|
-
)
|
|
1025
|
-
end
|
|
1026
|
-
|
|
1027
|
-
# Log final agent responses
|
|
1028
|
-
add_default_callback(:agent_stop, priority: -100) do |context|
|
|
1029
|
-
# Only log if LogStream emitter is set (logging enabled)
|
|
1030
|
-
next unless LogStream.emitter
|
|
1031
|
-
|
|
1032
|
-
# Extract top-level fields and remove from metadata to avoid duplication
|
|
1033
|
-
metadata_without_duplicates = context.metadata.except(:model, :content, :tool_calls, :finish_reason, :usage, :tool_executions)
|
|
1034
|
-
|
|
1035
|
-
LogStream.emit(
|
|
1036
|
-
type: "agent_stop",
|
|
1037
|
-
agent: context.agent_name,
|
|
1038
|
-
swarm_id: @swarm_id,
|
|
1039
|
-
parent_swarm_id: @parent_swarm_id,
|
|
1040
|
-
model: context.metadata[:model],
|
|
1041
|
-
content: context.metadata[:content],
|
|
1042
|
-
tool_calls: context.metadata[:tool_calls],
|
|
1043
|
-
finish_reason: context.metadata[:finish_reason],
|
|
1044
|
-
usage: context.metadata[:usage],
|
|
1045
|
-
tool_executions: context.metadata[:tool_executions],
|
|
1046
|
-
metadata: metadata_without_duplicates,
|
|
1047
|
-
)
|
|
1048
|
-
end
|
|
1049
|
-
|
|
1050
|
-
# Log tool calls (pre_tool_use)
|
|
1051
|
-
add_default_callback(:pre_tool_use, priority: -100) do |context|
|
|
1052
|
-
# Only log if LogStream emitter is set (logging enabled)
|
|
1053
|
-
next unless LogStream.emitter
|
|
1054
|
-
|
|
1055
|
-
# Delegation tracking is handled separately in AgentChat
|
|
1056
|
-
# Just log the tool call - delegation info will be in metadata if needed
|
|
1057
|
-
LogStream.emit(
|
|
1058
|
-
type: "tool_call",
|
|
1059
|
-
agent: context.agent_name,
|
|
1060
|
-
swarm_id: @swarm_id,
|
|
1061
|
-
parent_swarm_id: @parent_swarm_id,
|
|
1062
|
-
tool_call_id: context.tool_call.id,
|
|
1063
|
-
tool: context.tool_call.name,
|
|
1064
|
-
arguments: context.tool_call.parameters,
|
|
1065
|
-
metadata: context.metadata,
|
|
1066
|
-
)
|
|
1067
|
-
end
|
|
1068
|
-
|
|
1069
|
-
# Log tool results (post_tool_use)
|
|
1070
|
-
add_default_callback(:post_tool_use, priority: -100) do |context|
|
|
1071
|
-
# Only log if LogStream emitter is set (logging enabled)
|
|
1072
|
-
next unless LogStream.emitter
|
|
1073
|
-
|
|
1074
|
-
# Delegation tracking is handled separately in AgentChat
|
|
1075
|
-
# Usage tracking is handled in agent_step/agent_stop events
|
|
1076
|
-
LogStream.emit(
|
|
1077
|
-
type: "tool_result",
|
|
1078
|
-
agent: context.agent_name,
|
|
1079
|
-
swarm_id: @swarm_id,
|
|
1080
|
-
parent_swarm_id: @parent_swarm_id,
|
|
1081
|
-
tool_call_id: context.tool_result.tool_call_id,
|
|
1082
|
-
tool: context.tool_result.tool_name,
|
|
1083
|
-
result: context.tool_result.content,
|
|
1084
|
-
metadata: context.metadata,
|
|
1085
|
-
)
|
|
1086
|
-
end
|
|
1087
|
-
|
|
1088
|
-
# Log context warnings
|
|
1089
|
-
add_default_callback(:context_warning, priority: -100) do |context|
|
|
1090
|
-
# Only log if LogStream emitter is set (logging enabled)
|
|
1091
|
-
next unless LogStream.emitter
|
|
1092
|
-
|
|
1093
|
-
LogStream.emit(
|
|
1094
|
-
type: "context_limit_warning",
|
|
1095
|
-
agent: context.agent_name,
|
|
1096
|
-
swarm_id: @swarm_id,
|
|
1097
|
-
parent_swarm_id: @parent_swarm_id,
|
|
1098
|
-
model: context.metadata[:model] || "unknown",
|
|
1099
|
-
threshold: "#{context.metadata[:threshold]}%",
|
|
1100
|
-
current_usage: "#{context.metadata[:percentage]}%",
|
|
1101
|
-
tokens_used: context.metadata[:tokens_used],
|
|
1102
|
-
tokens_remaining: context.metadata[:tokens_remaining],
|
|
1103
|
-
context_limit: context.metadata[:context_limit],
|
|
1104
|
-
metadata: context.metadata,
|
|
1105
|
-
)
|
|
1106
|
-
end
|
|
1107
|
-
end
|
|
1108
|
-
|
|
1109
|
-
# Trigger swarm_start hooks when swarm execution begins
|
|
1110
|
-
#
|
|
1111
|
-
# This is a swarm-level event that fires when Swarm.execute is called
|
|
1112
|
-
# (before first user message is sent). Hooks can halt execution or append stdout to prompt.
|
|
1113
|
-
# Default callback emits to LogStream for logging.
|
|
1114
|
-
#
|
|
1115
|
-
# @param prompt [String] The user's task prompt
|
|
1116
|
-
# @return [Hooks::Result, nil] Result with stdout to append (if exit 0) or nil
|
|
1117
|
-
# @raise [Hooks::Error] If hook halts execution
|
|
1118
|
-
def trigger_swarm_start(prompt)
|
|
1119
|
-
context = Hooks::Context.new(
|
|
1120
|
-
event: :swarm_start,
|
|
1121
|
-
agent_name: @lead_agent.to_s,
|
|
1122
|
-
swarm: self,
|
|
1123
|
-
metadata: {
|
|
1124
|
-
swarm_name: @name,
|
|
1125
|
-
lead_agent: @lead_agent,
|
|
1126
|
-
prompt: prompt,
|
|
1127
|
-
timestamp: Time.now.utc.iso8601,
|
|
1128
|
-
},
|
|
1129
|
-
)
|
|
1130
|
-
|
|
1131
|
-
executor = Hooks::Executor.new(@hook_registry, logger: RubyLLM.logger)
|
|
1132
|
-
result = executor.execute_safe(event: :swarm_start, context: context, callbacks: [])
|
|
1133
|
-
|
|
1134
|
-
# Halt execution if hook requests it
|
|
1135
|
-
raise Hooks::Error, "Swarm start halted by hook: #{result.value}" if result.halt?
|
|
1136
|
-
|
|
1137
|
-
# Return result so caller can check for replace (stdout injection)
|
|
1138
|
-
result
|
|
1139
|
-
rescue StandardError => e
|
|
1140
|
-
RubyLLM.logger.error("SwarmSDK: Error in swarm_start hook: #{e.message}")
|
|
1141
|
-
raise
|
|
1142
|
-
end
|
|
1143
|
-
|
|
1144
|
-
# Trigger swarm_stop for final event emission (called in ensure block)
|
|
1145
|
-
#
|
|
1146
|
-
# This ALWAYS emits the swarm_stop event, even if there was an error.
|
|
1147
|
-
# It does NOT check for reprompt (that's done in trigger_swarm_stop_for_reprompt_check).
|
|
1148
|
-
#
|
|
1149
|
-
# @param result [Result, nil] Execution result (may be nil if exception before result created)
|
|
1150
|
-
# @param start_time [Time] Execution start time
|
|
1151
|
-
# @param logs [Array] Collected logs
|
|
1152
|
-
# @return [void]
|
|
1153
|
-
def trigger_swarm_stop_final(result, start_time, logs)
|
|
1154
|
-
# Create a minimal result if one doesn't exist (exception before result created)
|
|
1155
|
-
result ||= Result.new(
|
|
1156
|
-
content: nil,
|
|
1157
|
-
agent: @lead_agent&.to_s || "unknown",
|
|
1158
|
-
logs: logs,
|
|
1159
|
-
duration: Time.now - start_time,
|
|
1160
|
-
error: StandardError.new("Unknown error"),
|
|
1161
|
-
)
|
|
1162
|
-
|
|
1163
|
-
context = Hooks::Context.new(
|
|
1164
|
-
event: :swarm_stop,
|
|
1165
|
-
agent_name: @lead_agent.to_s,
|
|
1166
|
-
swarm: self,
|
|
1167
|
-
metadata: {
|
|
1168
|
-
swarm_name: @name,
|
|
1169
|
-
lead_agent: @lead_agent,
|
|
1170
|
-
last_agent: result.agent, # Agent that produced the final response
|
|
1171
|
-
content: result.content, # Final response content
|
|
1172
|
-
success: result.success?,
|
|
1173
|
-
duration: result.duration,
|
|
1174
|
-
total_cost: result.total_cost,
|
|
1175
|
-
total_tokens: result.total_tokens,
|
|
1176
|
-
agents_involved: result.agents_involved,
|
|
1177
|
-
result: result,
|
|
1178
|
-
timestamp: Time.now.utc.iso8601,
|
|
1179
|
-
},
|
|
1180
|
-
)
|
|
1181
|
-
|
|
1182
|
-
executor = Hooks::Executor.new(@hook_registry, logger: RubyLLM.logger)
|
|
1183
|
-
executor.execute_safe(event: :swarm_stop, context: context, callbacks: [])
|
|
1184
|
-
rescue StandardError => e
|
|
1185
|
-
# Don't let swarm_stop errors break the ensure block
|
|
1186
|
-
RubyLLM.logger.error("SwarmSDK: Error in swarm_stop final emission: #{e.message}")
|
|
1187
|
-
end
|
|
1188
|
-
|
|
1189
|
-
# Trigger swarm_stop hooks for reprompt check and event emission
|
|
1190
|
-
#
|
|
1191
|
-
# This is called in the normal execution flow to check if hooks request reprompting.
|
|
1192
|
-
# The default callback also emits the swarm_stop event to LogStream.
|
|
1193
|
-
#
|
|
1194
|
-
# @param result [Result] The execution result
|
|
1195
|
-
# @return [Hooks::Result, nil] Hook result (reprompt action if applicable)
|
|
1196
|
-
def trigger_swarm_stop(result)
|
|
1197
|
-
context = Hooks::Context.new(
|
|
1198
|
-
event: :swarm_stop,
|
|
1199
|
-
agent_name: @lead_agent.to_s,
|
|
1200
|
-
swarm: self,
|
|
1201
|
-
metadata: {
|
|
1202
|
-
swarm_name: @name,
|
|
1203
|
-
lead_agent: @lead_agent,
|
|
1204
|
-
last_agent: result.agent, # Agent that produced the final response
|
|
1205
|
-
content: result.content, # Final response content
|
|
1206
|
-
success: result.success?,
|
|
1207
|
-
duration: result.duration,
|
|
1208
|
-
total_cost: result.total_cost,
|
|
1209
|
-
total_tokens: result.total_tokens,
|
|
1210
|
-
agents_involved: result.agents_involved,
|
|
1211
|
-
result: result, # Include full result for hook access
|
|
1212
|
-
timestamp: Time.now.utc.iso8601,
|
|
1213
|
-
},
|
|
1214
|
-
)
|
|
1215
|
-
|
|
1216
|
-
executor = Hooks::Executor.new(@hook_registry, logger: RubyLLM.logger)
|
|
1217
|
-
hook_result = executor.execute_safe(event: :swarm_stop, context: context, callbacks: [])
|
|
1218
|
-
|
|
1219
|
-
# Return hook result so caller can handle reprompt
|
|
1220
|
-
hook_result
|
|
1221
|
-
rescue StandardError => e
|
|
1222
|
-
RubyLLM.logger.error("SwarmSDK: Error in swarm_stop hook: #{e.message}")
|
|
1223
|
-
nil
|
|
1224
|
-
end
|
|
1225
|
-
|
|
1226
|
-
# Trigger first_message hooks when first user message is sent
|
|
1227
|
-
#
|
|
1228
|
-
# This is a swarm-level event that fires once on the first call to execute().
|
|
1229
|
-
# Hooks can halt execution before the first message is sent.
|
|
1230
|
-
#
|
|
1231
|
-
# @param prompt [String] The first user message
|
|
1232
|
-
# @return [void]
|
|
1233
|
-
# @raise [Hooks::Error] If hook halts execution
|
|
1234
|
-
def trigger_first_message(prompt)
|
|
1235
|
-
return if @hook_registry.get_defaults(:first_message).empty?
|
|
1236
|
-
|
|
1237
|
-
context = Hooks::Context.new(
|
|
1238
|
-
event: :first_message,
|
|
1239
|
-
agent_name: @lead_agent.to_s,
|
|
1240
|
-
swarm: self,
|
|
1241
|
-
metadata: {
|
|
1242
|
-
swarm_name: @name,
|
|
1243
|
-
lead_agent: @lead_agent,
|
|
1244
|
-
prompt: prompt,
|
|
1245
|
-
timestamp: Time.now.utc.iso8601,
|
|
1246
|
-
},
|
|
1247
|
-
)
|
|
1248
|
-
|
|
1249
|
-
executor = Hooks::Executor.new(@hook_registry, logger: RubyLLM.logger)
|
|
1250
|
-
result = executor.execute_safe(event: :first_message, context: context, callbacks: [])
|
|
1251
|
-
|
|
1252
|
-
# Halt execution if hook requests it
|
|
1253
|
-
raise Hooks::Error, "First message halted by hook: #{result.value}" if result.halt?
|
|
1254
|
-
rescue StandardError => e
|
|
1255
|
-
RubyLLM.logger.error("SwarmSDK: Error in first_message hook: #{e.message}")
|
|
1256
|
-
raise
|
|
1257
|
-
end
|
|
1258
715
|
end
|
|
1259
716
|
end
|