swarm_sdk 2.2.0 → 2.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/lib/swarm_sdk/agent/builder.rb +58 -0
  3. data/lib/swarm_sdk/agent/chat.rb +527 -1059
  4. data/lib/swarm_sdk/agent/{chat → chat_helpers}/context_tracker.rb +9 -88
  5. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +204 -0
  6. data/lib/swarm_sdk/agent/{chat → chat_helpers}/hook_integration.rb +111 -44
  7. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +78 -0
  8. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +233 -0
  9. data/lib/swarm_sdk/agent/{chat → chat_helpers}/logging_helpers.rb +1 -1
  10. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +83 -0
  11. data/lib/swarm_sdk/agent/{chat → chat_helpers}/system_reminder_injector.rb +12 -12
  12. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +79 -0
  13. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +98 -0
  14. data/lib/swarm_sdk/agent/context.rb +2 -2
  15. data/lib/swarm_sdk/agent/definition.rb +66 -154
  16. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +4 -2
  17. data/lib/swarm_sdk/agent/system_prompt_builder.rb +161 -0
  18. data/lib/swarm_sdk/builders/base_builder.rb +409 -0
  19. data/lib/swarm_sdk/concerns/cleanupable.rb +39 -0
  20. data/lib/swarm_sdk/concerns/snapshotable.rb +67 -0
  21. data/lib/swarm_sdk/concerns/validatable.rb +55 -0
  22. data/lib/swarm_sdk/configuration/parser.rb +353 -0
  23. data/lib/swarm_sdk/configuration/translator.rb +255 -0
  24. data/lib/swarm_sdk/configuration.rb +65 -543
  25. data/lib/swarm_sdk/context_compactor/token_counter.rb +3 -3
  26. data/lib/swarm_sdk/context_compactor.rb +6 -11
  27. data/lib/swarm_sdk/context_management/builder.rb +128 -0
  28. data/lib/swarm_sdk/context_management/context.rb +328 -0
  29. data/lib/swarm_sdk/defaults.rb +196 -0
  30. data/lib/swarm_sdk/events_to_messages.rb +18 -0
  31. data/lib/swarm_sdk/hooks/shell_executor.rb +2 -1
  32. data/lib/swarm_sdk/log_collector.rb +179 -29
  33. data/lib/swarm_sdk/log_stream.rb +29 -0
  34. data/lib/swarm_sdk/node_context.rb +1 -1
  35. data/lib/swarm_sdk/observer/builder.rb +81 -0
  36. data/lib/swarm_sdk/observer/config.rb +45 -0
  37. data/lib/swarm_sdk/observer/manager.rb +236 -0
  38. data/lib/swarm_sdk/patterns/agent_observer.rb +160 -0
  39. data/lib/swarm_sdk/plugin.rb +93 -3
  40. data/lib/swarm_sdk/snapshot.rb +6 -6
  41. data/lib/swarm_sdk/snapshot_from_events.rb +13 -2
  42. data/lib/swarm_sdk/state_restorer.rb +136 -151
  43. data/lib/swarm_sdk/state_snapshot.rb +65 -100
  44. data/lib/swarm_sdk/swarm/agent_initializer.rb +180 -136
  45. data/lib/swarm_sdk/swarm/builder.rb +44 -578
  46. data/lib/swarm_sdk/swarm/executor.rb +213 -0
  47. data/lib/swarm_sdk/swarm/hook_triggers.rb +150 -0
  48. data/lib/swarm_sdk/swarm/logging_callbacks.rb +340 -0
  49. data/lib/swarm_sdk/swarm/mcp_configurator.rb +7 -4
  50. data/lib/swarm_sdk/swarm/tool_configurator.rb +42 -138
  51. data/lib/swarm_sdk/swarm.rb +137 -679
  52. data/lib/swarm_sdk/tools/bash.rb +11 -3
  53. data/lib/swarm_sdk/tools/delegate.rb +61 -43
  54. data/lib/swarm_sdk/tools/edit.rb +8 -13
  55. data/lib/swarm_sdk/tools/glob.rb +9 -1
  56. data/lib/swarm_sdk/tools/grep.rb +7 -0
  57. data/lib/swarm_sdk/tools/multi_edit.rb +15 -11
  58. data/lib/swarm_sdk/tools/path_resolver.rb +51 -2
  59. data/lib/swarm_sdk/tools/read.rb +11 -13
  60. data/lib/swarm_sdk/tools/registry.rb +122 -10
  61. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +8 -5
  62. data/lib/swarm_sdk/tools/stores/storage.rb +0 -6
  63. data/lib/swarm_sdk/tools/todo_write.rb +7 -0
  64. data/lib/swarm_sdk/tools/web_fetch.rb +3 -2
  65. data/lib/swarm_sdk/tools/write.rb +8 -13
  66. data/lib/swarm_sdk/version.rb +1 -1
  67. data/lib/swarm_sdk/{node → workflow}/agent_config.rb +1 -1
  68. data/lib/swarm_sdk/workflow/builder.rb +143 -0
  69. data/lib/swarm_sdk/workflow/executor.rb +497 -0
  70. data/lib/swarm_sdk/{node/builder.rb → workflow/node_builder.rb} +3 -3
  71. data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +3 -2
  72. data/lib/swarm_sdk/{node_orchestrator.rb → workflow.rb} +152 -456
  73. data/lib/swarm_sdk.rb +33 -3
  74. metadata +67 -15
  75. data/lib/swarm_sdk/providers/openai_with_responses.rb +0 -589
@@ -28,7 +28,7 @@ module SwarmSDK
28
28
  # end
29
29
  #
30
30
  # swarm.execute("Build auth API")
31
- class Builder
31
+ class Builder < Builders::BaseBuilder
32
32
  # Main entry point for DSL
33
33
  #
34
34
  # @example
@@ -45,29 +45,10 @@ module SwarmSDK
45
45
  end
46
46
 
47
47
  def initialize(allow_filesystem_tools: nil)
48
- @swarm_id = nil
49
- @swarm_name = nil
48
+ super
50
49
  @lead_agent = nil
51
- @agents = {}
52
- @all_agents_config = nil
53
50
  @swarm_hooks = []
54
- @swarm_registry_config = [] # NEW - stores register() calls for composable swarms
55
- @nodes = {}
56
- @start_node = nil
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
66
- end
67
-
68
- # Set swarm name
69
- def name(swarm_name)
70
- @swarm_name = swarm_name
51
+ @observer_configs = []
71
52
  end
72
53
 
73
54
  # Set lead agent
@@ -75,75 +56,42 @@ module SwarmSDK
75
56
  @lead_agent = agent_name
76
57
  end
77
58
 
78
- # Configure scratchpad mode
59
+ # Define observer agent behavior
79
60
  #
80
- # For NodeOrchestrator: :enabled (shared across nodes), :per_node (isolated), or :disabled
81
- # For regular Swarm: :enabled or :disabled
61
+ # Configures an agent to run in parallel with main execution,
62
+ # triggered by specific events. The block defines event handlers.
82
63
  #
83
- # @param mode [Symbol, Boolean] Scratchpad mode
84
- def scratchpad(mode)
85
- @scratchpad = mode
86
- end
87
-
88
- # Register external swarms for composable swarms
64
+ # @param agent_name [Symbol] Name of agent to use as observer (must be defined)
65
+ # @param options [Hash] Optional observer settings (timeout, max_concurrent, etc.)
66
+ # @yield Observer configuration block
89
67
  #
90
- # @example
91
- # swarms do
92
- # register "code_review", file: "./swarms/code_review.rb"
93
- # register "testing", file: "./swarms/testing.yml", keep_context: false
68
+ # @example Basic observer
69
+ # observer :profiler do
70
+ # on :swarm_start do |event|
71
+ # "Analyze this prompt: #{event[:prompt]}"
72
+ # end
94
73
  # end
95
74
  #
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
101
- end
102
-
103
- # Define an agent with fluent API or load from markdown content
104
- #
105
- # Supports two forms:
106
- # 1. Inline DSL: agent :name do ... end
107
- # 2. Markdown content: agent :name, <<~MD ... MD
108
- #
109
- # The name parameter is always required. If the markdown has a name field
110
- # in frontmatter, it will be replaced by the name parameter.
111
- #
112
- # @example Inline DSL
113
- # agent :backend do
114
- # model "gpt-5"
115
- # system_prompt "You build APIs"
116
- # tools :Read, :Write
117
- #
118
- # hook :pre_tool_use, matcher: "Bash" do |ctx|
119
- # # Inline validation logic!
75
+ # @example Observer with options
76
+ # observer :monitor, timeout: 120 do
77
+ # on :tool_call do |event|
78
+ # next unless event[:tool_name] == "Bash"
79
+ # "Check command: #{event[:arguments][:command]}"
120
80
  # end
121
81
  # end
122
- #
123
- # @example Markdown content
124
- # agent :backend, <<~MD
125
- # ---
126
- # description: "Backend developer"
127
- # model: "gpt-4"
128
- # ---
129
- #
130
- # You build APIs.
131
- # MD
132
- def agent(name, content = nil, &block)
133
- # Case 1: agent :name, <<~MD do ... end (markdown + overrides)
134
- if content.is_a?(String) && block_given? && markdown_content?(content)
135
- load_agent_from_markdown_with_overrides(content, name, &block)
136
- # Case 2: agent :name, <<~MD (markdown only)
137
- elsif content.is_a?(String) && !block_given? && markdown_content?(content)
138
- load_agent_from_markdown(content, name)
139
- # Case 3: agent :name do ... end (inline DSL)
140
- elsif block_given?
141
- builder = Agent::Builder.new(name)
142
- builder.instance_eval(&block)
143
- @agents[name] = builder
144
- else
145
- raise ArgumentError, "Invalid agent definition. Use: agent :name { ... } OR agent :name, <<~MD ... MD OR agent :name, <<~MD do ... end"
82
+ def observer(agent_name, **options, &block)
83
+ unless @agents.key?(agent_name)
84
+ raise ConfigurationError,
85
+ "Observer agent '#{agent_name}' not defined. " \
86
+ "Define the agent first with `agent :#{agent_name} do ... end`"
146
87
  end
88
+
89
+ config = Observer::Config.new(agent_name)
90
+ config.options.merge!(options) if options.any?
91
+ builder = Observer::Builder.new(agent_name, config)
92
+ builder.instance_eval(&block)
93
+
94
+ @observer_configs << config
147
95
  end
148
96
 
149
97
  # Add swarm-level hook (swarm_start, swarm_stop only)
@@ -164,202 +112,21 @@ module SwarmSDK
164
112
  @swarm_hooks << { event: event, command: command, timeout: timeout, block: block }
165
113
  end
166
114
 
167
- # Configure all agents with a block
168
- #
169
- # @example
170
- # all_agents do
171
- # tools :Read, :Write
172
- #
173
- # hook :pre_tool_use, matcher: "Write" do |ctx|
174
- # # Validation for all agents
175
- # end
176
- # end
177
- def all_agents(&block)
178
- builder = AllAgentsBuilder.new
179
- builder.instance_eval(&block)
180
- @all_agents_config = builder
181
- end
182
-
183
- # Define a node (mini-swarm execution stage)
184
- #
185
- # Nodes enable multi-stage workflows where different agent teams
186
- # collaborate in sequence. Each node is an independent swarm execution.
187
- #
188
- # @param name [Symbol] Node name
189
- # @yield Block for node configuration
190
- # @return [void]
191
- #
192
- # @example Solo agent node
193
- # node :planning do
194
- # agent(:architect)
195
- # end
196
- #
197
- # @example Multi-agent node with delegation
198
- # node :implementation do
199
- # agent(:backend).delegates_to(:tester, :database)
200
- # agent(:tester).delegates_to(:database)
201
- # agent(:database)
202
- # after :planning
203
- # end
204
- def node(name, &block)
205
- builder = Node::Builder.new(name)
206
- builder.instance_eval(&block)
207
- @nodes[name] = builder
208
- end
209
-
210
- # Set the starting node for workflow execution
211
- #
212
- # Required when nodes are defined. Specifies which node to execute first.
213
- #
214
- # @param name [Symbol] Name of starting node
215
- # @return [void]
216
- #
217
- # @example
218
- # start_node :planning
219
- def start_node(name)
220
- @start_node = name.to_sym
221
- end
222
-
223
- # Build the actual Swarm instance or NodeOrchestrator
115
+ # Build the actual Swarm instance
224
116
  def build_swarm
225
117
  raise ConfigurationError, "Swarm name not set. Use: name 'My Swarm'" unless @swarm_name
118
+ raise ConfigurationError, "No agents defined. Use: agent :name { ... }" if @agents.empty?
119
+ raise ConfigurationError, "Lead agent not set. Use: lead :agent_name" unless @lead_agent
226
120
 
227
- # Validate all_agents filesystem tools BEFORE building
121
+ # Validate filesystem tools BEFORE building
228
122
  validate_all_agents_filesystem_tools if @all_agents_config
229
-
230
- # Validate individual agent filesystem tools BEFORE building
231
123
  validate_agent_filesystem_tools
232
124
 
233
- # Check if nodes are defined
234
- if @nodes.any?
235
- # Node-based workflow (agents optional for agent-less workflows)
236
- build_node_orchestrator
237
- else
238
- # Traditional single-swarm execution (requires agents and lead)
239
- raise ConfigurationError, "No agents defined. Use: agent :name { ... }" if @agents.empty?
240
- raise ConfigurationError, "Lead agent not set. Use: lead :agent_name" unless @lead_agent
241
-
242
- build_single_swarm
243
- end
125
+ build_single_swarm
244
126
  end
245
127
 
246
128
  private
247
129
 
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
-
268
- # Check if a string is markdown content (has frontmatter)
269
- #
270
- # @param str [String] String to check
271
- # @return [Boolean] true if string contains markdown frontmatter
272
- def markdown_content?(str)
273
- str.start_with?("---") || str.include?("\n---\n")
274
- end
275
-
276
- # Load an agent from markdown content
277
- #
278
- # Returns a hash of the agent config (not a Definition yet) so that
279
- # all_agents config can be applied later in the build process.
280
- #
281
- # @param content [String] Markdown content with frontmatter
282
- # @param name_override [Symbol, nil] Optional name to override frontmatter name
283
- # @return [void]
284
- def load_agent_from_markdown(content, name_override = nil)
285
- # Parse markdown content - will extract name from frontmatter if not overridden
286
- definition = MarkdownParser.parse(content, name_override)
287
-
288
- # Store the config hash (not Definition) so all_agents can be applied
289
- # We'll wrap this in a special marker so we know it came from markdown
290
- @agents[definition.name] = { __file_config__: definition.to_h }
291
- end
292
-
293
- # Load an agent from markdown content with DSL overrides
294
- #
295
- # This allows loading from a file and then overriding specific settings:
296
- # agent :reviewer, File.read("reviewer.md") do
297
- # provider :openai
298
- # model "gpt-4o"
299
- # end
300
- #
301
- # @param content [String] Markdown content with frontmatter
302
- # @param name_override [Symbol, nil] Optional name to override frontmatter name
303
- # @yield Block with DSL overrides
304
- # @return [void]
305
- def load_agent_from_markdown_with_overrides(content, name_override = nil, &block)
306
- # Parse markdown content first
307
- definition = MarkdownParser.parse(content, name_override)
308
-
309
- # Create a builder with the markdown config
310
- builder = Agent::Builder.new(definition.name)
311
-
312
- # Apply markdown settings to builder (these become the base)
313
- apply_definition_to_builder(builder, definition.to_h)
314
-
315
- # Apply DSL overrides (these override the markdown settings)
316
- builder.instance_eval(&block)
317
-
318
- # Store the builder (not file config) so overrides are preserved
319
- @agents[definition.name] = builder
320
- end
321
-
322
- # Apply agent definition hash to a builder
323
- #
324
- # @param builder [Agent::Builder] Builder to configure
325
- # @param config [Hash] Configuration hash from definition
326
- # @return [void]
327
- def apply_definition_to_builder(builder, config)
328
- builder.description(config[:description]) if config[:description]
329
- builder.model(config[:model]) if config[:model]
330
- builder.provider(config[:provider]) if config[:provider]
331
- builder.base_url(config[:base_url]) if config[:base_url]
332
- builder.api_version(config[:api_version]) if config[:api_version]
333
- builder.context_window(config[:context_window]) if config[:context_window]
334
- builder.system_prompt(config[:system_prompt]) if config[:system_prompt]
335
- builder.directory(config[:directory]) if config[:directory]
336
- builder.timeout(config[:timeout]) if config[:timeout]
337
- builder.parameters(config[:parameters]) if config[:parameters]
338
- builder.headers(config[:headers]) if config[:headers]
339
- builder.coding_agent(config[:coding_agent]) unless config[:coding_agent].nil?
340
- # Don't apply assume_model_exists from markdown - let DSL overrides or auto-enable handle it
341
- # builder.assume_model_exists(config[:assume_model_exists]) unless config[:assume_model_exists].nil?
342
- builder.bypass_permissions(config[:bypass_permissions]) if config[:bypass_permissions]
343
- builder.disable_default_tools(config[:disable_default_tools]) unless config[:disable_default_tools].nil?
344
-
345
- # Add tools from markdown
346
- if config[:tools]&.any?
347
- # Extract tool names from the tools array (which may be hashes with permissions)
348
- tool_names = config[:tools].map do |tool|
349
- tool.is_a?(Hash) ? tool[:name] : tool
350
- end
351
- builder.tools(*tool_names)
352
- end
353
-
354
- # Add delegates_to
355
- builder.delegates_to(*config[:delegates_to]) if config[:delegates_to]&.any?
356
-
357
- # Add MCP servers
358
- config[:mcp_servers]&.each do |server|
359
- builder.mcp_server(server[:name], **server.except(:name))
360
- end
361
- end
362
-
363
130
  # Build a traditional single-swarm execution
364
131
  #
365
132
  # @return [Swarm] Configured swarm instance
@@ -386,20 +153,9 @@ module SwarmSDK
386
153
  swarm.swarm_registry = registry
387
154
  end
388
155
 
389
- # Merge all_agents config into each agent (including file-loaded ones)
390
- merge_all_agents_config_into_agents if @all_agents_config
391
-
392
- # Build definitions and add to swarm
393
- # Handle both Agent::Builder (inline DSL) and file configs (from files)
394
- @agents.each do |agent_name, agent_builder_or_config|
395
- definition = if agent_builder_or_config.is_a?(Hash) && agent_builder_or_config.key?(:__file_config__)
396
- # File-loaded agent config (with all_agents merged)
397
- Agent::Definition.new(agent_name, agent_builder_or_config[:__file_config__])
398
- else
399
- # Builder object (from inline DSL) - convert to definition
400
- agent_builder_or_config.to_definition
401
- end
402
-
156
+ # Build agent definitions and add to swarm
157
+ agent_definitions = build_agent_definitions
158
+ agent_definitions.each_value do |definition|
403
159
  swarm.add_agent(definition)
404
160
  end
405
161
 
@@ -407,198 +163,21 @@ module SwarmSDK
407
163
  swarm.lead = @lead_agent
408
164
 
409
165
  # Apply swarm hooks (Ruby blocks)
410
- # These are swarm-level hooks (swarm_start, swarm_stop)
411
166
  @swarm_hooks.each do |hook_config|
412
167
  apply_swarm_hook(swarm, hook_config)
413
168
  end
414
169
 
415
170
  # Apply all_agents hooks (Ruby blocks)
416
- # These become swarm-level default callbacks that apply to all agents
417
171
  @all_agents_config&.hooks&.each do |hook_config|
418
172
  apply_all_agents_hook(swarm, hook_config)
419
173
  end
420
174
 
421
- # NOTE: Agent-specific hooks are already stored in Agent::Definition.callbacks
422
- # They'll be applied automatically during agent initialization (pass_4_configure_hooks)
423
- # This ensures they're applied at the right time, after LogStream is set up
175
+ # Add observer configurations to swarm
176
+ @observer_configs.each { |c| swarm.add_observer_config(c) }
424
177
 
425
178
  swarm
426
179
  end
427
180
 
428
- # Build a node-based workflow orchestrator
429
- #
430
- # @return [NodeOrchestrator] Configured orchestrator
431
- def build_node_orchestrator
432
- raise ConfigurationError, "start_node required when nodes are defined. Use: start_node :name" unless @start_node
433
-
434
- # Merge all_agents config into each agent (applies to all nodes)
435
- merge_all_agents_config_into_agents if @all_agents_config
436
-
437
- # Build agent definitions
438
- # Handle both Agent::Builder (inline DSL) and file configs (from files)
439
- agent_definitions = {}
440
- @agents.each do |agent_name, agent_builder_or_config|
441
- agent_definitions[agent_name] = if agent_builder_or_config.is_a?(Hash) && agent_builder_or_config.key?(:__file_config__)
442
- # File-loaded agent config (with all_agents merged)
443
- Agent::Definition.new(agent_name, agent_builder_or_config[:__file_config__])
444
- else
445
- # Builder object (from inline DSL) - convert to definition
446
- agent_builder_or_config.to_definition
447
- end
448
- end
449
-
450
- # Create node orchestrator
451
- orchestrator = NodeOrchestrator.new(
452
- swarm_name: @swarm_name,
453
- swarm_id: @swarm_id,
454
- agent_definitions: agent_definitions,
455
- nodes: @nodes,
456
- start_node: @start_node,
457
- scratchpad: @scratchpad,
458
- allow_filesystem_tools: @allow_filesystem_tools,
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
465
- end
466
-
467
- # Merge all_agents configuration into each agent
468
- #
469
- # All_agents values are used as defaults - agent-specific values override.
470
- # This applies to both inline DSL agents (Builder) and file-loaded agents (config hash).
471
- #
472
- # @return [void]
473
- def merge_all_agents_config_into_agents
474
- return unless @all_agents_config
475
-
476
- all_agents_hash = @all_agents_config.to_h
477
-
478
- @agents.each_value do |agent_builder_or_config|
479
- if agent_builder_or_config.is_a?(Hash) && agent_builder_or_config.key?(:__file_config__)
480
- # File-loaded agent - merge into the config hash
481
- file_config = agent_builder_or_config[:__file_config__]
482
-
483
- # Merge all_agents into file config (file config overrides)
484
- # Use same merge strategy as Configuration class
485
- merged_config = merge_all_agents_into_config(all_agents_hash, file_config)
486
-
487
- # Update the stored config
488
- agent_builder_or_config[:__file_config__] = merged_config
489
- else
490
- # Builder object (inline DSL agent)
491
- agent_builder = agent_builder_or_config
492
-
493
- # Apply all_agents defaults that haven't been set at agent level
494
- # Agent values override all_agents values
495
- apply_all_agents_defaults(agent_builder, all_agents_hash)
496
-
497
- # Merge tools (prepend all_agents tools)
498
- all_agents_tools = @all_agents_config.tools_list
499
- agent_builder.prepend_tools(*all_agents_tools) if all_agents_tools.any?
500
-
501
- # Pass all_agents permissions as default_permissions
502
- if @all_agents_config.permissions_config.any?
503
- agent_builder.default_permissions = @all_agents_config.permissions_config
504
- end
505
- end
506
- end
507
- end
508
-
509
- # Merge all_agents config into file-loaded agent config
510
- #
511
- # Follows same merge strategy as Configuration class:
512
- # - Arrays (tools, delegates_to): Concatenate (all_agents + file)
513
- # - Hashes (parameters, headers): Merge (file values override)
514
- # - Scalars (model, provider, etc.): File overrides
515
- #
516
- # @param all_agents_hash [Hash] All_agents configuration
517
- # @param file_config [Hash] File-loaded agent configuration
518
- # @return [Hash] Merged configuration
519
- def merge_all_agents_into_config(all_agents_hash, file_config)
520
- merged = all_agents_hash.dup
521
-
522
- file_config.each do |key, value|
523
- case key
524
- when :tools
525
- # Concatenate tools: all_agents.tools + file.tools
526
- merged[:tools] = Array(merged[:tools]) + Array(value)
527
- when :delegates_to
528
- # Concatenate delegates_to
529
- merged[:delegates_to] = Array(merged[:delegates_to]) + Array(value)
530
- when :parameters
531
- # Merge parameters: file values override all_agents
532
- merged[:parameters] = (merged[:parameters] || {}).merge(value || {})
533
- when :headers
534
- # Merge headers: file values override all_agents
535
- merged[:headers] = (merged[:headers] || {}).merge(value || {})
536
- else
537
- # For everything else, file value overrides all_agents value
538
- merged[key] = value
539
- end
540
- end
541
-
542
- # Pass all_agents permissions as default_permissions
543
- if all_agents_hash[:permissions] && !merged[:default_permissions]
544
- merged[:default_permissions] = all_agents_hash[:permissions]
545
- end
546
-
547
- merged
548
- end
549
-
550
- # Apply all_agents defaults to an agent builder
551
- #
552
- # Only sets values that haven't been explicitly set at the agent level.
553
- # This implements the override semantics: agent values take precedence.
554
- #
555
- # @param agent_builder [Agent::Builder] The agent builder to configure
556
- # @param all_agents_hash [Hash] All_agents configuration
557
- # @return [void]
558
- def apply_all_agents_defaults(agent_builder, all_agents_hash)
559
- # Model: only set if agent hasn't explicitly set it
560
- if all_agents_hash[:model] && !agent_builder.model_set?
561
- agent_builder.model(all_agents_hash[:model])
562
- end
563
-
564
- # Provider: only set if agent hasn't set it
565
- if all_agents_hash[:provider] && !agent_builder.provider_set?
566
- agent_builder.provider(all_agents_hash[:provider])
567
- end
568
-
569
- # Base URL: only set if agent hasn't set it
570
- if all_agents_hash[:base_url] && !agent_builder.base_url_set?
571
- agent_builder.base_url(all_agents_hash[:base_url])
572
- end
573
-
574
- # API Version: only set if agent hasn't set it
575
- if all_agents_hash[:api_version] && !agent_builder.api_version_set?
576
- agent_builder.api_version(all_agents_hash[:api_version])
577
- end
578
-
579
- # Timeout: only set if agent hasn't set it
580
- if all_agents_hash[:timeout] && !agent_builder.timeout_set?
581
- agent_builder.timeout(all_agents_hash[:timeout])
582
- end
583
-
584
- # Parameters: merge (all_agents + agent, agent values override)
585
- if all_agents_hash[:parameters]
586
- merged_params = all_agents_hash[:parameters].merge(agent_builder.parameters)
587
- agent_builder.parameters(merged_params)
588
- end
589
-
590
- # Headers: merge (all_agents + agent, agent values override)
591
- if all_agents_hash[:headers]
592
- merged_headers = all_agents_hash[:headers].merge(agent_builder.headers)
593
- agent_builder.headers(merged_headers)
594
- end
595
-
596
- # Coding_agent: only set if agent hasn't set it
597
- if !all_agents_hash[:coding_agent].nil? && !agent_builder.coding_agent_set?
598
- agent_builder.coding_agent(all_agents_hash[:coding_agent])
599
- end
600
- end
601
-
602
181
  def apply_swarm_hook(swarm, config)
603
182
  event = config[:event]
604
183
 
@@ -644,7 +223,7 @@ module SwarmSDK
644
223
  end
645
224
 
646
225
  def build_hook_input(context, event)
647
- # Build JSON input for shell hooks (similar to HooksAdapter)
226
+ # Build JSON input for shell hooks
648
227
  base = { event: event.to_s }
649
228
 
650
229
  case event
@@ -662,122 +241,9 @@ module SwarmSDK
662
241
  base
663
242
  end
664
243
  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
747
244
  end
748
245
 
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
246
+ # Helper class for swarms block in DSL (kept in this file for reference)
247
+ # Actual implementation is in swarm_registry_builder.rb for Zeitwerk
782
248
  end
783
249
  end