swarm_sdk 2.1.2 → 2.2.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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_sdk/agent/builder.rb +33 -0
  3. data/lib/swarm_sdk/agent/chat/context_tracker.rb +33 -0
  4. data/lib/swarm_sdk/agent/chat/hook_integration.rb +41 -0
  5. data/lib/swarm_sdk/agent/chat/system_reminder_injector.rb +11 -27
  6. data/lib/swarm_sdk/agent/chat.rb +198 -51
  7. data/lib/swarm_sdk/agent/context.rb +6 -2
  8. data/lib/swarm_sdk/agent/context_manager.rb +6 -0
  9. data/lib/swarm_sdk/agent/definition.rb +15 -22
  10. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +180 -0
  11. data/lib/swarm_sdk/configuration.rb +420 -103
  12. data/lib/swarm_sdk/events_to_messages.rb +181 -0
  13. data/lib/swarm_sdk/log_collector.rb +31 -5
  14. data/lib/swarm_sdk/log_stream.rb +37 -8
  15. data/lib/swarm_sdk/model_aliases.json +4 -1
  16. data/lib/swarm_sdk/node/agent_config.rb +33 -8
  17. data/lib/swarm_sdk/node/builder.rb +39 -18
  18. data/lib/swarm_sdk/node_orchestrator.rb +293 -26
  19. data/lib/swarm_sdk/proc_helpers.rb +53 -0
  20. data/lib/swarm_sdk/prompts/base_system_prompt.md.erb +0 -126
  21. data/lib/swarm_sdk/providers/openai_with_responses.rb +22 -15
  22. data/lib/swarm_sdk/restore_result.rb +65 -0
  23. data/lib/swarm_sdk/snapshot.rb +156 -0
  24. data/lib/swarm_sdk/snapshot_from_events.rb +386 -0
  25. data/lib/swarm_sdk/state_restorer.rb +491 -0
  26. data/lib/swarm_sdk/state_snapshot.rb +369 -0
  27. data/lib/swarm_sdk/swarm/agent_initializer.rb +360 -55
  28. data/lib/swarm_sdk/swarm/all_agents_builder.rb +28 -1
  29. data/lib/swarm_sdk/swarm/builder.rb +208 -12
  30. data/lib/swarm_sdk/swarm/swarm_registry_builder.rb +67 -0
  31. data/lib/swarm_sdk/swarm/tool_configurator.rb +46 -11
  32. data/lib/swarm_sdk/swarm.rb +367 -90
  33. data/lib/swarm_sdk/swarm_loader.rb +145 -0
  34. data/lib/swarm_sdk/swarm_registry.rb +136 -0
  35. data/lib/swarm_sdk/tools/delegate.rb +92 -7
  36. data/lib/swarm_sdk/tools/read.rb +17 -5
  37. data/lib/swarm_sdk/tools/scratchpad/scratchpad_list.rb +23 -2
  38. data/lib/swarm_sdk/tools/scratchpad/scratchpad_read.rb +23 -2
  39. data/lib/swarm_sdk/tools/scratchpad/scratchpad_write.rb +21 -4
  40. data/lib/swarm_sdk/tools/stores/read_tracker.rb +47 -12
  41. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +45 -0
  42. data/lib/swarm_sdk/tools/stores/storage.rb +4 -4
  43. data/lib/swarm_sdk/tools/think.rb +4 -1
  44. data/lib/swarm_sdk/tools/todo_write.rb +20 -8
  45. data/lib/swarm_sdk/utils.rb +18 -0
  46. data/lib/swarm_sdk/validation_result.rb +33 -0
  47. data/lib/swarm_sdk/version.rb +1 -1
  48. data/lib/swarm_sdk.rb +362 -21
  49. metadata +17 -5
@@ -37,22 +37,32 @@ module SwarmSDK
37
37
  # agent :backend { ... }
38
38
  # end
39
39
  class << self
40
- def build(&block)
41
- builder = new
40
+ def build(allow_filesystem_tools: nil, &block)
41
+ builder = new(allow_filesystem_tools: allow_filesystem_tools)
42
42
  builder.instance_eval(&block)
43
43
  builder.build_swarm
44
44
  end
45
45
  end
46
46
 
47
- def initialize
47
+ def initialize(allow_filesystem_tools: nil)
48
+ @swarm_id = nil
48
49
  @swarm_name = nil
49
50
  @lead_agent = nil
50
51
  @agents = {}
51
52
  @all_agents_config = nil
52
53
  @swarm_hooks = []
54
+ @swarm_registry_config = [] # NEW - stores register() calls for composable swarms
53
55
  @nodes = {}
54
56
  @start_node = nil
55
- @scratchpad_enabled = true # Default: enabled
57
+ @scratchpad = :disabled # Default: disabled
58
+ @allow_filesystem_tools = allow_filesystem_tools
59
+ end
60
+
61
+ # Set swarm ID
62
+ #
63
+ # @param swarm_id [String] Unique identifier for this swarm
64
+ def id(swarm_id)
65
+ @swarm_id = swarm_id
56
66
  end
57
67
 
58
68
  # Set swarm name
@@ -65,11 +75,29 @@ module SwarmSDK
65
75
  @lead_agent = agent_name
66
76
  end
67
77
 
68
- # Enable or disable shared scratchpad
78
+ # Configure scratchpad mode
79
+ #
80
+ # For NodeOrchestrator: :enabled (shared across nodes), :per_node (isolated), or :disabled
81
+ # For regular Swarm: :enabled or :disabled
82
+ #
83
+ # @param mode [Symbol, Boolean] Scratchpad mode
84
+ def scratchpad(mode)
85
+ @scratchpad = mode
86
+ end
87
+
88
+ # Register external swarms for composable swarms
89
+ #
90
+ # @example
91
+ # swarms do
92
+ # register "code_review", file: "./swarms/code_review.rb"
93
+ # register "testing", file: "./swarms/testing.yml", keep_context: false
94
+ # end
69
95
  #
70
- # @param enabled [Boolean] Whether to enable scratchpad tools
71
- def use_scratchpad(enabled)
72
- @scratchpad_enabled = enabled
96
+ # @yield Block containing register() calls
97
+ def swarms(&block)
98
+ builder = SwarmRegistryBuilder.new
99
+ builder.instance_eval(&block)
100
+ @swarm_registry_config = builder.registrations
73
101
  end
74
102
 
75
103
  # Define an agent with fluent API or load from markdown content
@@ -196,6 +224,12 @@ module SwarmSDK
196
224
  def build_swarm
197
225
  raise ConfigurationError, "Swarm name not set. Use: name 'My Swarm'" unless @swarm_name
198
226
 
227
+ # Validate all_agents filesystem tools BEFORE building
228
+ validate_all_agents_filesystem_tools if @all_agents_config
229
+
230
+ # Validate individual agent filesystem tools BEFORE building
231
+ validate_agent_filesystem_tools
232
+
199
233
  # Check if nodes are defined
200
234
  if @nodes.any?
201
235
  # Node-based workflow (agents optional for agent-less workflows)
@@ -211,6 +245,26 @@ module SwarmSDK
211
245
 
212
246
  private
213
247
 
248
+ # Normalize scratchpad mode parameter
249
+ #
250
+ # Accepts symbols: :enabled, :per_node, or :disabled
251
+ #
252
+ # @param value [Symbol, String] Scratchpad mode (strings from YAML converted to symbols)
253
+ # @return [Symbol] Normalized mode (:enabled, :per_node, or :disabled)
254
+ # @raise [ConfigurationError] If value is invalid
255
+ def normalize_scratchpad_mode(value)
256
+ # Convert strings from YAML to symbols
257
+ value = value.to_sym if value.is_a?(String)
258
+
259
+ case value
260
+ when :enabled, :per_node, :disabled
261
+ value
262
+ else
263
+ raise ConfigurationError,
264
+ "Invalid scratchpad mode: #{value.inspect}. Use :enabled, :per_node, or :disabled"
265
+ end
266
+ end
267
+
214
268
  # Check if a string is markdown content (has frontmatter)
215
269
  #
216
270
  # @param str [String] String to check
@@ -310,8 +364,27 @@ module SwarmSDK
310
364
  #
311
365
  # @return [Swarm] Configured swarm instance
312
366
  def build_single_swarm
313
- # Create swarm using SDK
314
- swarm = Swarm.new(name: @swarm_name, scratchpad_enabled: @scratchpad_enabled)
367
+ # Validate swarm_id is set if external swarms are registered (required for composable swarms)
368
+ if @swarm_registry_config.any? && @swarm_id.nil?
369
+ raise ConfigurationError, "Swarm id must be set using id(...) when using composable swarms"
370
+ end
371
+
372
+ # Create swarm using SDK (swarm_id auto-generates if nil)
373
+ swarm = Swarm.new(
374
+ name: @swarm_name,
375
+ swarm_id: @swarm_id,
376
+ scratchpad_mode: @scratchpad,
377
+ allow_filesystem_tools: @allow_filesystem_tools,
378
+ )
379
+
380
+ # Setup swarm registry if external swarms are registered
381
+ if @swarm_registry_config.any?
382
+ registry = SwarmRegistry.new(parent_swarm_id: @swarm_id)
383
+ @swarm_registry_config.each do |reg|
384
+ registry.register(reg[:name], source: reg[:source], keep_context: reg[:keep_context])
385
+ end
386
+ swarm.swarm_registry = registry
387
+ end
315
388
 
316
389
  # Merge all_agents config into each agent (including file-loaded ones)
317
390
  merge_all_agents_config_into_agents if @all_agents_config
@@ -375,13 +448,20 @@ module SwarmSDK
375
448
  end
376
449
 
377
450
  # Create node orchestrator
378
- NodeOrchestrator.new(
451
+ orchestrator = NodeOrchestrator.new(
379
452
  swarm_name: @swarm_name,
453
+ swarm_id: @swarm_id,
380
454
  agent_definitions: agent_definitions,
381
455
  nodes: @nodes,
382
456
  start_node: @start_node,
383
- scratchpad_enabled: @scratchpad_enabled,
457
+ scratchpad: @scratchpad,
458
+ allow_filesystem_tools: @allow_filesystem_tools,
384
459
  )
460
+
461
+ # Pass swarm registry config to orchestrator if external swarms registered
462
+ orchestrator.swarm_registry_config = @swarm_registry_config if @swarm_registry_config.any?
463
+
464
+ orchestrator
385
465
  end
386
466
 
387
467
  # Merge all_agents configuration into each agent
@@ -582,6 +662,122 @@ module SwarmSDK
582
662
  base
583
663
  end
584
664
  end
665
+
666
+ # Validate all_agents filesystem tools
667
+ #
668
+ # Raises ConfigurationError if filesystem tools are globally disabled
669
+ # but all_agents configuration includes them.
670
+ #
671
+ # @raise [ConfigurationError] If filesystem tools are disabled and all_agents has them
672
+ # @return [void]
673
+ def validate_all_agents_filesystem_tools
674
+ # Resolve the effective setting
675
+ resolved_setting = if @allow_filesystem_tools.nil?
676
+ SwarmSDK.settings.allow_filesystem_tools
677
+ else
678
+ @allow_filesystem_tools
679
+ end
680
+
681
+ return if resolved_setting # If true, allow everything
682
+ return unless @all_agents_config&.tools_list&.any?
683
+
684
+ forbidden = @all_agents_config.tools_list.select do |tool|
685
+ SwarmSDK::Swarm::ToolConfigurator::FILESYSTEM_TOOLS.include?(tool.to_sym)
686
+ end
687
+
688
+ return if forbidden.empty?
689
+
690
+ raise ConfigurationError,
691
+ "Filesystem tools are globally disabled (SwarmSDK.settings.allow_filesystem_tools = false) " \
692
+ "but all_agents configuration includes: #{forbidden.join(", ")}.\n\n" \
693
+ "This is a system-wide security setting that cannot be overridden by swarm configuration.\n" \
694
+ "To use filesystem tools, set SwarmSDK.settings.allow_filesystem_tools = true before loading the swarm."
695
+ end
696
+
697
+ # Validate individual agent filesystem tools
698
+ #
699
+ # Raises ConfigurationError if filesystem tools are globally disabled
700
+ # but any agent attempts to use them.
701
+ #
702
+ # @raise [ConfigurationError] If filesystem tools are disabled and any agent has them
703
+ # @return [void]
704
+ def validate_agent_filesystem_tools
705
+ # Resolve the effective setting
706
+ resolved_setting = if @allow_filesystem_tools.nil?
707
+ SwarmSDK.settings.allow_filesystem_tools
708
+ else
709
+ @allow_filesystem_tools
710
+ end
711
+
712
+ return if resolved_setting # If true, allow everything
713
+
714
+ # Check each agent for forbidden tools
715
+ @agents.each do |agent_name, agent_builder_or_config|
716
+ # Extract tool list from either Builder or file config
717
+ tools_list = if agent_builder_or_config.is_a?(Hash) && agent_builder_or_config.key?(:__file_config__)
718
+ # File-loaded agent
719
+ agent_builder_or_config[:__file_config__][:tools] || []
720
+ elsif agent_builder_or_config.is_a?(Agent::Builder)
721
+ # Builder object - use tools_list method
722
+ agent_builder_or_config.tools_list
723
+ else
724
+ []
725
+ end
726
+
727
+ # Extract tool names (they might be hashes with permissions) and convert to symbols
728
+ tool_names = tools_list.map do |tool|
729
+ name = tool.is_a?(Hash) ? tool[:name] : tool
730
+ name.to_sym
731
+ end
732
+
733
+ # Find forbidden tools
734
+ forbidden = tool_names.select do |tool|
735
+ SwarmSDK::Swarm::ToolConfigurator::FILESYSTEM_TOOLS.include?(tool)
736
+ end
737
+
738
+ next if forbidden.empty?
739
+
740
+ raise ConfigurationError,
741
+ "Filesystem tools are globally disabled (SwarmSDK.settings.allow_filesystem_tools = false) " \
742
+ "but agent '#{agent_name}' attempts to use: #{forbidden.join(", ")}.\n\n" \
743
+ "This is a system-wide security setting that cannot be overridden by swarm configuration.\n" \
744
+ "To use filesystem tools, set SwarmSDK.settings.allow_filesystem_tools = true before loading the swarm."
745
+ end
746
+ end
585
747
  end
748
+
749
+ # Helper class for swarms block in DSL
750
+ #
751
+ # Provides a clean API for registering external swarms within the swarms { } block.
752
+ # Supports three registration methods:
753
+ # 1. File path: register "name", file: "./swarm.rb"
754
+ # 2. YAML string: register "name", yaml: "version: 2\n..."
755
+ # 3. Inline block: register "name" do ... end
756
+ #
757
+ # @example From file
758
+ # swarms do
759
+ # register "code_review", file: "./swarms/code_review.rb"
760
+ # end
761
+ #
762
+ # @example From YAML string
763
+ # swarms do
764
+ # yaml_content = File.read("testing.yml")
765
+ # register "testing", yaml: yaml_content, keep_context: false
766
+ # end
767
+ #
768
+ # @example Inline block
769
+ # swarms do
770
+ # register "testing", keep_context: false do
771
+ # id "testing_team"
772
+ # name "Testing Team"
773
+ # lead :tester
774
+ # agent :tester do
775
+ # model "gpt-4o-mini"
776
+ # system "You test code"
777
+ # end
778
+ # end
779
+ # end
780
+ #
781
+ # NOTE: SwarmRegistryBuilder is now in swarm_registry_builder.rb for Zeitwerk
586
782
  end
587
783
  end
@@ -0,0 +1,67 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ class Swarm
5
+ # Builder for swarm registry in DSL
6
+ #
7
+ # Supports registering external swarms for composable swarms pattern.
8
+ #
9
+ # @example
10
+ # swarms do
11
+ # register "code_review", file: "./swarms/code_review.rb"
12
+ # register "testing", file: "./swarms/testing.yml", keep_context: false
13
+ # end
14
+ #
15
+ # @example Inline swarm definition
16
+ # swarms do
17
+ # register "tester" do
18
+ # lead :tester
19
+ # agent :tester do
20
+ # model "gpt-4o-mini"
21
+ # system "You test code"
22
+ # end
23
+ # end
24
+ # end
25
+ #
26
+ class SwarmRegistryBuilder
27
+ attr_reader :registrations
28
+
29
+ def initialize
30
+ @registrations = []
31
+ end
32
+
33
+ # Register a swarm from file, YAML string, or inline block
34
+ #
35
+ # @param name [String, Symbol] Registration name
36
+ # @param file [String, nil] Path to swarm file (.rb or .yml)
37
+ # @param yaml [String, nil] YAML content string
38
+ # @param keep_context [Boolean] Whether to preserve conversation state (default: true)
39
+ # @yield Optional block for inline swarm definition
40
+ # @raise [ArgumentError] If neither file, yaml, nor block provided
41
+ def register(name, file: nil, yaml: nil, keep_context: true, &block)
42
+ # Validate that exactly one source is provided
43
+ sources = [file, yaml, block].compact
44
+ if sources.empty?
45
+ raise ArgumentError, "register '#{name}' requires either file:, yaml:, or a block"
46
+ elsif sources.size > 1
47
+ raise ArgumentError, "register '#{name}' accepts only one of: file:, yaml:, or block (got #{sources.size})"
48
+ end
49
+
50
+ # Determine source type and store
51
+ source = if file
52
+ { type: :file, value: file }
53
+ elsif yaml
54
+ { type: :yaml, value: yaml }
55
+ else
56
+ { type: :block, value: block }
57
+ end
58
+
59
+ @registrations << {
60
+ name: name.to_s,
61
+ source: source,
62
+ keep_context: keep_context,
63
+ }
64
+ end
65
+ end
66
+ end
67
+ end
@@ -17,10 +17,6 @@ module SwarmSDK
17
17
  :Read,
18
18
  :Grep,
19
19
  :Glob,
20
- :TodoWrite,
21
- :Think,
22
- :WebFetch,
23
- :Clock,
24
20
  ].freeze
25
21
 
26
22
  # Scratchpad tools (added if scratchpad is enabled)
@@ -30,6 +26,17 @@ module SwarmSDK
30
26
  :ScratchpadList,
31
27
  ].freeze
32
28
 
29
+ # Filesystem tools that can be globally disabled for security
30
+ FILESYSTEM_TOOLS = [
31
+ :Read,
32
+ :Write,
33
+ :Edit,
34
+ :MultiEdit,
35
+ :Grep,
36
+ :Glob,
37
+ :Bash,
38
+ ].freeze
39
+
33
40
  def initialize(swarm, scratchpad_storage, plugin_storages = {})
34
41
  @swarm = swarm
35
42
  @scratchpad_storage = scratchpad_storage
@@ -144,6 +151,19 @@ module SwarmSDK
144
151
  # @param agent_name [Symbol] Agent name
145
152
  # @param agent_definition [AgentDefinition] Agent definition
146
153
  def register_explicit_tools(chat, tool_configs, agent_name:, agent_definition:)
154
+ # Validate filesystem tools if globally disabled
155
+ unless @swarm.allow_filesystem_tools
156
+ # Extract tool names from hashes and convert to symbols for comparison
157
+ forbidden = tool_configs.map { |tc| tc[:name].to_sym }.select { |name| FILESYSTEM_TOOLS.include?(name) }
158
+ unless forbidden.empty?
159
+ raise ConfigurationError,
160
+ "Filesystem tools are globally disabled (SwarmSDK.settings.allow_filesystem_tools = false) " \
161
+ "but agent '#{agent_name}' attempts to use: #{forbidden.join(", ")}.\n\n" \
162
+ "This is a system-wide security setting that cannot be overridden by swarm configuration.\n" \
163
+ "To use filesystem tools, set SwarmSDK.settings.allow_filesystem_tools = true before loading the swarm."
164
+ end
165
+ end
166
+
147
167
  tool_configs.each do |tool_config|
148
168
  tool_name = tool_config[:name]
149
169
  permissions_config = tool_config[:permissions]
@@ -177,6 +197,9 @@ module SwarmSDK
177
197
  # Register core default tools (unless disabled)
178
198
  if agent_definition.disable_default_tools != true
179
199
  DEFAULT_TOOLS.each do |tool_name|
200
+ # Skip filesystem tools if globally disabled
201
+ next if !@swarm.allow_filesystem_tools && FILESYSTEM_TOOLS.include?(tool_name)
202
+
180
203
  register_tool_if_not_disabled(chat, tool_name, explicit_tool_names, agent_name, agent_definition)
181
204
  end
182
205
 
@@ -229,15 +252,21 @@ module SwarmSDK
229
252
  plugin = PluginRegistry.plugin_for_tool(tool_name)
230
253
  raise ConfigurationError, "Tool #{tool_name} is not provided by any plugin" unless plugin
231
254
 
232
- # Get plugin storage for this agent
255
+ # V7.0: Extract base name for storage lookup (handles delegation instances)
256
+ # For primary agents: :tester → :tester (no change)
257
+ # For delegation instances: "tester@frontend" → :tester (extracts base)
258
+ base_name = agent_name.to_s.split("@").first.to_sym
259
+
260
+ # Get plugin storage using BASE NAME (shared across instances)
233
261
  plugin_storages = @plugin_storages[plugin.name] || {}
234
- storage = plugin_storages[agent_name]
262
+ storage = plugin_storages[base_name] # ← Changed from agent_name to base_name
235
263
 
236
264
  # Build context for tool creation
265
+ # Pass full agent_name for tool state tracking (TodoWrite, ReadTracker, etc.)
237
266
  context = {
238
- agent_name: agent_name,
267
+ agent_name: agent_name, # Full instance name for tool's use
239
268
  directory: directory,
240
- storage: storage,
269
+ storage: storage, # Shared storage by base name
241
270
  agent_definition: agent_definition,
242
271
  chat: chat,
243
272
  tool_configurator: self,
@@ -303,15 +332,21 @@ module SwarmSDK
303
332
  def tool_disabled?(tool_name, disable_config)
304
333
  return false if disable_config.nil?
305
334
 
335
+ # Normalize tool_name to symbol for comparison
336
+ tool_name_sym = tool_name.to_sym
337
+
306
338
  if disable_config == true
307
339
  # Disable all default tools
308
340
  true
309
341
  elsif disable_config.is_a?(Symbol)
310
342
  # Single tool name
311
- disable_config == tool_name
343
+ disable_config == tool_name_sym
344
+ elsif disable_config.is_a?(String)
345
+ # Single tool name as string (from YAML)
346
+ disable_config.to_sym == tool_name_sym
312
347
  elsif disable_config.is_a?(Array)
313
- # Disable only tools in the array
314
- disable_config.include?(tool_name)
348
+ # Disable only tools in the array - normalize to symbols for comparison
349
+ disable_config.map(&:to_sym).include?(tool_name_sym)
315
350
  else
316
351
  false
317
352
  end