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
@@ -4,25 +4,55 @@ module SwarmSDK
4
4
  class Configuration
5
5
  ENV_VAR_WITH_DEFAULT_PATTERN = /\$\{([^:}]+)(:=([^}]*))?\}/
6
6
 
7
- attr_reader :config_path, :swarm_name, :lead_agent, :agents, :all_agents_config, :swarm_hooks, :all_agents_hooks, :scratchpad_enabled
7
+ attr_reader :swarm_name, :swarm_id, :lead_agent, :agents, :all_agents_config, :swarm_hooks, :all_agents_hooks, :scratchpad_enabled, :nodes, :start_node, :external_swarms
8
8
 
9
9
  class << self
10
- def load(path)
11
- new(path).tap(&:load_and_validate)
10
+ # Load configuration from YAML file
11
+ #
12
+ # Convenience method that reads the file and uses the file's directory
13
+ # as the base directory for resolving agent file paths.
14
+ #
15
+ # @param path [String, Pathname] Path to YAML configuration file
16
+ # @return [Configuration] Validated configuration instance
17
+ # @raise [ConfigurationError] If file not found or invalid
18
+ def load_file(path)
19
+ path = Pathname.new(path).expand_path
20
+
21
+ unless path.exist?
22
+ raise ConfigurationError, "Configuration file not found: #{path}"
23
+ end
24
+
25
+ yaml_content = File.read(path)
26
+ base_dir = path.dirname
27
+
28
+ new(yaml_content, base_dir: base_dir).tap(&:load_and_validate)
29
+ rescue Errno::ENOENT
30
+ raise ConfigurationError, "Configuration file not found: #{path}"
12
31
  end
13
32
  end
14
33
 
15
- def initialize(config_path)
16
- @config_path = Pathname.new(config_path).expand_path
17
- @config_dir = @config_path.dirname
18
- @agents = {}
34
+ # Initialize configuration from YAML string
35
+ #
36
+ # @param yaml_content [String] YAML configuration content
37
+ # @param base_dir [String, Pathname] Base directory for resolving agent file paths (default: Dir.pwd)
38
+ def initialize(yaml_content, base_dir: Dir.pwd)
39
+ raise ArgumentError, "yaml_content cannot be nil" if yaml_content.nil?
40
+ raise ArgumentError, "base_dir cannot be nil" if base_dir.nil?
41
+
42
+ @yaml_content = yaml_content
43
+ @base_dir = Pathname.new(base_dir).expand_path
44
+ @swarm_id = nil # Optional swarm ID from YAML
45
+ @agents = {} # Parsed agent configs (hashes, not Definitions)
19
46
  @all_agents_config = {} # Settings applied to all agents
20
47
  @swarm_hooks = {} # Swarm-level hooks (swarm_start, swarm_stop)
21
48
  @all_agents_hooks = {} # Hooks applied to all agents
49
+ @external_swarms = {} # External swarms for composable swarms
50
+ @nodes = {} # Parsed node configs (hashes)
51
+ @start_node = nil # Starting node for workflows
22
52
  end
23
53
 
24
54
  def load_and_validate
25
- @config = YAML.load_file(@config_path, aliases: true)
55
+ @config = YAML.safe_load(@yaml_content, permitted_classes: [Symbol], aliases: true)
26
56
 
27
57
  unless @config.is_a?(Hash)
28
58
  raise ConfigurationError, "Invalid YAML syntax: configuration must be a Hash"
@@ -35,10 +65,9 @@ module SwarmSDK
35
65
  load_hooks_config
36
66
  validate_swarm
37
67
  load_agents
68
+ load_nodes
38
69
  detect_circular_dependencies
39
70
  self
40
- rescue Errno::ENOENT
41
- raise ConfigurationError, "Configuration file not found: #{@config_path}"
42
71
  rescue Psych::SyntaxError => e
43
72
  raise ConfigurationError, "Invalid YAML syntax: #{e.message}"
44
73
  end
@@ -48,36 +77,84 @@ module SwarmSDK
48
77
  end
49
78
 
50
79
  def connections_for(agent_name)
51
- @agents[agent_name]&.delegates_to || []
80
+ agent_config = @agents[agent_name]
81
+ return [] unless agent_config
82
+
83
+ # Extract delegates_to from hash and convert to symbols
84
+ delegates = agent_config[:delegates_to] || []
85
+ Array(delegates).map(&:to_sym)
52
86
  end
53
87
 
54
- # Convert configuration to Swarm instance using Ruby API
88
+ # Convert configuration to Swarm or NodeOrchestrator using DSL
55
89
  #
56
- # This method bridges YAML configuration to the Ruby API, making YAML
57
- # a thin convenience layer over the programmatic interface.
90
+ # This method translates YAML configuration to Ruby DSL calls.
91
+ # The DSL (Swarm::Builder) handles all validation, merging, and construction.
58
92
  #
59
- # @return [Swarm] Configured swarm instance
60
- def to_swarm
61
- swarm = Swarm.new(
62
- name: @swarm_name,
63
- global_concurrency: Swarm::DEFAULT_GLOBAL_CONCURRENCY,
64
- default_local_concurrency: Swarm::DEFAULT_LOCAL_CONCURRENCY,
65
- scratchpad_enabled: @scratchpad_enabled,
66
- )
67
-
68
- # Add all agents - pass definitions directly
69
- @agents.each do |_name, agent_def|
70
- swarm.add_agent(agent_def)
93
+ # @param allow_filesystem_tools [Boolean, nil] Whether to allow filesystem tools (nil uses global setting)
94
+ # @return [Swarm, NodeOrchestrator] Configured swarm or orchestrator
95
+ def to_swarm(allow_filesystem_tools: nil)
96
+ builder = Swarm::Builder.new(allow_filesystem_tools: allow_filesystem_tools)
97
+
98
+ # Translate basic swarm config to DSL
99
+ builder.id(@swarm_id) if @swarm_id
100
+ builder.name(@swarm_name)
101
+ builder.lead(@lead_agent)
102
+ builder.scratchpad(@scratchpad_mode)
103
+
104
+ # Translate external swarms
105
+ if @external_swarms&.any?
106
+ builder.swarms do
107
+ @external_swarms.each do |name, config|
108
+ source = config[:source]
109
+ case source[:type]
110
+ when :file
111
+ register(name, file: source[:value], keep_context: config[:keep_context])
112
+ when :yaml
113
+ register(name, yaml: source[:value], keep_context: config[:keep_context])
114
+ else
115
+ raise ConfigurationError, "Unknown source type: #{source[:type]}"
116
+ end
117
+ end
118
+ end
71
119
  end
72
120
 
73
- # Set lead agent
74
- swarm.lead = @lead_agent
121
+ # Translate all_agents config to DSL (if present)
122
+ translate_all_agents(builder) if @all_agents_config.any?
75
123
 
76
- swarm
124
+ # Translate agents to DSL
125
+ translate_agents(builder)
126
+
127
+ # Translate swarm-level hooks to DSL (if present)
128
+ translate_swarm_hooks(builder) if @swarm_hooks.any?
129
+
130
+ # Translate nodes to DSL (if present)
131
+ if @nodes.any?
132
+ translate_nodes(builder)
133
+ builder.start_node(@start_node)
134
+ end
135
+
136
+ # Build the swarm or orchestrator (DSL decides based on presence of nodes)
137
+ builder.build_swarm
77
138
  end
78
139
 
79
140
  private
80
141
 
142
+ def parse_scratchpad_mode(value)
143
+ return :disabled if value.nil? # Default
144
+
145
+ # Convert strings from YAML to symbols
146
+ value = value.to_sym if value.is_a?(String)
147
+
148
+ # Validate symbols
149
+ case value
150
+ when :enabled, :disabled, :per_node
151
+ value
152
+ else
153
+ raise ConfigurationError,
154
+ "Invalid scratchpad mode: #{value.inspect}. Use :enabled, :per_node, or :disabled"
155
+ end
156
+ end
157
+
81
158
  def interpolate_env_vars!(obj)
82
159
  case obj
83
160
  when String
@@ -146,8 +223,46 @@ module SwarmSDK
146
223
  raise ConfigurationError, "No agents defined" if swarm[:agents].empty?
147
224
 
148
225
  @swarm_name = swarm[:name]
226
+ @swarm_id = swarm[:id] # Optional - will auto-generate if missing
149
227
  @lead_agent = swarm[:lead].to_sym # Convert to symbol for consistency
150
- @scratchpad_enabled = swarm[:use_scratchpad].nil? ? true : swarm[:use_scratchpad] # Default: enabled
228
+ @scratchpad_mode = parse_scratchpad_mode(swarm[:scratchpad])
229
+
230
+ # Load external swarms for composable swarms
231
+ load_external_swarms(swarm[:swarms]) if swarm[:swarms]
232
+ end
233
+
234
+ def load_external_swarms(swarms_config)
235
+ @external_swarms = {}
236
+ swarms_config.each do |name, config|
237
+ # Determine source type: file, yaml string, or inline swarm definition
238
+ source = if config[:file]
239
+ # File path - resolve relative to base_dir
240
+ file_path = if config[:file].start_with?("/")
241
+ config[:file]
242
+ else
243
+ (@base_dir / config[:file]).to_s
244
+ end
245
+ { type: :file, value: file_path }
246
+ elsif config[:yaml]
247
+ # YAML string provided directly
248
+ { type: :yaml, value: config[:yaml] }
249
+ elsif config[:swarm]
250
+ # Inline swarm definition - convert to YAML string
251
+ inline_config = {
252
+ version: 2,
253
+ swarm: config[:swarm],
254
+ }
255
+ yaml_string = Utils.hash_to_yaml(inline_config)
256
+ { type: :yaml, value: yaml_string }
257
+ else
258
+ raise ConfigurationError, "Swarm '#{name}' must specify either 'file:', 'yaml:', or 'swarm:' (inline definition)"
259
+ end
260
+
261
+ @external_swarms[name.to_sym] = {
262
+ source: source,
263
+ keep_context: config.fetch(:keep_context, true),
264
+ }
265
+ end
151
266
  end
152
267
 
153
268
  def load_agents
@@ -158,28 +273,30 @@ module SwarmSDK
158
273
  # 1. String: assistant: "agents/assistant.md" (file path)
159
274
  # 2. Hash with agent_file: assistant: { agent_file: "..." }
160
275
  # 3. Hash with inline definition: assistant: { description: "...", model: "..." }
276
+ # 4. nil: Invalid (will be caught when building swarm)
161
277
 
162
- if agent_config.is_a?(String)
278
+ parsed_config = if agent_config.nil?
279
+ # Null config - store empty hash, will fail during swarm building
280
+ {}
281
+ elsif agent_config.is_a?(String)
163
282
  # Format 1: Direct file path as string
164
- file_path = agent_config
165
- merged_config = merge_all_agents_config({})
166
- @agents[name] = load_agent_from_file(name, file_path, merged_config)
283
+ { agent_file: agent_config }
284
+ elsif agent_config.is_a?(Hash) && agent_config[:agent_file]
285
+ # Format 2: Hash with agent_file key
286
+ agent_config
167
287
  else
168
- # Format 2 or 3: Hash configuration
169
- agent_config ||= {}
170
-
171
- # Merge all_agents_config into agent config
172
- # Agent-specific config overrides all_agents config
173
- merged_config = merge_all_agents_config(agent_config)
288
+ # Format 3: Inline definition
289
+ agent_config || {}
290
+ end
174
291
 
175
- @agents[name] = if agent_config[:agent_file]
176
- # Format 2: Hash with agent_file key
177
- load_agent_from_file(name, agent_config[:agent_file], merged_config)
178
- else
179
- # Format 3: Inline definition
180
- Agent::Definition.new(name, merged_config)
181
- end
292
+ # Validate required fields for inline definitions (strict validation for YAML)
293
+ # File-based agents are validated when loaded
294
+ if parsed_config[:agent_file].nil? && parsed_config[:description].nil?
295
+ raise ConfigurationError,
296
+ "Agent '#{name}' missing required 'description' field"
182
297
  end
298
+
299
+ @agents[name] = parsed_config
183
300
  end
184
301
 
185
302
  unless @agents.key?(@lead_agent)
@@ -187,80 +304,276 @@ module SwarmSDK
187
304
  end
188
305
  end
189
306
 
190
- # Merge all_agents config with agent-specific config
191
- # Agent config takes precedence over all_agents config
307
+ def load_nodes
308
+ return unless @config[:swarm][:nodes]
309
+
310
+ @nodes = Utils.symbolize_keys(@config[:swarm][:nodes])
311
+ @start_node = @config[:swarm][:start_node]&.to_sym
312
+
313
+ # Validate start_node is required if nodes defined
314
+ if @nodes.any? && !@start_node
315
+ raise ConfigurationError, "start_node required when nodes are defined"
316
+ end
317
+
318
+ # Validate start_node exists
319
+ if @start_node && !@nodes.key?(@start_node)
320
+ raise ConfigurationError, "start_node '#{@start_node}' not found in nodes"
321
+ end
322
+
323
+ # Basic node structure validation
324
+ @nodes.each do |node_name, node_config|
325
+ unless node_config.is_a?(Hash)
326
+ raise ConfigurationError, "Node '#{node_name}' must be a hash"
327
+ end
328
+
329
+ # Validate agents if present (optional for agent-less nodes)
330
+ if node_config[:agents]
331
+ unless node_config[:agents].is_a?(Array)
332
+ raise ConfigurationError, "Node '#{node_name}' agents must be an array"
333
+ end
334
+
335
+ # Validate each agent config
336
+ node_config[:agents].each do |agent_config|
337
+ unless agent_config.is_a?(Hash) && agent_config[:agent]
338
+ raise ConfigurationError,
339
+ "Node '#{node_name}' agents must be hashes with 'agent' key"
340
+ end
341
+
342
+ # Validate agent exists in swarm agents
343
+ agent_sym = agent_config[:agent].to_sym
344
+ unless @agents.key?(agent_sym)
345
+ raise ConfigurationError,
346
+ "Node '#{node_name}' references undefined agent '#{agent_config[:agent]}'"
347
+ end
348
+ end
349
+ end
350
+
351
+ # Validate dependencies if present
352
+ next unless node_config[:dependencies]
353
+ unless node_config[:dependencies].is_a?(Array)
354
+ raise ConfigurationError, "Node '#{node_name}' dependencies must be an array"
355
+ end
356
+
357
+ # Validate each dependency exists
358
+ node_config[:dependencies].each do |dep|
359
+ dep_sym = dep.to_sym
360
+ unless @nodes.key?(dep_sym)
361
+ raise ConfigurationError,
362
+ "Node '#{node_name}' depends on undefined node '#{dep}'"
363
+ end
364
+ end
365
+ end
366
+ end
367
+
368
+ # Translate all_agents configuration to DSL
369
+ #
370
+ # @param builder [Swarm::Builder] DSL builder instance
371
+ # @return [void]
372
+ def translate_all_agents(builder)
373
+ # Capture instance variables for block scope
374
+ all_agents_cfg = @all_agents_config
375
+ all_agents_hks = @all_agents_hooks
376
+
377
+ builder.all_agents do
378
+ # Translate each all_agents field to DSL method calls
379
+ tools(*all_agents_cfg[:tools]) if all_agents_cfg[:tools]&.any?
380
+ model(all_agents_cfg[:model]) if all_agents_cfg[:model]
381
+ provider(all_agents_cfg[:provider]) if all_agents_cfg[:provider]
382
+ base_url(all_agents_cfg[:base_url]) if all_agents_cfg[:base_url]
383
+ api_version(all_agents_cfg[:api_version]) if all_agents_cfg[:api_version]
384
+ timeout(all_agents_cfg[:timeout]) if all_agents_cfg[:timeout]
385
+ parameters(all_agents_cfg[:parameters]) if all_agents_cfg[:parameters]
386
+ headers(all_agents_cfg[:headers]) if all_agents_cfg[:headers]
387
+ coding_agent(all_agents_cfg[:coding_agent]) unless all_agents_cfg[:coding_agent].nil?
388
+ disable_default_tools(all_agents_cfg[:disable_default_tools]) unless all_agents_cfg[:disable_default_tools].nil?
389
+
390
+ # Translate all_agents hooks
391
+ if all_agents_hks.any?
392
+ all_agents_hks.each do |event, hook_specs|
393
+ Array(hook_specs).each do |spec|
394
+ matcher = spec[:matcher]
395
+ hook(event, matcher: matcher, command: spec[:command], timeout: spec[:timeout]) if spec[:type] == "command"
396
+ end
397
+ end
398
+ end
399
+
400
+ # Permissions - set directly as hash (YAML doesn't use DSL block syntax)
401
+ self.permissions_hash = all_agents_cfg[:permissions] if all_agents_cfg[:permissions]
402
+ end
403
+ end
404
+
405
+ # Translate agents to DSL
192
406
  #
193
- # Merge strategy:
194
- # - Arrays (tools, delegates_to): Concatenate
195
- # - Hashes (parameters, headers): Merge (agent values override)
196
- # - Scalars (model, provider, base_url, timeout, coding_agent): Agent overrides
407
+ # @param builder [Swarm::Builder] DSL builder instance
408
+ # @return [void]
409
+ def translate_agents(builder)
410
+ @agents.each do |name, agent_config|
411
+ translate_agent(builder, name, agent_config)
412
+ rescue ConfigurationError => e
413
+ # Re-raise with agent context for better error messages
414
+ raise ConfigurationError, "Error in swarm.agents.#{name}: #{e.message}"
415
+ end
416
+ end
417
+
418
+ # Translate single agent to DSL
197
419
  #
198
- # @param agent_config [Hash] Agent-specific configuration
199
- # @return [Hash] Merged configuration
200
- def merge_all_agents_config(agent_config)
201
- merged = @all_agents_config.dup
202
-
203
- # For arrays, concatenate
204
- # For hashes, merge (agent values override)
205
- # For scalars, agent value overrides
206
- agent_config.each do |key, value|
207
- case key
208
- when :tools
209
- # Concatenate tools: all_agents.tools + agent.tools
210
- merged[:tools] = Array(merged[:tools]) + Array(value)
211
- when :delegates_to
212
- # Concatenate delegates_to
213
- merged[:delegates_to] = Array(merged[:delegates_to]) + Array(value)
214
- when :parameters
215
- # Merge parameters: all_agents.parameters + agent.parameters
216
- # Agent values override all_agents values for same keys
217
- merged[:parameters] = (merged[:parameters] || {}).merge(value || {})
218
- when :headers
219
- # Merge headers: all_agents.headers + agent.headers
220
- # Agent values override all_agents values for same keys
221
- merged[:headers] = (merged[:headers] || {}).merge(value || {})
222
- when :disable_default_tools
223
- # Convert array elements to symbols if it's an array
224
- merged[key] = value.is_a?(Array) ? value.map(&:to_sym) : value
420
+ # @param builder [Swarm::Builder] DSL builder instance
421
+ # @param name [Symbol] Agent name
422
+ # @param config [Hash] Agent configuration
423
+ # @return [void]
424
+ def translate_agent(builder, name, config)
425
+ if config[:agent_file]
426
+ # Load from file
427
+ agent_file_path = resolve_agent_file_path(config[:agent_file])
428
+
429
+ unless File.exist?(agent_file_path)
430
+ raise ConfigurationError, "Agent file not found: #{agent_file_path}"
431
+ end
432
+
433
+ content = File.read(agent_file_path)
434
+
435
+ # Check if there are overrides besides agent_file
436
+ overrides = config.except(:agent_file)
437
+
438
+ if overrides.any?
439
+ # Load from markdown with DSL overrides
440
+ builder.agent(name, content, &create_agent_config_block(overrides))
225
441
  else
226
- # For everything else (model, provider, base_url, timeout, coding_agent, etc.),
227
- # agent value overrides all_agents value
228
- merged[key] = value
442
+ # Load from markdown only
443
+ builder.agent(name, content)
229
444
  end
445
+ else
446
+ # Inline definition - translate to DSL
447
+ builder.agent(name, &create_agent_config_block(config))
230
448
  end
449
+ rescue StandardError => e
450
+ raise ConfigurationError, "Error loading agent '#{name}': #{e.message}"
451
+ end
452
+
453
+ # Create a block that configures an agent builder with the given config
454
+ #
455
+ # Returns a proc that can be passed to builder.agent
456
+ #
457
+ # @param config [Hash] Agent configuration hash
458
+ # @return [Proc] Block that configures agent builder
459
+ def create_agent_config_block(config)
460
+ proc do
461
+ description(config[:description]) if config[:description]
462
+ model(config[:model]) if config[:model]
463
+ provider(config[:provider]) if config[:provider]
464
+ base_url(config[:base_url]) if config[:base_url]
465
+ api_version(config[:api_version]) if config[:api_version]
466
+ context_window(config[:context_window]) if config[:context_window]
467
+ system_prompt(config[:system_prompt]) if config[:system_prompt]
468
+ directory(config[:directory]) if config[:directory]
469
+ timeout(config[:timeout]) if config[:timeout]
470
+ parameters(config[:parameters]) if config[:parameters]
471
+ headers(config[:headers]) if config[:headers]
472
+ coding_agent(config[:coding_agent]) unless config[:coding_agent].nil?
473
+ bypass_permissions(config[:bypass_permissions]) if config[:bypass_permissions]
474
+ disable_default_tools(config[:disable_default_tools]) unless config[:disable_default_tools].nil?
475
+ shared_across_delegations(config[:shared_across_delegations]) unless config[:shared_across_delegations].nil?
476
+
477
+ # Tools
478
+ if config[:tools]&.any?
479
+ tool_names = config[:tools].map { |t| t.is_a?(Hash) ? t[:name] : t }
480
+ tools(*tool_names)
481
+ end
482
+
483
+ # Delegation
484
+ delegates_to(*config[:delegates_to]) if config[:delegates_to]&.any?
231
485
 
232
- # Pass all_agents permissions as default_permissions for backward compat with AgentDefinition
233
- if @all_agents_config[:permissions]
234
- merged[:default_permissions] = @all_agents_config[:permissions]
486
+ # MCP servers
487
+ config[:mcp_servers]&.each do |server|
488
+ mcp_server(server[:name], **server.except(:name))
489
+ end
490
+
491
+ # Hooks (YAML-style command hooks)
492
+ config[:hooks]&.each do |event, hook_specs|
493
+ Array(hook_specs).each do |spec|
494
+ matcher = spec[:matcher]
495
+ hook(event, matcher: matcher, command: spec[:command], timeout: spec[:timeout]) if spec[:type] == "command"
496
+ end
497
+ end
498
+
499
+ # Memory
500
+ if config[:memory]
501
+ memory do
502
+ directory(config[:memory][:directory]) if config[:memory][:directory]
503
+ adapter(config[:memory][:adapter]) if config[:memory][:adapter]
504
+ mode(config[:memory][:mode]) if config[:memory][:mode]
505
+ end
506
+ end
507
+
508
+ # Permissions - set directly as hash (YAML doesn't use DSL block syntax)
509
+ self.permissions_hash = config[:permissions] if config[:permissions]
235
510
  end
511
+ end
236
512
 
237
- merged
513
+ # Translate swarm-level hooks to DSL
514
+ #
515
+ # @param builder [Swarm::Builder] Swarm builder instance
516
+ # @return [void]
517
+ def translate_swarm_hooks(builder)
518
+ @swarm_hooks.each do |event, hook_specs|
519
+ Array(hook_specs).each do |spec|
520
+ if spec[:type] == "command"
521
+ builder.hook(event, command: spec[:command], timeout: spec[:timeout])
522
+ end
523
+ end
524
+ end
238
525
  end
239
526
 
240
- def load_agent_from_file(name, file_path, merged_config)
241
- agent_file_path = resolve_agent_file_path(file_path)
527
+ # Translate nodes to DSL
528
+ #
529
+ # @param builder [Swarm::Builder] Swarm builder instance
530
+ # @return [void]
531
+ def translate_nodes(builder)
532
+ @nodes.each do |node_name, node_config|
533
+ builder.node(node_name) do
534
+ # Translate agents
535
+ node_config[:agents]&.each do |agent_config|
536
+ agent_name = agent_config[:agent].to_sym
537
+ delegates = agent_config[:delegates_to] || []
538
+ reset_ctx = agent_config.key?(:reset_context) ? agent_config[:reset_context] : true
539
+ tools_override = agent_config[:tools]
540
+
541
+ # Build agent config with fluent API
542
+ agent_cfg = agent(agent_name, reset_context: reset_ctx)
543
+
544
+ # Apply delegation if present
545
+ agent_cfg = agent_cfg.delegates_to(*delegates) if delegates.any?
546
+
547
+ # Apply tools override if present
548
+ agent_cfg.tools(*tools_override) if tools_override # Return config (finalize will be called automatically)
549
+ end
242
550
 
243
- unless File.exist?(agent_file_path)
244
- raise ConfigurationError, "Agent file not found: #{agent_file_path}"
245
- end
551
+ # Translate dependencies
552
+ depends_on(*node_config[:dependencies]) if node_config[:dependencies]&.any?
246
553
 
247
- content = File.read(agent_file_path)
248
- # Parse markdown and merge with YAML config
249
- agent_def_from_file = MarkdownParser.parse(content, name)
554
+ # Translate lead override
555
+ lead(node_config[:lead].to_sym) if node_config[:lead]
250
556
 
251
- # Merge: YAML config overrides markdown file (YAML takes precedence)
252
- # This allows YAML to override any settings from the markdown file
253
- final_config = agent_def_from_file.to_h.compact.merge(merged_config.compact)
557
+ # Translate transformers
558
+ if node_config[:input_command]
559
+ input_command(node_config[:input_command], timeout: node_config[:input_timeout] || 60)
560
+ end
254
561
 
255
- Agent::Definition.new(name, final_config)
256
- rescue StandardError => e
257
- raise ConfigurationError, "Error loading agent '#{name}' from file '#{file_path}': #{e.message}"
562
+ if node_config[:output_command]
563
+ output_command(node_config[:output_command], timeout: node_config[:output_timeout] || 60)
564
+ end
565
+ end
566
+ end
258
567
  end
259
568
 
569
+ # Resolve agent file path relative to base_dir
570
+ #
571
+ # @param file_path [String] Relative or absolute file path
572
+ # @return [String] Resolved absolute path
260
573
  def resolve_agent_file_path(file_path)
261
574
  return file_path if Pathname.new(file_path).absolute?
262
575
 
263
- @config_dir.join(file_path).to_s
576
+ @base_dir.join(file_path).to_s
264
577
  end
265
578
 
266
579
  def detect_circular_dependencies
@@ -283,8 +596,12 @@ module SwarmSDK
283
596
  path.push(agent_name)
284
597
  connections_for(agent_name).each do |connection|
285
598
  connection_sym = connection.to_sym # Convert to symbol for lookup
599
+
600
+ # Skip external swarms - they are not local agents and don't have circular dependency issues
601
+ next if @external_swarms.key?(connection_sym)
602
+
286
603
  unless @agents.key?(connection_sym)
287
- raise ConfigurationError, "Agent '#{agent_name}' has connection to unknown agent '#{connection}'"
604
+ raise ConfigurationError, "Agent '#{agent_name}' delegates to unknown target '#{connection}' (not a local agent or registered swarm)"
288
605
  end
289
606
 
290
607
  detect_cycle_from(connection_sym, visited, path)