claude_swarm 1.0.9 → 1.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. checksums.yaml +4 -4
  2. data/{CHANGELOG.md → CHANGELOG.claude-swarm.md} +10 -0
  3. data/CLAUDE.md +346 -191
  4. data/decisions/2025-11-22-001-global-agent-registry.md +172 -0
  5. data/docs/v2/CHANGELOG.swarm_cli.md +20 -0
  6. data/docs/v2/CHANGELOG.swarm_memory.md +146 -1
  7. data/docs/v2/CHANGELOG.swarm_sdk.md +433 -10
  8. data/docs/v2/README.md +20 -5
  9. data/docs/v2/guides/complete-tutorial.md +95 -9
  10. data/docs/v2/guides/getting-started.md +10 -8
  11. data/docs/v2/guides/memory-adapters.md +41 -0
  12. data/docs/v2/guides/migrating-to-2.x.md +746 -0
  13. data/docs/v2/guides/plugins.md +52 -5
  14. data/docs/v2/guides/rails-integration.md +6 -0
  15. data/docs/v2/guides/snapshots.md +14 -14
  16. data/docs/v2/guides/swarm-memory.md +2 -13
  17. data/docs/v2/reference/architecture-flow.md +3 -3
  18. data/docs/v2/reference/cli.md +0 -1
  19. data/docs/v2/reference/configuration_reference.md +300 -0
  20. data/docs/v2/reference/event_payload_structures.md +27 -5
  21. data/docs/v2/reference/ruby-dsl.md +614 -18
  22. data/docs/v2/reference/swarm_memory_technical_details.md +7 -29
  23. data/docs/v2/reference/yaml.md +172 -54
  24. data/examples/snapshot_demo.rb +2 -2
  25. data/lib/claude_swarm/mcp_generator.rb +8 -21
  26. data/lib/claude_swarm/orchestrator.rb +8 -1
  27. data/lib/claude_swarm/version.rb +1 -1
  28. data/lib/swarm_cli/commands/run.rb +2 -2
  29. data/lib/swarm_cli/config_loader.rb +11 -11
  30. data/lib/swarm_cli/formatters/human_formatter.rb +0 -33
  31. data/lib/swarm_cli/interactive_repl.rb +2 -2
  32. data/lib/swarm_cli/ui/icons.rb +0 -23
  33. data/lib/swarm_cli/version.rb +1 -1
  34. data/lib/swarm_memory/adapters/filesystem_adapter.rb +11 -34
  35. data/lib/swarm_memory/core/semantic_index.rb +10 -2
  36. data/lib/swarm_memory/core/storage.rb +7 -2
  37. data/lib/swarm_memory/dsl/memory_config.rb +37 -0
  38. data/lib/swarm_memory/integration/sdk_plugin.rb +201 -28
  39. data/lib/swarm_memory/optimization/defragmenter.rb +1 -1
  40. data/lib/swarm_memory/prompts/memory_researcher.md.erb +0 -1
  41. data/lib/swarm_memory/tools/load_skill.rb +0 -1
  42. data/lib/swarm_memory/tools/memory_edit.rb +2 -1
  43. data/lib/swarm_memory/tools/memory_read.rb +1 -1
  44. data/lib/swarm_memory/version.rb +1 -1
  45. data/lib/swarm_memory.rb +8 -6
  46. data/lib/swarm_sdk/agent/builder.rb +58 -0
  47. data/lib/swarm_sdk/agent/chat.rb +527 -1061
  48. data/lib/swarm_sdk/agent/{chat → chat_helpers}/context_tracker.rb +13 -88
  49. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +204 -0
  50. data/lib/swarm_sdk/agent/{chat → chat_helpers}/hook_integration.rb +108 -46
  51. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +78 -0
  52. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +267 -0
  53. data/lib/swarm_sdk/agent/{chat → chat_helpers}/logging_helpers.rb +3 -3
  54. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +83 -0
  55. data/lib/swarm_sdk/agent/{chat → chat_helpers}/system_reminder_injector.rb +11 -13
  56. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +79 -0
  57. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +146 -0
  58. data/lib/swarm_sdk/agent/context.rb +1 -2
  59. data/lib/swarm_sdk/agent/definition.rb +66 -154
  60. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +4 -2
  61. data/lib/swarm_sdk/agent/system_prompt_builder.rb +161 -0
  62. data/lib/swarm_sdk/agent_registry.rb +146 -0
  63. data/lib/swarm_sdk/builders/base_builder.rb +488 -0
  64. data/lib/swarm_sdk/concerns/cleanupable.rb +39 -0
  65. data/lib/swarm_sdk/concerns/snapshotable.rb +67 -0
  66. data/lib/swarm_sdk/concerns/validatable.rb +55 -0
  67. data/lib/swarm_sdk/config.rb +302 -0
  68. data/lib/swarm_sdk/configuration/parser.rb +373 -0
  69. data/lib/swarm_sdk/configuration/translator.rb +255 -0
  70. data/lib/swarm_sdk/configuration.rb +77 -546
  71. data/lib/swarm_sdk/context_compactor/token_counter.rb +2 -6
  72. data/lib/swarm_sdk/context_compactor.rb +6 -11
  73. data/lib/swarm_sdk/context_management/builder.rb +128 -0
  74. data/lib/swarm_sdk/context_management/context.rb +328 -0
  75. data/lib/swarm_sdk/custom_tool_registry.rb +226 -0
  76. data/lib/swarm_sdk/defaults.rb +196 -0
  77. data/lib/swarm_sdk/events_to_messages.rb +18 -0
  78. data/lib/swarm_sdk/hooks/adapter.rb +3 -3
  79. data/lib/swarm_sdk/hooks/shell_executor.rb +4 -2
  80. data/lib/swarm_sdk/log_collector.rb +179 -29
  81. data/lib/swarm_sdk/log_stream.rb +29 -0
  82. data/lib/swarm_sdk/models.json +4333 -1
  83. data/lib/swarm_sdk/models.rb +43 -2
  84. data/lib/swarm_sdk/node_context.rb +1 -1
  85. data/lib/swarm_sdk/observer/builder.rb +81 -0
  86. data/lib/swarm_sdk/observer/config.rb +45 -0
  87. data/lib/swarm_sdk/observer/manager.rb +236 -0
  88. data/lib/swarm_sdk/patterns/agent_observer.rb +160 -0
  89. data/lib/swarm_sdk/plugin.rb +95 -5
  90. data/lib/swarm_sdk/result.rb +52 -0
  91. data/lib/swarm_sdk/snapshot.rb +6 -6
  92. data/lib/swarm_sdk/snapshot_from_events.rb +13 -2
  93. data/lib/swarm_sdk/state_restorer.rb +136 -151
  94. data/lib/swarm_sdk/state_snapshot.rb +65 -100
  95. data/lib/swarm_sdk/swarm/agent_initializer.rb +181 -137
  96. data/lib/swarm_sdk/swarm/builder.rb +44 -578
  97. data/lib/swarm_sdk/swarm/executor.rb +213 -0
  98. data/lib/swarm_sdk/swarm/hook_triggers.rb +151 -0
  99. data/lib/swarm_sdk/swarm/logging_callbacks.rb +341 -0
  100. data/lib/swarm_sdk/swarm/mcp_configurator.rb +7 -4
  101. data/lib/swarm_sdk/swarm/tool_configurator.rb +58 -140
  102. data/lib/swarm_sdk/swarm.rb +203 -683
  103. data/lib/swarm_sdk/tools/bash.rb +14 -8
  104. data/lib/swarm_sdk/tools/delegate.rb +61 -43
  105. data/lib/swarm_sdk/tools/edit.rb +8 -13
  106. data/lib/swarm_sdk/tools/glob.rb +12 -4
  107. data/lib/swarm_sdk/tools/grep.rb +7 -0
  108. data/lib/swarm_sdk/tools/multi_edit.rb +15 -11
  109. data/lib/swarm_sdk/tools/path_resolver.rb +51 -2
  110. data/lib/swarm_sdk/tools/read.rb +16 -18
  111. data/lib/swarm_sdk/tools/registry.rb +122 -10
  112. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +9 -5
  113. data/lib/swarm_sdk/tools/stores/storage.rb +0 -6
  114. data/lib/swarm_sdk/tools/todo_write.rb +7 -0
  115. data/lib/swarm_sdk/tools/web_fetch.rb +20 -17
  116. data/lib/swarm_sdk/tools/write.rb +8 -13
  117. data/lib/swarm_sdk/version.rb +1 -1
  118. data/lib/swarm_sdk/{node → workflow}/agent_config.rb +1 -1
  119. data/lib/swarm_sdk/workflow/builder.rb +192 -0
  120. data/lib/swarm_sdk/workflow/executor.rb +497 -0
  121. data/lib/swarm_sdk/{node/builder.rb → workflow/node_builder.rb} +7 -5
  122. data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +5 -3
  123. data/lib/swarm_sdk/{node_orchestrator.rb → workflow.rb} +152 -456
  124. data/lib/swarm_sdk.rb +294 -108
  125. data/rubocop/cop/security/no_reflection_methods.rb +1 -1
  126. data/swarm_cli.gemspec +1 -1
  127. data/swarm_memory.gemspec +8 -3
  128. data/swarm_sdk.gemspec +6 -4
  129. data/team_full.yml +124 -320
  130. metadata +42 -14
  131. data/lib/swarm_memory/chat_extension.rb +0 -34
  132. data/lib/swarm_memory/tools/memory_multi_edit.rb +0 -281
  133. data/lib/swarm_sdk/providers/openai_with_responses.rb +0 -589
  134. /data/lib/swarm_memory/{errors.rb → error.rb} +0 -0
@@ -61,14 +61,18 @@ module SwarmSDK
61
61
  # - McpConfigurator: MCP client management (via AgentInitializer)
62
62
  #
63
63
  class Swarm
64
- DEFAULT_GLOBAL_CONCURRENCY = 50
65
- DEFAULT_LOCAL_CONCURRENCY = 10
66
- DEFAULT_MCP_LOG_LEVEL = Logger::WARN
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 = DEFAULT_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 = DEFAULT_MCP_LOG_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: DEFAULT_GLOBAL_CONCURRENCY, default_local_concurrency: DEFAULT_LOCAL_CONCURRENCY, scratchpad: nil, scratchpad_mode: :enabled, allow_filesystem_tools: nil)
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 settings
155
+ # 2. Global config
152
156
  @allow_filesystem_tools = if allow_filesystem_tools.nil?
153
- SwarmSDK.settings.allow_filesystem_tools
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] Execution result
262
- def execute(prompt, &block)
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
- # Force cleanup of any lingering scheduler from previous requests
270
- # This ensures we always take the clean Path C in Async()
271
- # See: Async expert analysis - prevents scheduler leak in Puma
272
- if Fiber.scheduler && !Async::Task.current?
273
- Fiber.set_scheduler(nil)
274
- end
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 block_given?
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
- # Set LogStream to use LogCollector as emitter
295
- LogStream.emitter = LogCollector
296
- end
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
- # Hook can append stdout to prompt (exit code 0)
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
- # If agents were initialized BEFORE logging was set up (e.g., via restore()),
317
- # we need to retroactively set up logging callbacks and emit agent_start events
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
- result
380
- rescue ConfigurationError, AgentNotFoundError
381
- # Re-raise configuration errors - these should be fixed, not caught
382
- raise
383
- rescue TypeError => e
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
- duration: Time.now - start_time,
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,87 +366,59 @@ module SwarmSDK
487
366
  @agent_definitions.keys
488
367
  end
489
368
 
490
- # Validate swarm configuration and return warnings
369
+ # Get context usage breakdown for all agents
491
370
  #
492
- # This performs lightweight validation checks without creating agents.
493
- # Useful for displaying configuration warnings before execution.
371
+ # Returns per-agent context statistics including tokens used, context limit,
372
+ # usage percentage, and cost. Useful for monitoring context window consumption
373
+ # across the swarm.
494
374
  #
495
- # @return [Array<Hash>] Array of warning hashes from all agent definitions
375
+ # @return [Hash{Symbol => Hash}] Per-agent context breakdown
496
376
  #
497
377
  # @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 }
505
- end
378
+ # breakdown = swarm.context_breakdown
379
+ # breakdown[:backend]
380
+ # # => {
381
+ # # input_tokens: 15000,
382
+ # # output_tokens: 5000,
383
+ # # total_tokens: 20000,
384
+ # # cached_tokens: 2000,
385
+ # # context_limit: 200000,
386
+ # # usage_percentage: 10.0,
387
+ # # tokens_remaining: 180000,
388
+ # # input_cost: 0.045,
389
+ # # output_cost: 0.075,
390
+ # # total_cost: 0.12
391
+ # # }
392
+ def context_breakdown
393
+ initialize_agents unless @agents_initialized
506
394
 
507
- # Emit validation warnings as log events
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
395
+ breakdown = {}
539
396
 
540
- warnings
541
- end
397
+ # Include primary agents
398
+ @agents.each do |name, chat|
399
+ breakdown[name] = build_agent_context_info(chat)
400
+ end
542
401
 
543
- # Cleanup all MCP clients
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
402
+ # Include delegation instances
403
+ @delegation_instances.each do |instance_name, chat|
404
+ breakdown[instance_name.to_sym] = build_agent_context_info(chat)
563
405
  end
564
406
 
565
- @mcp_clients.clear
407
+ breakdown
408
+ end
566
409
 
567
- # Clear delegation instances (V7.0: Added for completeness)
568
- @delegation_instances&.clear
410
+ # Implement Snapshotable interface
411
+ def primary_agents
412
+ @agents
569
413
  end
570
414
 
415
+ def delegation_instances_hash
416
+ @delegation_instances
417
+ end
418
+
419
+ # NOTE: validate() and emit_validation_warnings() are provided by Concerns::Validatable
420
+ # Note: cleanup() is provided by Concerns::Cleanupable
421
+
571
422
  # Register a named hook that can be referenced in agent configurations
572
423
  #
573
424
  # Named hooks are stored in the registry and can be referenced by symbol
@@ -586,26 +437,6 @@ module SwarmSDK
586
437
  self
587
438
  end
588
439
 
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
440
  # Reset context for all agents
610
441
  #
611
442
  # Clears conversation history for all agents. This is used by composable swarms
@@ -618,6 +449,39 @@ module SwarmSDK
618
449
  end
619
450
  end
620
451
 
452
+ # Add observer configuration
453
+ #
454
+ # Called by Swarm::Builder to register observer agent configurations.
455
+ # Validates that the referenced agent exists.
456
+ #
457
+ # @param config [Observer::Config] Observer configuration
458
+ # @return [void]
459
+ def add_observer_config(config)
460
+ validate_observer_agent(config.agent_name)
461
+ @observer_configs << config
462
+ end
463
+
464
+ # Wait for all observer tasks to complete
465
+ #
466
+ # Called by Executor to wait for observer agents before cleanup.
467
+ # Safe to call even if no observers are configured.
468
+ #
469
+ # @return [void]
470
+ def wait_for_observers
471
+ @observer_manager&.wait_for_completion
472
+ end
473
+
474
+ # Cleanup observer subscriptions
475
+ #
476
+ # Called by Executor.cleanup_after_execution to unsubscribe observers.
477
+ # Matches the MCP cleanup pattern.
478
+ #
479
+ # @return [void]
480
+ def cleanup_observers
481
+ @observer_manager&.cleanup
482
+ @observer_manager = nil
483
+ end
484
+
621
485
  # Create snapshot of current conversation state
622
486
  #
623
487
  # Returns a Snapshot object containing:
@@ -705,12 +569,76 @@ module SwarmSDK
705
569
  # @return [void]
706
570
  attr_writer :swarm_registry
707
571
 
572
+ # --- Internal API (for Executor use only) ---
573
+ # Hook triggers for swarm lifecycle events are provided by HookTriggers module
574
+
708
575
  private
709
576
 
577
+ # Apply swarm_start hooks to prompt
578
+ #
579
+ # @param prompt [String] Original prompt
580
+ # @return [String] Modified prompt (possibly with hook context appended)
581
+ def apply_swarm_start_hooks(prompt)
582
+ swarm_start_result = trigger_swarm_start(prompt)
583
+ if swarm_start_result&.replace?
584
+ "#{prompt}\n\n<hook-context>\n#{swarm_start_result.value}\n</hook-context>"
585
+ else
586
+ prompt
587
+ end
588
+ end
589
+
590
+ # Build context info hash for an agent chat instance
591
+ #
592
+ # @param chat [Agent::Chat] Agent chat instance with TokenTracking
593
+ # @return [Hash] Context usage information
594
+ def build_agent_context_info(chat)
595
+ return {} unless chat.respond_to?(:cumulative_input_tokens)
596
+
597
+ {
598
+ input_tokens: chat.cumulative_input_tokens,
599
+ output_tokens: chat.cumulative_output_tokens,
600
+ total_tokens: chat.cumulative_total_tokens,
601
+ cached_tokens: chat.cumulative_cached_tokens,
602
+ cache_creation_tokens: chat.cumulative_cache_creation_tokens,
603
+ effective_input_tokens: chat.effective_input_tokens,
604
+ context_limit: chat.context_limit,
605
+ usage_percentage: chat.context_usage_percentage,
606
+ tokens_remaining: chat.tokens_remaining,
607
+ input_cost: chat.cumulative_input_cost,
608
+ output_cost: chat.cumulative_output_cost,
609
+ total_cost: chat.cumulative_total_cost,
610
+ }
611
+ end
612
+
613
+ # Validate that observer agent exists
614
+ #
615
+ # @param agent_name [Symbol] Name of the observer agent
616
+ # @raise [ConfigurationError] If agent not found
617
+ # @return [void]
618
+ def validate_observer_agent(agent_name)
619
+ return if @agent_definitions.key?(agent_name)
620
+
621
+ raise ConfigurationError,
622
+ "Observer agent '#{agent_name}' not found. " \
623
+ "Define the agent first with `agent :#{agent_name} do ... end`"
624
+ end
625
+
626
+ # Setup observer manager and subscriptions
627
+ #
628
+ # Creates Observer::Manager and registers event subscriptions.
629
+ # Must be called AFTER setup_logging (which clears Fiber[:log_subscriptions]).
630
+ #
631
+ # @return [void]
632
+ def setup_observer_execution
633
+ @observer_manager = Observer::Manager.new(self)
634
+ @observer_configs.each { |c| @observer_manager.add_config(c) }
635
+ @observer_manager.setup
636
+ end
637
+
710
638
  # Validate and normalize scratchpad mode for Swarm
711
639
  #
712
640
  # Regular Swarms support :enabled or :disabled.
713
- # Rejects :per_node since it only makes sense for NodeOrchestrator with multiple nodes.
641
+ # Rejects :per_node since it only makes sense for Workflow with multiple nodes.
714
642
  #
715
643
  # @param value [Symbol, String] Scratchpad mode (strings from YAML converted to symbols)
716
644
  # @return [Symbol] :enabled or :disabled
@@ -724,7 +652,7 @@ module SwarmSDK
724
652
  value
725
653
  when :per_node
726
654
  raise ArgumentError,
727
- "scratchpad: :per_node is only valid for NodeOrchestrator with nodes. " \
655
+ "scratchpad: :per_node is only valid for Workflow with nodes. " \
728
656
  "For regular Swarms, use :enabled or :disabled."
729
657
  else
730
658
  raise ArgumentError,
@@ -764,15 +692,7 @@ module SwarmSDK
764
692
  def initialize_agents
765
693
  return if @agents_initialized
766
694
 
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
- )
695
+ initializer = AgentInitializer.new(self)
776
696
 
777
697
  @agents = initializer.initialize_all
778
698
  @agent_contexts = initializer.agent_contexts
@@ -782,85 +702,6 @@ module SwarmSDK
782
702
  # This ensures events are never lost, even if agents are initialized early (e.g., by restore())
783
703
  end
784
704
 
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
705
  # Normalize tools to internal format (kept for add_agent)
865
706
  #
866
707
  # Handles both Ruby API (simple symbols) and YAML API (already parsed configs)
@@ -908,7 +749,7 @@ module SwarmSDK
908
749
 
909
750
  # Create delegation tool (delegates to AgentInitializer)
910
751
  def create_delegation_tool(name:, description:, delegate_chat:, agent_name:)
911
- AgentInitializer.new(self, @agent_definitions, @global_semaphore, @hook_registry, @scratchpad_storage, @plugin_storages)
752
+ AgentInitializer.new(self)
912
753
  .create_delegation_tool(name: name, description: description, delegate_chat: delegate_chat, agent_name: agent_name)
913
754
  end
914
755
 
@@ -935,326 +776,5 @@ module SwarmSDK
935
776
  # Unknown config type
936
777
  nil
937
778
  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
- source: context.metadata[:source] || "user",
1001
- metadata: context.metadata,
1002
- )
1003
- end
1004
-
1005
- # Log intermediate agent responses with tool calls
1006
- add_default_callback(:agent_step, priority: -100) do |context|
1007
- # Only log if LogStream emitter is set (logging enabled)
1008
- next unless LogStream.emitter
1009
-
1010
- # Extract top-level fields and remove from metadata to avoid duplication
1011
- metadata_without_duplicates = context.metadata.except(:model, :content, :tool_calls, :finish_reason, :usage, :tool_executions)
1012
-
1013
- LogStream.emit(
1014
- type: "agent_step",
1015
- agent: context.agent_name,
1016
- swarm_id: @swarm_id,
1017
- parent_swarm_id: @parent_swarm_id,
1018
- model: context.metadata[:model],
1019
- content: context.metadata[:content],
1020
- tool_calls: context.metadata[:tool_calls],
1021
- finish_reason: context.metadata[:finish_reason],
1022
- usage: context.metadata[:usage],
1023
- tool_executions: context.metadata[:tool_executions],
1024
- metadata: metadata_without_duplicates,
1025
- )
1026
- end
1027
-
1028
- # Log final agent responses
1029
- add_default_callback(:agent_stop, priority: -100) do |context|
1030
- # Only log if LogStream emitter is set (logging enabled)
1031
- next unless LogStream.emitter
1032
-
1033
- # Extract top-level fields and remove from metadata to avoid duplication
1034
- metadata_without_duplicates = context.metadata.except(:model, :content, :tool_calls, :finish_reason, :usage, :tool_executions)
1035
-
1036
- LogStream.emit(
1037
- type: "agent_stop",
1038
- agent: context.agent_name,
1039
- swarm_id: @swarm_id,
1040
- parent_swarm_id: @parent_swarm_id,
1041
- model: context.metadata[:model],
1042
- content: context.metadata[:content],
1043
- tool_calls: context.metadata[:tool_calls],
1044
- finish_reason: context.metadata[:finish_reason],
1045
- usage: context.metadata[:usage],
1046
- tool_executions: context.metadata[:tool_executions],
1047
- metadata: metadata_without_duplicates,
1048
- )
1049
- end
1050
-
1051
- # Log tool calls (pre_tool_use)
1052
- add_default_callback(:pre_tool_use, priority: -100) do |context|
1053
- # Only log if LogStream emitter is set (logging enabled)
1054
- next unless LogStream.emitter
1055
-
1056
- # Delegation tracking is handled separately in AgentChat
1057
- # Just log the tool call - delegation info will be in metadata if needed
1058
- LogStream.emit(
1059
- type: "tool_call",
1060
- agent: context.agent_name,
1061
- swarm_id: @swarm_id,
1062
- parent_swarm_id: @parent_swarm_id,
1063
- tool_call_id: context.tool_call.id,
1064
- tool: context.tool_call.name,
1065
- arguments: context.tool_call.parameters,
1066
- metadata: context.metadata,
1067
- )
1068
- end
1069
-
1070
- # Log tool results (post_tool_use)
1071
- add_default_callback(:post_tool_use, priority: -100) do |context|
1072
- # Only log if LogStream emitter is set (logging enabled)
1073
- next unless LogStream.emitter
1074
-
1075
- # Delegation tracking is handled separately in AgentChat
1076
- # Usage tracking is handled in agent_step/agent_stop events
1077
- LogStream.emit(
1078
- type: "tool_result",
1079
- agent: context.agent_name,
1080
- swarm_id: @swarm_id,
1081
- parent_swarm_id: @parent_swarm_id,
1082
- tool_call_id: context.tool_result.tool_call_id,
1083
- tool: context.tool_result.tool_name,
1084
- result: context.tool_result.content,
1085
- metadata: context.metadata,
1086
- )
1087
- end
1088
-
1089
- # Log context warnings
1090
- add_default_callback(:context_warning, priority: -100) do |context|
1091
- # Only log if LogStream emitter is set (logging enabled)
1092
- next unless LogStream.emitter
1093
-
1094
- LogStream.emit(
1095
- type: "context_limit_warning",
1096
- agent: context.agent_name,
1097
- swarm_id: @swarm_id,
1098
- parent_swarm_id: @parent_swarm_id,
1099
- model: context.metadata[:model] || "unknown",
1100
- threshold: "#{context.metadata[:threshold]}%",
1101
- current_usage: "#{context.metadata[:percentage]}%",
1102
- tokens_used: context.metadata[:tokens_used],
1103
- tokens_remaining: context.metadata[:tokens_remaining],
1104
- context_limit: context.metadata[:context_limit],
1105
- metadata: context.metadata,
1106
- )
1107
- end
1108
- end
1109
-
1110
- # Trigger swarm_start hooks when swarm execution begins
1111
- #
1112
- # This is a swarm-level event that fires when Swarm.execute is called
1113
- # (before first user message is sent). Hooks can halt execution or append stdout to prompt.
1114
- # Default callback emits to LogStream for logging.
1115
- #
1116
- # @param prompt [String] The user's task prompt
1117
- # @return [Hooks::Result, nil] Result with stdout to append (if exit 0) or nil
1118
- # @raise [Hooks::Error] If hook halts execution
1119
- def trigger_swarm_start(prompt)
1120
- context = Hooks::Context.new(
1121
- event: :swarm_start,
1122
- agent_name: @lead_agent.to_s,
1123
- swarm: self,
1124
- metadata: {
1125
- swarm_name: @name,
1126
- lead_agent: @lead_agent,
1127
- prompt: prompt,
1128
- timestamp: Time.now.utc.iso8601,
1129
- },
1130
- )
1131
-
1132
- executor = Hooks::Executor.new(@hook_registry, logger: RubyLLM.logger)
1133
- result = executor.execute_safe(event: :swarm_start, context: context, callbacks: [])
1134
-
1135
- # Halt execution if hook requests it
1136
- raise Hooks::Error, "Swarm start halted by hook: #{result.value}" if result.halt?
1137
-
1138
- # Return result so caller can check for replace (stdout injection)
1139
- result
1140
- rescue StandardError => e
1141
- RubyLLM.logger.error("SwarmSDK: Error in swarm_start hook: #{e.message}")
1142
- raise
1143
- end
1144
-
1145
- # Trigger swarm_stop for final event emission (called in ensure block)
1146
- #
1147
- # This ALWAYS emits the swarm_stop event, even if there was an error.
1148
- # It does NOT check for reprompt (that's done in trigger_swarm_stop_for_reprompt_check).
1149
- #
1150
- # @param result [Result, nil] Execution result (may be nil if exception before result created)
1151
- # @param start_time [Time] Execution start time
1152
- # @param logs [Array] Collected logs
1153
- # @return [void]
1154
- def trigger_swarm_stop_final(result, start_time, logs)
1155
- # Create a minimal result if one doesn't exist (exception before result created)
1156
- result ||= Result.new(
1157
- content: nil,
1158
- agent: @lead_agent&.to_s || "unknown",
1159
- logs: logs,
1160
- duration: Time.now - start_time,
1161
- error: StandardError.new("Unknown error"),
1162
- )
1163
-
1164
- context = Hooks::Context.new(
1165
- event: :swarm_stop,
1166
- agent_name: @lead_agent.to_s,
1167
- swarm: self,
1168
- metadata: {
1169
- swarm_name: @name,
1170
- lead_agent: @lead_agent,
1171
- last_agent: result.agent, # Agent that produced the final response
1172
- content: result.content, # Final response content
1173
- success: result.success?,
1174
- duration: result.duration,
1175
- total_cost: result.total_cost,
1176
- total_tokens: result.total_tokens,
1177
- agents_involved: result.agents_involved,
1178
- result: result,
1179
- timestamp: Time.now.utc.iso8601,
1180
- },
1181
- )
1182
-
1183
- executor = Hooks::Executor.new(@hook_registry, logger: RubyLLM.logger)
1184
- executor.execute_safe(event: :swarm_stop, context: context, callbacks: [])
1185
- rescue StandardError => e
1186
- # Don't let swarm_stop errors break the ensure block
1187
- RubyLLM.logger.error("SwarmSDK: Error in swarm_stop final emission: #{e.message}")
1188
- end
1189
-
1190
- # Trigger swarm_stop hooks for reprompt check and event emission
1191
- #
1192
- # This is called in the normal execution flow to check if hooks request reprompting.
1193
- # The default callback also emits the swarm_stop event to LogStream.
1194
- #
1195
- # @param result [Result] The execution result
1196
- # @return [Hooks::Result, nil] Hook result (reprompt action if applicable)
1197
- def trigger_swarm_stop(result)
1198
- context = Hooks::Context.new(
1199
- event: :swarm_stop,
1200
- agent_name: @lead_agent.to_s,
1201
- swarm: self,
1202
- metadata: {
1203
- swarm_name: @name,
1204
- lead_agent: @lead_agent,
1205
- last_agent: result.agent, # Agent that produced the final response
1206
- content: result.content, # Final response content
1207
- success: result.success?,
1208
- duration: result.duration,
1209
- total_cost: result.total_cost,
1210
- total_tokens: result.total_tokens,
1211
- agents_involved: result.agents_involved,
1212
- result: result, # Include full result for hook access
1213
- timestamp: Time.now.utc.iso8601,
1214
- },
1215
- )
1216
-
1217
- executor = Hooks::Executor.new(@hook_registry, logger: RubyLLM.logger)
1218
- hook_result = executor.execute_safe(event: :swarm_stop, context: context, callbacks: [])
1219
-
1220
- # Return hook result so caller can handle reprompt
1221
- hook_result
1222
- rescue StandardError => e
1223
- RubyLLM.logger.error("SwarmSDK: Error in swarm_stop hook: #{e.message}")
1224
- nil
1225
- end
1226
-
1227
- # Trigger first_message hooks when first user message is sent
1228
- #
1229
- # This is a swarm-level event that fires once on the first call to execute().
1230
- # Hooks can halt execution before the first message is sent.
1231
- #
1232
- # @param prompt [String] The first user message
1233
- # @return [void]
1234
- # @raise [Hooks::Error] If hook halts execution
1235
- def trigger_first_message(prompt)
1236
- return if @hook_registry.get_defaults(:first_message).empty?
1237
-
1238
- context = Hooks::Context.new(
1239
- event: :first_message,
1240
- agent_name: @lead_agent.to_s,
1241
- swarm: self,
1242
- metadata: {
1243
- swarm_name: @name,
1244
- lead_agent: @lead_agent,
1245
- prompt: prompt,
1246
- timestamp: Time.now.utc.iso8601,
1247
- },
1248
- )
1249
-
1250
- executor = Hooks::Executor.new(@hook_registry, logger: RubyLLM.logger)
1251
- result = executor.execute_safe(event: :first_message, context: context, callbacks: [])
1252
-
1253
- # Halt execution if hook requests it
1254
- raise Hooks::Error, "First message halted by hook: #{result.value}" if result.halt?
1255
- rescue StandardError => e
1256
- RubyLLM.logger.error("SwarmSDK: Error in first_message hook: #{e.message}")
1257
- raise
1258
- end
1259
779
  end
1260
780
  end