swarm_sdk 2.2.0 → 2.3.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 +233 -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 +12 -12
- 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 +2 -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/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 +3 -3
- 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/shell_executor.rb +2 -1
- data/lib/swarm_sdk/log_collector.rb +179 -29
- data/lib/swarm_sdk/log_stream.rb +29 -0
- 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 +42 -138
- data/lib/swarm_sdk/swarm.rb +137 -679
- data/lib/swarm_sdk/tools/bash.rb +11 -3
- 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 +9 -1
- 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 +11 -13
- data/lib/swarm_sdk/tools/registry.rb +122 -10
- data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +8 -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 +3 -2
- 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} +3 -3
- data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +3 -2
- data/lib/swarm_sdk/{node_orchestrator.rb → workflow.rb} +152 -456
- data/lib/swarm_sdk.rb +33 -3
- metadata +67 -15
- data/lib/swarm_sdk/providers/openai_with_responses.rb +0 -589
data/lib/swarm_sdk/swarm.rb
CHANGED
|
@@ -61,14 +61,19 @@ 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
|
+
# Backward compatibility aliases - use Defaults module for new code
|
|
71
|
+
DEFAULT_MCP_LOG_LEVEL = Defaults::Logging::MCP_LOG_LEVEL
|
|
67
72
|
|
|
68
73
|
# Default tools available to all agents
|
|
69
74
|
DEFAULT_TOOLS = ToolConfigurator::DEFAULT_TOOLS
|
|
70
75
|
|
|
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
|
|
76
|
+
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
77
|
attr_accessor :delegation_call_stack
|
|
73
78
|
|
|
74
79
|
# Check if scratchpad tools are enabled
|
|
@@ -135,7 +140,7 @@ module SwarmSDK
|
|
|
135
140
|
# @param scratchpad [Tools::Stores::Scratchpad, nil] Optional scratchpad instance (for testing/internal use)
|
|
136
141
|
# @param scratchpad_mode [Symbol, String] Scratchpad mode (:enabled or :disabled). :per_node not allowed for non-node swarms.
|
|
137
142
|
# @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:
|
|
143
|
+
def initialize(name:, swarm_id: nil, parent_swarm_id: nil, global_concurrency: Defaults::Concurrency::GLOBAL_LIMIT, default_local_concurrency: Defaults::Concurrency::LOCAL_LIMIT, scratchpad: nil, scratchpad_mode: :enabled, allow_filesystem_tools: nil)
|
|
139
144
|
@name = name
|
|
140
145
|
@swarm_id = swarm_id || generate_swarm_id(name)
|
|
141
146
|
@parent_swarm_id = parent_swarm_id
|
|
@@ -201,6 +206,10 @@ module SwarmSDK
|
|
|
201
206
|
# Track if agent_start events have been emitted
|
|
202
207
|
# This prevents duplicate emissions and ensures events are emitted when logging is ready
|
|
203
208
|
@agent_start_events_emitted = false
|
|
209
|
+
|
|
210
|
+
# Observer agent configurations
|
|
211
|
+
@observer_configs = []
|
|
212
|
+
@observer_manager = nil
|
|
204
213
|
end
|
|
205
214
|
|
|
206
215
|
# Add an agent to the swarm
|
|
@@ -256,53 +265,53 @@ module SwarmSDK
|
|
|
256
265
|
# and the entire swarm coordinates with shared rate limiting.
|
|
257
266
|
# Supports reprompting via swarm_stop hooks.
|
|
258
267
|
#
|
|
268
|
+
# By default, this method blocks until execution completes. Set wait: false
|
|
269
|
+
# to return an Async::Task immediately, enabling cancellation via task.stop.
|
|
270
|
+
#
|
|
259
271
|
# @param prompt [String] Task to execute
|
|
272
|
+
# @param wait [Boolean] If true (default), blocks until execution completes.
|
|
273
|
+
# If false, returns Async::Task immediately for non-blocking execution.
|
|
260
274
|
# @yield [Hash] Log entry if block given (for streaming)
|
|
261
|
-
# @return [Result]
|
|
262
|
-
|
|
275
|
+
# @return [Result, Async::Task] Result if wait: true, Async::Task if wait: false
|
|
276
|
+
#
|
|
277
|
+
# @example Blocking execution (default)
|
|
278
|
+
# result = swarm.execute("Build auth")
|
|
279
|
+
# puts result.content
|
|
280
|
+
#
|
|
281
|
+
# @example Non-blocking execution with cancellation
|
|
282
|
+
# task = swarm.execute("Build auth", wait: false) { |event| puts event }
|
|
283
|
+
# # ... do other work ...
|
|
284
|
+
# task.stop # Cancel anytime
|
|
285
|
+
# result = task.wait # Returns nil for cancelled tasks
|
|
286
|
+
def execute(prompt, wait: true, &block)
|
|
263
287
|
raise ConfigurationError, "No lead agent set. Set lead= first." unless @lead_agent
|
|
264
288
|
|
|
265
|
-
start_time = Time.now
|
|
266
289
|
logs = []
|
|
267
290
|
current_prompt = prompt
|
|
291
|
+
has_logging = block_given?
|
|
268
292
|
|
|
269
|
-
#
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
Fiber
|
|
274
|
-
|
|
293
|
+
# Save original Fiber storage for restoration (preserves parent context for nested swarms)
|
|
294
|
+
original_fiber_storage = {
|
|
295
|
+
execution_id: Fiber[:execution_id],
|
|
296
|
+
swarm_id: Fiber[:swarm_id],
|
|
297
|
+
parent_swarm_id: Fiber[:parent_swarm_id],
|
|
298
|
+
}
|
|
275
299
|
|
|
276
300
|
# Set fiber-local execution context
|
|
277
301
|
# Use ||= to inherit parent's execution_id if one exists (for mini-swarms)
|
|
278
|
-
# Child fibers (tools, delegations) inherit automatically
|
|
279
302
|
Fiber[:execution_id] ||= generate_execution_id
|
|
280
303
|
Fiber[:swarm_id] = @swarm_id
|
|
281
304
|
Fiber[:parent_swarm_id] = @parent_swarm_id
|
|
282
305
|
|
|
283
306
|
# 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
|
|
307
|
+
setup_logging(logs, &block) if has_logging
|
|
293
308
|
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
309
|
+
# Setup observer execution if any observers configured
|
|
310
|
+
# MUST happen AFTER setup_logging (which clears Fiber[:log_subscriptions])
|
|
311
|
+
setup_observer_execution if @observer_configs.any?
|
|
297
312
|
|
|
298
313
|
# 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
|
|
314
|
+
current_prompt = apply_swarm_start_hooks(current_prompt)
|
|
306
315
|
|
|
307
316
|
# Trigger first_message hooks on first execution
|
|
308
317
|
unless @first_message_sent
|
|
@@ -313,147 +322,18 @@ module SwarmSDK
|
|
|
313
322
|
# Lazy initialization of agents (with optional logging)
|
|
314
323
|
initialize_agents unless @agents_initialized
|
|
315
324
|
|
|
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
|
|
325
|
+
# Emit agent_start events if agents were initialized before logging was set up
|
|
326
|
+
emit_retroactive_agent_start_events if has_logging
|
|
378
327
|
|
|
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,
|
|
328
|
+
# Delegate to Executor for actual execution
|
|
329
|
+
executor = Executor.new(self)
|
|
330
|
+
@current_task = executor.run(
|
|
331
|
+
current_prompt,
|
|
332
|
+
wait: wait,
|
|
417
333
|
logs: logs,
|
|
418
|
-
|
|
334
|
+
has_logging: has_logging,
|
|
335
|
+
original_fiber_storage: original_fiber_storage,
|
|
419
336
|
)
|
|
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
337
|
end
|
|
458
338
|
|
|
459
339
|
# Get an agent chat instance by name
|
|
@@ -487,86 +367,17 @@ module SwarmSDK
|
|
|
487
367
|
@agent_definitions.keys
|
|
488
368
|
end
|
|
489
369
|
|
|
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 }
|
|
370
|
+
# Implement Snapshotable interface
|
|
371
|
+
def primary_agents
|
|
372
|
+
@agents
|
|
505
373
|
end
|
|
506
374
|
|
|
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
|
|
375
|
+
def delegation_instances_hash
|
|
376
|
+
@delegation_instances
|
|
541
377
|
end
|
|
542
378
|
|
|
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
|
|
379
|
+
# NOTE: validate() and emit_validation_warnings() are provided by Concerns::Validatable
|
|
380
|
+
# Note: cleanup() is provided by Concerns::Cleanupable
|
|
570
381
|
|
|
571
382
|
# Register a named hook that can be referenced in agent configurations
|
|
572
383
|
#
|
|
@@ -586,26 +397,6 @@ module SwarmSDK
|
|
|
586
397
|
self
|
|
587
398
|
end
|
|
588
399
|
|
|
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
400
|
# Reset context for all agents
|
|
610
401
|
#
|
|
611
402
|
# Clears conversation history for all agents. This is used by composable swarms
|
|
@@ -618,6 +409,39 @@ module SwarmSDK
|
|
|
618
409
|
end
|
|
619
410
|
end
|
|
620
411
|
|
|
412
|
+
# Add observer configuration
|
|
413
|
+
#
|
|
414
|
+
# Called by Swarm::Builder to register observer agent configurations.
|
|
415
|
+
# Validates that the referenced agent exists.
|
|
416
|
+
#
|
|
417
|
+
# @param config [Observer::Config] Observer configuration
|
|
418
|
+
# @return [void]
|
|
419
|
+
def add_observer_config(config)
|
|
420
|
+
validate_observer_agent(config.agent_name)
|
|
421
|
+
@observer_configs << config
|
|
422
|
+
end
|
|
423
|
+
|
|
424
|
+
# Wait for all observer tasks to complete
|
|
425
|
+
#
|
|
426
|
+
# Called by Executor to wait for observer agents before cleanup.
|
|
427
|
+
# Safe to call even if no observers are configured.
|
|
428
|
+
#
|
|
429
|
+
# @return [void]
|
|
430
|
+
def wait_for_observers
|
|
431
|
+
@observer_manager&.wait_for_completion
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
# Cleanup observer subscriptions
|
|
435
|
+
#
|
|
436
|
+
# Called by Executor.cleanup_after_execution to unsubscribe observers.
|
|
437
|
+
# Matches the MCP cleanup pattern.
|
|
438
|
+
#
|
|
439
|
+
# @return [void]
|
|
440
|
+
def cleanup_observers
|
|
441
|
+
@observer_manager&.cleanup
|
|
442
|
+
@observer_manager = nil
|
|
443
|
+
end
|
|
444
|
+
|
|
621
445
|
# Create snapshot of current conversation state
|
|
622
446
|
#
|
|
623
447
|
# Returns a Snapshot object containing:
|
|
@@ -705,12 +529,53 @@ module SwarmSDK
|
|
|
705
529
|
# @return [void]
|
|
706
530
|
attr_writer :swarm_registry
|
|
707
531
|
|
|
532
|
+
# --- Internal API (for Executor use only) ---
|
|
533
|
+
# Hook triggers for swarm lifecycle events are provided by HookTriggers module
|
|
534
|
+
|
|
708
535
|
private
|
|
709
536
|
|
|
537
|
+
# Apply swarm_start hooks to prompt
|
|
538
|
+
#
|
|
539
|
+
# @param prompt [String] Original prompt
|
|
540
|
+
# @return [String] Modified prompt (possibly with hook context appended)
|
|
541
|
+
def apply_swarm_start_hooks(prompt)
|
|
542
|
+
swarm_start_result = trigger_swarm_start(prompt)
|
|
543
|
+
if swarm_start_result&.replace?
|
|
544
|
+
"#{prompt}\n\n<hook-context>\n#{swarm_start_result.value}\n</hook-context>"
|
|
545
|
+
else
|
|
546
|
+
prompt
|
|
547
|
+
end
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
# Validate that observer agent exists
|
|
551
|
+
#
|
|
552
|
+
# @param agent_name [Symbol] Name of the observer agent
|
|
553
|
+
# @raise [ConfigurationError] If agent not found
|
|
554
|
+
# @return [void]
|
|
555
|
+
def validate_observer_agent(agent_name)
|
|
556
|
+
return if @agent_definitions.key?(agent_name)
|
|
557
|
+
|
|
558
|
+
raise ConfigurationError,
|
|
559
|
+
"Observer agent '#{agent_name}' not found. " \
|
|
560
|
+
"Define the agent first with `agent :#{agent_name} do ... end`"
|
|
561
|
+
end
|
|
562
|
+
|
|
563
|
+
# Setup observer manager and subscriptions
|
|
564
|
+
#
|
|
565
|
+
# Creates Observer::Manager and registers event subscriptions.
|
|
566
|
+
# Must be called AFTER setup_logging (which clears Fiber[:log_subscriptions]).
|
|
567
|
+
#
|
|
568
|
+
# @return [void]
|
|
569
|
+
def setup_observer_execution
|
|
570
|
+
@observer_manager = Observer::Manager.new(self)
|
|
571
|
+
@observer_configs.each { |c| @observer_manager.add_config(c) }
|
|
572
|
+
@observer_manager.setup
|
|
573
|
+
end
|
|
574
|
+
|
|
710
575
|
# Validate and normalize scratchpad mode for Swarm
|
|
711
576
|
#
|
|
712
577
|
# Regular Swarms support :enabled or :disabled.
|
|
713
|
-
# Rejects :per_node since it only makes sense for
|
|
578
|
+
# Rejects :per_node since it only makes sense for Workflow with multiple nodes.
|
|
714
579
|
#
|
|
715
580
|
# @param value [Symbol, String] Scratchpad mode (strings from YAML converted to symbols)
|
|
716
581
|
# @return [Symbol] :enabled or :disabled
|
|
@@ -724,7 +589,7 @@ module SwarmSDK
|
|
|
724
589
|
value
|
|
725
590
|
when :per_node
|
|
726
591
|
raise ArgumentError,
|
|
727
|
-
"scratchpad: :per_node is only valid for
|
|
592
|
+
"scratchpad: :per_node is only valid for Workflow with nodes. " \
|
|
728
593
|
"For regular Swarms, use :enabled or :disabled."
|
|
729
594
|
else
|
|
730
595
|
raise ArgumentError,
|
|
@@ -764,15 +629,7 @@ module SwarmSDK
|
|
|
764
629
|
def initialize_agents
|
|
765
630
|
return if @agents_initialized
|
|
766
631
|
|
|
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
|
-
)
|
|
632
|
+
initializer = AgentInitializer.new(self)
|
|
776
633
|
|
|
777
634
|
@agents = initializer.initialize_all
|
|
778
635
|
@agent_contexts = initializer.agent_contexts
|
|
@@ -782,85 +639,6 @@ module SwarmSDK
|
|
|
782
639
|
# This ensures events are never lost, even if agents are initialized early (e.g., by restore())
|
|
783
640
|
end
|
|
784
641
|
|
|
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
642
|
# Normalize tools to internal format (kept for add_agent)
|
|
865
643
|
#
|
|
866
644
|
# Handles both Ruby API (simple symbols) and YAML API (already parsed configs)
|
|
@@ -908,7 +686,7 @@ module SwarmSDK
|
|
|
908
686
|
|
|
909
687
|
# Create delegation tool (delegates to AgentInitializer)
|
|
910
688
|
def create_delegation_tool(name:, description:, delegate_chat:, agent_name:)
|
|
911
|
-
AgentInitializer.new(self
|
|
689
|
+
AgentInitializer.new(self)
|
|
912
690
|
.create_delegation_tool(name: name, description: description, delegate_chat: delegate_chat, agent_name: agent_name)
|
|
913
691
|
end
|
|
914
692
|
|
|
@@ -935,325 +713,5 @@ module SwarmSDK
|
|
|
935
713
|
# Unknown config type
|
|
936
714
|
nil
|
|
937
715
|
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
716
|
end
|
|
1259
717
|
end
|