claude_swarm 1.0.6 → 1.0.8

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 (107) hide show
  1. checksums.yaml +4 -4
  2. data/.ruby-version +1 -1
  3. data/CHANGELOG.md +27 -0
  4. data/README.md +336 -1037
  5. data/docs/V1_TO_V2_MIGRATION_GUIDE.md +1120 -0
  6. data/docs/v1/README.md +1197 -0
  7. data/docs/v2/CHANGELOG.swarm_cli.md +22 -0
  8. data/docs/v2/CHANGELOG.swarm_memory.md +20 -0
  9. data/docs/v2/CHANGELOG.swarm_sdk.md +287 -10
  10. data/docs/v2/README.md +32 -6
  11. data/docs/v2/guides/complete-tutorial.md +133 -37
  12. data/docs/v2/guides/composable-swarms.md +1178 -0
  13. data/docs/v2/guides/getting-started.md +42 -1
  14. data/docs/v2/guides/snapshots.md +1498 -0
  15. data/docs/v2/reference/architecture-flow.md +5 -3
  16. data/docs/v2/reference/event_payload_structures.md +249 -12
  17. data/docs/v2/reference/execution-flow.md +1 -1
  18. data/docs/v2/reference/ruby-dsl.md +368 -22
  19. data/docs/v2/reference/yaml.md +314 -63
  20. data/examples/snapshot_demo.rb +119 -0
  21. data/examples/v2/dsl/01_basic.rb +0 -2
  22. data/examples/v2/dsl/02_core_parameters.rb +0 -2
  23. data/examples/v2/dsl/03_capabilities.rb +0 -2
  24. data/examples/v2/dsl/04_llm_parameters.rb +0 -2
  25. data/examples/v2/dsl/05_advanced_flags.rb +0 -3
  26. data/examples/v2/dsl/06_permissions.rb +0 -4
  27. data/examples/v2/dsl/07_mcp_server.rb +0 -2
  28. data/examples/v2/dsl/08_swarm_hooks.rb +0 -2
  29. data/examples/v2/dsl/09_agent_hooks.rb +0 -2
  30. data/examples/v2/dsl/10_all_agents_hooks.rb +0 -3
  31. data/examples/v2/dsl/11_delegation.rb +0 -2
  32. data/examples/v2/dsl/12_complete_integration.rb +2 -6
  33. data/examples/v2/node_context_demo.rb +1 -1
  34. data/examples/v2/node_workflow.rb +2 -4
  35. data/examples/v2/plan_and_execute.rb +157 -0
  36. data/lib/claude_swarm/cli.rb +0 -18
  37. data/lib/claude_swarm/configuration.rb +28 -18
  38. data/lib/claude_swarm/openai/chat_completion.rb +2 -11
  39. data/lib/claude_swarm/openai/responses.rb +2 -11
  40. data/lib/claude_swarm/version.rb +1 -1
  41. data/lib/swarm_cli/formatters/human_formatter.rb +103 -0
  42. data/lib/swarm_cli/interactive_repl.rb +9 -3
  43. data/lib/swarm_cli/version.rb +1 -1
  44. data/lib/swarm_memory/core/storage_read_tracker.rb +51 -14
  45. data/lib/swarm_memory/integration/cli_registration.rb +3 -2
  46. data/lib/swarm_memory/integration/sdk_plugin.rb +11 -5
  47. data/lib/swarm_memory/tools/memory_edit.rb +2 -2
  48. data/lib/swarm_memory/tools/memory_multi_edit.rb +2 -2
  49. data/lib/swarm_memory/tools/memory_read.rb +3 -3
  50. data/lib/swarm_memory/version.rb +1 -1
  51. data/lib/swarm_memory.rb +5 -0
  52. data/lib/swarm_sdk/agent/builder.rb +33 -0
  53. data/lib/swarm_sdk/agent/chat/context_tracker.rb +33 -0
  54. data/lib/swarm_sdk/agent/chat/hook_integration.rb +49 -3
  55. data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +11 -27
  56. data/lib/swarm_sdk/agent/chat.rb +200 -51
  57. data/lib/swarm_sdk/agent/context.rb +6 -2
  58. data/lib/swarm_sdk/agent/context_manager.rb +6 -0
  59. data/lib/swarm_sdk/agent/definition.rb +14 -2
  60. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +180 -0
  61. data/lib/swarm_sdk/configuration.rb +387 -94
  62. data/lib/swarm_sdk/events_to_messages.rb +181 -0
  63. data/lib/swarm_sdk/log_collector.rb +31 -5
  64. data/lib/swarm_sdk/log_stream.rb +37 -8
  65. data/lib/swarm_sdk/model_aliases.json +4 -1
  66. data/lib/swarm_sdk/node/agent_config.rb +33 -8
  67. data/lib/swarm_sdk/node/builder.rb +39 -18
  68. data/lib/swarm_sdk/node_orchestrator.rb +293 -26
  69. data/lib/swarm_sdk/proc_helpers.rb +53 -0
  70. data/lib/swarm_sdk/providers/openai_with_responses.rb +22 -15
  71. data/lib/swarm_sdk/restore_result.rb +65 -0
  72. data/lib/swarm_sdk/snapshot.rb +156 -0
  73. data/lib/swarm_sdk/snapshot_from_events.rb +386 -0
  74. data/lib/swarm_sdk/state_restorer.rb +491 -0
  75. data/lib/swarm_sdk/state_snapshot.rb +369 -0
  76. data/lib/swarm_sdk/swarm/agent_initializer.rb +360 -55
  77. data/lib/swarm_sdk/swarm/all_agents_builder.rb +28 -1
  78. data/lib/swarm_sdk/swarm/builder.rb +208 -12
  79. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +67 -0
  80. data/lib/swarm_sdk/swarm/tool_configurator.rb +46 -11
  81. data/lib/swarm_sdk/swarm.rb +338 -42
  82. data/lib/swarm_sdk/swarm_loader.rb +145 -0
  83. data/lib/swarm_sdk/swarm_registry.rb +136 -0
  84. data/lib/swarm_sdk/tools/delegate.rb +92 -7
  85. data/lib/swarm_sdk/tools/read.rb +17 -5
  86. data/lib/swarm_sdk/tools/stores/read_tracker.rb +47 -12
  87. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +45 -0
  88. data/lib/swarm_sdk/utils.rb +18 -0
  89. data/lib/swarm_sdk/validation_result.rb +33 -0
  90. data/lib/swarm_sdk/version.rb +1 -1
  91. data/lib/swarm_sdk.rb +40 -8
  92. data/swarm_cli.gemspec +1 -1
  93. data/swarm_memory.gemspec +2 -2
  94. data/swarm_sdk.gemspec +2 -2
  95. metadata +21 -13
  96. data/examples/learning-assistant/assistant.md +0 -7
  97. data/examples/learning-assistant/example-memories/concept-example.md +0 -90
  98. data/examples/learning-assistant/example-memories/experience-example.md +0 -66
  99. data/examples/learning-assistant/example-memories/fact-example.md +0 -76
  100. data/examples/learning-assistant/example-memories/memory-index.md +0 -78
  101. data/examples/learning-assistant/example-memories/skill-example.md +0 -168
  102. data/examples/learning-assistant/learning_assistant.rb +0 -34
  103. data/examples/learning-assistant/learning_assistant.yml +0 -20
  104. data/lib/swarm_sdk/mcp.rb +0 -16
  105. data/llm.v2.txt +0 -13407
  106. /data/docs/v2/guides/{MEMORY_DEFRAG_GUIDE.md → memory-defrag-guide.md} +0 -0
  107. /data/{llms.txt → llms.claude-swarm.txt} +0 -0
@@ -68,16 +68,30 @@ module SwarmSDK
68
68
  # Default tools available to all agents
69
69
  DEFAULT_TOOLS = ToolConfigurator::DEFAULT_TOOLS
70
70
 
71
- attr_reader :name, :agents, :lead_agent, :mcp_clients
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
72
+ attr_accessor :delegation_call_stack
72
73
 
73
74
  # Check if scratchpad tools are enabled
74
75
  #
75
76
  # @return [Boolean]
76
77
  def scratchpad_enabled?
77
- @scratchpad_enabled
78
+ @scratchpad_mode == :enabled
78
79
  end
79
80
  attr_writer :config_for_hooks
80
81
 
82
+ # Check if first message has been sent (for system reminder injection)
83
+ #
84
+ # @return [Boolean]
85
+ def first_message_sent?
86
+ @first_message_sent
87
+ end
88
+
89
+ # Set first message sent flag (used by snapshot/restore)
90
+ #
91
+ # @param value [Boolean] New value
92
+ # @return [void]
93
+ attr_writer :first_message_sent
94
+
81
95
  # Class-level MCP log level configuration
82
96
  @mcp_log_level = DEFAULT_MCP_LOG_LEVEL
83
97
  @mcp_logging_configured = false
@@ -103,8 +117,6 @@ module SwarmSDK
103
117
  def apply_mcp_logging_configuration
104
118
  return if @mcp_logging_configured
105
119
 
106
- SwarmSDK::MCP.lazy_load
107
-
108
120
  RubyLLM::MCP.configure do |config|
109
121
  config.log_level = @mcp_log_level
110
122
  end
@@ -116,22 +128,49 @@ module SwarmSDK
116
128
  # Initialize a new Swarm
117
129
  #
118
130
  # @param name [String] Human-readable swarm name
131
+ # @param swarm_id [String, nil] Optional swarm ID (auto-generated if not provided)
132
+ # @param parent_swarm_id [String, nil] Optional parent swarm ID (nil for root swarms)
119
133
  # @param global_concurrency [Integer] Max concurrent LLM calls across entire swarm
120
134
  # @param default_local_concurrency [Integer] Default max concurrent tool calls per agent
121
- # @param scratchpad [Tools::Stores::Scratchpad, nil] Optional scratchpad instance (for testing)
122
- # @param scratchpad_enabled [Boolean] Whether to enable scratchpad tools (default: true)
123
- def initialize(name:, global_concurrency: DEFAULT_GLOBAL_CONCURRENCY, default_local_concurrency: DEFAULT_LOCAL_CONCURRENCY, scratchpad: nil, scratchpad_enabled: true)
135
+ # @param scratchpad [Tools::Stores::Scratchpad, nil] Optional scratchpad instance (for testing/internal use)
136
+ # @param scratchpad_mode [Symbol, String] Scratchpad mode (:enabled or :disabled). :per_node not allowed for non-node swarms.
137
+ # @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)
124
139
  @name = name
140
+ @swarm_id = swarm_id || generate_swarm_id(name)
141
+ @parent_swarm_id = parent_swarm_id
125
142
  @global_concurrency = global_concurrency
126
143
  @default_local_concurrency = default_local_concurrency
127
- @scratchpad_enabled = scratchpad_enabled
144
+
145
+ # Handle scratchpad_mode parameter
146
+ # For Swarm: :enabled or :disabled (not :per_node - that's for nodes)
147
+ @scratchpad_mode = validate_swarm_scratchpad_mode(scratchpad_mode)
148
+
149
+ # Resolve allow_filesystem_tools with priority:
150
+ # 1. Explicit parameter (if not nil)
151
+ # 2. Global settings
152
+ @allow_filesystem_tools = if allow_filesystem_tools.nil?
153
+ SwarmSDK.settings.allow_filesystem_tools
154
+ else
155
+ allow_filesystem_tools
156
+ end
157
+
158
+ # Swarm registry for managing sub-swarms (initialized later if needed)
159
+ @swarm_registry = nil
160
+
161
+ # Delegation call stack for circular dependency detection
162
+ @delegation_call_stack = []
128
163
 
129
164
  # Shared semaphore for all agents
130
165
  @global_semaphore = Async::Semaphore.new(@global_concurrency)
131
166
 
132
167
  # Shared scratchpad storage for all agents (volatile)
133
- # Use provided scratchpad storage (for testing) or create volatile one
134
- @scratchpad_storage = scratchpad || Tools::Stores::ScratchpadStorage.new
168
+ # Use provided scratchpad storage (for testing) or create volatile one based on mode
169
+ @scratchpad_storage = if scratchpad
170
+ scratchpad # Testing/internal use - explicit instance provided
171
+ elsif @scratchpad_mode == :enabled
172
+ Tools::Stores::ScratchpadStorage.new
173
+ end
135
174
 
136
175
  # Per-agent plugin storages (persistent)
137
176
  # Format: { plugin_name => { agent_name => storage } }
@@ -147,6 +186,7 @@ module SwarmSDK
147
186
  # Agent definitions and instances
148
187
  @agent_definitions = {}
149
188
  @agents = {}
189
+ @delegation_instances = {} # { "delegate@delegator" => Agent::Chat }
150
190
  @agents_initialized = false
151
191
  @agent_contexts = {}
152
192
 
@@ -157,6 +197,10 @@ module SwarmSDK
157
197
 
158
198
  # Track if first message has been sent
159
199
  @first_message_sent = false
200
+
201
+ # Track if agent_start events have been emitted
202
+ # This prevents duplicate emissions and ensures events are emitted when logging is ready
203
+ @agent_start_events_emitted = false
160
204
  end
161
205
 
162
206
  # Add an agent to the swarm
@@ -222,8 +266,25 @@ module SwarmSDK
222
266
  logs = []
223
267
  current_prompt = prompt
224
268
 
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
275
+
276
+ # Set fiber-local execution context
277
+ # Use ||= to inherit parent's execution_id if one exists (for mini-swarms)
278
+ # Child fibers (tools, delegations) inherit automatically
279
+ Fiber[:execution_id] ||= generate_execution_id
280
+ Fiber[:swarm_id] = @swarm_id
281
+ Fiber[:parent_swarm_id] = @parent_swarm_id
282
+
225
283
  # Setup logging FIRST if block given (so swarm_start event can be emitted)
226
284
  if block_given?
285
+ # Force fresh callback array for this execution
286
+ Fiber[:log_callbacks] = []
287
+
227
288
  # Register callback to collect logs and forward to user's block
228
289
  LogCollector.on_log do |entry|
229
290
  logs << entry
@@ -252,6 +313,17 @@ module SwarmSDK
252
313
  # Lazy initialization of agents (with optional logging)
253
314
  initialize_agents unless @agents_initialized
254
315
 
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
+
255
327
  # Execution loop (supports reprompting)
256
328
  result = nil
257
329
  swarm_stop_triggered = false
@@ -355,6 +427,14 @@ module SwarmSDK
355
427
  # Cleanup MCP clients after execution
356
428
  cleanup
357
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
+
358
438
  # Reset logging state for next execution if we set it up
359
439
  #
360
440
  # IMPORTANT: Only reset if we set up logging (block_given? == true).
@@ -447,6 +527,8 @@ module SwarmSDK
447
527
  LogStream.emit(
448
528
  type: "model_lookup_warning",
449
529
  agent: warning[:agent],
530
+ swarm_id: @swarm_id,
531
+ parent_swarm_id: @parent_swarm_id,
450
532
  model: warning[:model],
451
533
  error_message: warning[:error_message],
452
534
  suggestions: warning[:suggestions],
@@ -465,18 +547,25 @@ module SwarmSDK
465
547
  #
466
548
  # @return [void]
467
549
  def cleanup
468
- return if @mcp_clients.empty?
550
+ # Check if there's anything to clean up
551
+ return if @mcp_clients.empty? && (!@delegation_instances || @delegation_instances.empty?)
469
552
 
553
+ # Stop MCP clients for all agents (primaries + delegations tracked by instance name)
470
554
  @mcp_clients.each do |agent_name, clients|
471
555
  clients.each do |client|
472
- client.stop if client.alive?
556
+ # Always call stop - this sets @running = false and stops background threads
557
+ client.stop
473
558
  RubyLLM.logger.debug("SwarmSDK: Stopped MCP client '#{client.name}' for agent #{agent_name}")
474
559
  rescue StandardError => e
475
- RubyLLM.logger.error("SwarmSDK: Error stopping MCP client '#{client.name}' for agent #{agent_name}: #{e.message}")
560
+ # Don't fail cleanup if stopping one client fails
561
+ RubyLLM.logger.debug("SwarmSDK: Error stopping MCP client '#{client.name}': #{e.message}")
476
562
  end
477
563
  end
478
564
 
479
565
  @mcp_clients.clear
566
+
567
+ # Clear delegation instances (V7.0: Added for completeness)
568
+ @delegation_instances&.clear
480
569
  end
481
570
 
482
571
  # Register a named hook that can be referenced in agent configurations
@@ -517,8 +606,155 @@ module SwarmSDK
517
606
  self
518
607
  end
519
608
 
609
+ # Reset context for all agents
610
+ #
611
+ # Clears conversation history for all agents. This is used by composable swarms
612
+ # to reset sub-swarm context when keep_context: false is specified.
613
+ #
614
+ # @return [void]
615
+ def reset_context!
616
+ @agents.each_value do |agent_chat|
617
+ agent_chat.clear_conversation if agent_chat.respond_to?(:clear_conversation)
618
+ end
619
+ end
620
+
621
+ # Create snapshot of current conversation state
622
+ #
623
+ # Returns a Snapshot object containing:
624
+ # - All agent conversations (@messages arrays)
625
+ # - Agent context state (warnings, compression, TodoWrite tracking, skills)
626
+ # - Delegation instance conversations
627
+ # - Scratchpad contents (volatile shared storage)
628
+ # - Read tracking state (which files each agent has read with digests)
629
+ # - Memory read tracking state (which memory entries each agent has read with digests)
630
+ #
631
+ # Configuration (agent definitions, tools, prompts) stays in your YAML/DSL
632
+ # and is NOT included in snapshots.
633
+ #
634
+ # @return [Snapshot] Snapshot object with convenient serialization methods
635
+ #
636
+ # @example Save snapshot to JSON file
637
+ # snapshot = swarm.snapshot
638
+ # snapshot.write_to_file("session.json")
639
+ #
640
+ # @example Convert to hash or JSON string
641
+ # snapshot = swarm.snapshot
642
+ # hash = snapshot.to_hash
643
+ # json_string = snapshot.to_json
644
+ def snapshot
645
+ StateSnapshot.new(self).snapshot
646
+ end
647
+
648
+ # Restore conversation state from snapshot
649
+ #
650
+ # Accepts a Snapshot object, hash, or JSON string. Validates compatibility
651
+ # between snapshot and current swarm configuration, restores agent conversations,
652
+ # context state, scratchpad, and read tracking. Returns RestoreResult with
653
+ # warnings about any agents that couldn't be restored due to configuration
654
+ # mismatches.
655
+ #
656
+ # The swarm must be created with the SAME configuration (agent definitions,
657
+ # tools, prompts) as when the snapshot was created. Only conversation state
658
+ # is restored from the snapshot.
659
+ #
660
+ # @param snapshot [Snapshot, Hash, String] Snapshot object, hash, or JSON string
661
+ # @return [RestoreResult] Result with warnings about skipped agents
662
+ #
663
+ # @example Restore from Snapshot object
664
+ # swarm = SwarmSDK.build { ... } # Same config as snapshot
665
+ # snapshot = Snapshot.from_file("session.json")
666
+ # result = swarm.restore(snapshot)
667
+ # if result.success?
668
+ # puts "All agents restored"
669
+ # else
670
+ # puts result.summary
671
+ # result.warnings.each { |w| puts " - #{w[:message]}" }
672
+ # end
673
+ #
674
+ # Restore swarm state from snapshot
675
+ #
676
+ # By default, uses current system prompts from agent definitions (YAML + SDK defaults + plugin injections).
677
+ # Set preserve_system_prompts: true to use historical prompts from snapshot.
678
+ #
679
+ # @param snapshot [Snapshot, Hash, String] Snapshot object, hash, or JSON string
680
+ # @param preserve_system_prompts [Boolean] Use historical system prompts instead of current config (default: false)
681
+ # @return [RestoreResult] Result with warnings about partial restores
682
+ def restore(snapshot, preserve_system_prompts: false)
683
+ StateRestorer.new(self, snapshot, preserve_system_prompts: preserve_system_prompts).restore
684
+ end
685
+
686
+ # Override swarm IDs for composable swarms
687
+ #
688
+ # Used by SwarmLoader to set hierarchical IDs when loading sub-swarms.
689
+ # This is called after the swarm is built to ensure proper parent/child relationships.
690
+ #
691
+ # @param swarm_id [String] New swarm ID
692
+ # @param parent_swarm_id [String] New parent swarm ID
693
+ # @return [void]
694
+ def override_swarm_ids(swarm_id:, parent_swarm_id:)
695
+ @swarm_id = swarm_id
696
+ @parent_swarm_id = parent_swarm_id
697
+ end
698
+
699
+ # Set swarm registry for composable swarms
700
+ #
701
+ # Used by Builder to set the registry after swarm creation.
702
+ # This must be called before agent initialization to enable swarm delegation.
703
+ #
704
+ # @param registry [SwarmRegistry] Configured swarm registry
705
+ # @return [void]
706
+ attr_writer :swarm_registry
707
+
520
708
  private
521
709
 
710
+ # Validate and normalize scratchpad mode for Swarm
711
+ #
712
+ # Regular Swarms support :enabled or :disabled.
713
+ # Rejects :per_node since it only makes sense for NodeOrchestrator with multiple nodes.
714
+ #
715
+ # @param value [Symbol, String] Scratchpad mode (strings from YAML converted to symbols)
716
+ # @return [Symbol] :enabled or :disabled
717
+ # @raise [ArgumentError] If :per_node used, or invalid value
718
+ def validate_swarm_scratchpad_mode(value)
719
+ # Convert strings from YAML to symbols
720
+ value = value.to_sym if value.is_a?(String)
721
+
722
+ case value
723
+ when :enabled, :disabled
724
+ value
725
+ when :per_node
726
+ raise ArgumentError,
727
+ "scratchpad: :per_node is only valid for NodeOrchestrator with nodes. " \
728
+ "For regular Swarms, use :enabled or :disabled."
729
+ else
730
+ raise ArgumentError,
731
+ "Invalid scratchpad mode for Swarm: #{value.inspect}. " \
732
+ "Use :enabled or :disabled."
733
+ end
734
+ end
735
+
736
+ # Generate a unique swarm ID from name
737
+ #
738
+ # Creates a swarm ID by sanitizing the name and appending a random suffix.
739
+ # Used when swarm_id is not explicitly provided.
740
+ #
741
+ # @param name [String] Swarm name
742
+ # @return [String] Generated swarm ID (e.g., "dev_team_a3f2b1c8")
743
+ def generate_swarm_id(name)
744
+ sanitized = name.to_s.gsub(/[^a-z0-9_-]/i, "_").downcase
745
+ "#{sanitized}_#{SecureRandom.hex(4)}"
746
+ end
747
+
748
+ # Generate a unique execution ID
749
+ #
750
+ # Creates an execution ID that uniquely identifies a single swarm.execute() call.
751
+ # Format: "exec_{swarm_id}_{random_hex}"
752
+ #
753
+ # @return [String] Generated execution ID (e.g., "exec_main_a3f2b1c8")
754
+ def generate_execution_id
755
+ "exec_#{@swarm_id}_#{SecureRandom.hex(8)}"
756
+ end
757
+
522
758
  # Initialize all agents using AgentInitializer
523
759
  #
524
760
  # This is called automatically (lazy initialization) by execute() and agent().
@@ -542,44 +778,87 @@ module SwarmSDK
542
778
  @agent_contexts = initializer.agent_contexts
543
779
  @agents_initialized = true
544
780
 
545
- # Emit agent_start events for all agents
546
- emit_agent_start_events
781
+ # NOTE: agent_start events are emitted in execute() when logging is set up
782
+ # This ensures events are never lost, even if agents are initialized early (e.g., by restore())
783
+ end
784
+
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
547
803
  end
548
804
 
549
805
  # Emit agent_start events for all initialized agents
550
806
  def emit_agent_start_events
551
- # Only emit if LogStream is enabled
552
807
  return unless LogStream.emitter
553
808
 
809
+ # Emit for PRIMARY agents
554
810
  @agents.each do |agent_name, chat|
555
- agent_def = @agent_definitions[agent_name]
556
-
557
- # Build plugin storage info for logging
558
- plugin_storage_info = {}
559
- @plugin_storages.each do |plugin_name, agent_storages|
560
- next unless agent_storages.key?(agent_name)
561
-
562
- plugin_storage_info[plugin_name] = {
563
- enabled: true,
564
- # Get additional info from agent definition if available
565
- config: agent_def.respond_to?(plugin_name) ? extract_plugin_config_info(agent_def.public_send(plugin_name)) : nil,
566
- }
567
- end
811
+ emit_agent_start_for(agent_name, chat, is_delegation: false)
812
+ end
568
813
 
569
- LogStream.emit(
570
- type: "agent_start",
571
- agent: agent_name,
572
- swarm_name: @name,
573
- model: agent_def.model,
574
- provider: agent_def.provider || "openai",
575
- directory: agent_def.directory,
576
- system_prompt: agent_def.system_prompt,
577
- tools: chat.tools.keys,
578
- delegates_to: agent_def.delegates_to,
579
- plugin_storages: plugin_storage_info,
580
- timestamp: Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ"),
581
- )
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
+ }
582
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
583
862
  end
584
863
 
585
864
  # Normalize tools to internal format (kept for add_agent)
@@ -672,6 +951,8 @@ module SwarmSDK
672
951
  LogStream.emit(
673
952
  type: "swarm_start",
674
953
  agent: context.metadata[:lead_agent], # Include agent for consistency
954
+ swarm_id: @swarm_id,
955
+ parent_swarm_id: @parent_swarm_id,
675
956
  swarm_name: context.metadata[:swarm_name],
676
957
  lead_agent: context.metadata[:lead_agent],
677
958
  prompt: context.metadata[:prompt],
@@ -686,6 +967,8 @@ module SwarmSDK
686
967
 
687
968
  LogStream.emit(
688
969
  type: "swarm_stop",
970
+ swarm_id: @swarm_id,
971
+ parent_swarm_id: @parent_swarm_id,
689
972
  swarm_name: context.metadata[:swarm_name],
690
973
  lead_agent: context.metadata[:lead_agent],
691
974
  last_agent: context.metadata[:last_agent], # Agent that produced final response
@@ -707,11 +990,14 @@ module SwarmSDK
707
990
  LogStream.emit(
708
991
  type: "user_prompt",
709
992
  agent: context.agent_name,
993
+ swarm_id: @swarm_id,
994
+ parent_swarm_id: @parent_swarm_id,
710
995
  model: context.metadata[:model] || "unknown",
711
996
  provider: context.metadata[:provider] || "unknown",
712
997
  message_count: context.metadata[:message_count] || 0,
713
998
  tools: context.metadata[:tools] || [],
714
999
  delegates_to: context.metadata[:delegates_to] || [],
1000
+ source: context.metadata[:source] || "user",
715
1001
  metadata: context.metadata,
716
1002
  )
717
1003
  end
@@ -727,6 +1013,8 @@ module SwarmSDK
727
1013
  LogStream.emit(
728
1014
  type: "agent_step",
729
1015
  agent: context.agent_name,
1016
+ swarm_id: @swarm_id,
1017
+ parent_swarm_id: @parent_swarm_id,
730
1018
  model: context.metadata[:model],
731
1019
  content: context.metadata[:content],
732
1020
  tool_calls: context.metadata[:tool_calls],
@@ -748,6 +1036,8 @@ module SwarmSDK
748
1036
  LogStream.emit(
749
1037
  type: "agent_stop",
750
1038
  agent: context.agent_name,
1039
+ swarm_id: @swarm_id,
1040
+ parent_swarm_id: @parent_swarm_id,
751
1041
  model: context.metadata[:model],
752
1042
  content: context.metadata[:content],
753
1043
  tool_calls: context.metadata[:tool_calls],
@@ -768,6 +1058,8 @@ module SwarmSDK
768
1058
  LogStream.emit(
769
1059
  type: "tool_call",
770
1060
  agent: context.agent_name,
1061
+ swarm_id: @swarm_id,
1062
+ parent_swarm_id: @parent_swarm_id,
771
1063
  tool_call_id: context.tool_call.id,
772
1064
  tool: context.tool_call.name,
773
1065
  arguments: context.tool_call.parameters,
@@ -785,6 +1077,8 @@ module SwarmSDK
785
1077
  LogStream.emit(
786
1078
  type: "tool_result",
787
1079
  agent: context.agent_name,
1080
+ swarm_id: @swarm_id,
1081
+ parent_swarm_id: @parent_swarm_id,
788
1082
  tool_call_id: context.tool_result.tool_call_id,
789
1083
  tool: context.tool_result.tool_name,
790
1084
  result: context.tool_result.content,
@@ -800,6 +1094,8 @@ module SwarmSDK
800
1094
  LogStream.emit(
801
1095
  type: "context_limit_warning",
802
1096
  agent: context.agent_name,
1097
+ swarm_id: @swarm_id,
1098
+ parent_swarm_id: @parent_swarm_id,
803
1099
  model: context.metadata[:model] || "unknown",
804
1100
  threshold: "#{context.metadata[:threshold]}%",
805
1101
  current_usage: "#{context.metadata[:percentage]}%",