claude_swarm 1.0.9 → 1.0.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. checksums.yaml +4 -4
  2. data/{CHANGELOG.md → CHANGELOG.claude-swarm.md} +10 -0
  3. data/CLAUDE.md +346 -191
  4. data/decisions/2025-11-22-001-global-agent-registry.md +172 -0
  5. data/docs/v2/CHANGELOG.swarm_cli.md +20 -0
  6. data/docs/v2/CHANGELOG.swarm_memory.md +146 -1
  7. data/docs/v2/CHANGELOG.swarm_sdk.md +433 -10
  8. data/docs/v2/README.md +20 -5
  9. data/docs/v2/guides/complete-tutorial.md +95 -9
  10. data/docs/v2/guides/getting-started.md +10 -8
  11. data/docs/v2/guides/memory-adapters.md +41 -0
  12. data/docs/v2/guides/migrating-to-2.x.md +746 -0
  13. data/docs/v2/guides/plugins.md +52 -5
  14. data/docs/v2/guides/rails-integration.md +6 -0
  15. data/docs/v2/guides/snapshots.md +14 -14
  16. data/docs/v2/guides/swarm-memory.md +2 -13
  17. data/docs/v2/reference/architecture-flow.md +3 -3
  18. data/docs/v2/reference/cli.md +0 -1
  19. data/docs/v2/reference/configuration_reference.md +300 -0
  20. data/docs/v2/reference/event_payload_structures.md +27 -5
  21. data/docs/v2/reference/ruby-dsl.md +614 -18
  22. data/docs/v2/reference/swarm_memory_technical_details.md +7 -29
  23. data/docs/v2/reference/yaml.md +172 -54
  24. data/examples/snapshot_demo.rb +2 -2
  25. data/lib/claude_swarm/mcp_generator.rb +8 -21
  26. data/lib/claude_swarm/orchestrator.rb +8 -1
  27. data/lib/claude_swarm/version.rb +1 -1
  28. data/lib/swarm_cli/commands/run.rb +2 -2
  29. data/lib/swarm_cli/config_loader.rb +11 -11
  30. data/lib/swarm_cli/formatters/human_formatter.rb +0 -33
  31. data/lib/swarm_cli/interactive_repl.rb +2 -2
  32. data/lib/swarm_cli/ui/icons.rb +0 -23
  33. data/lib/swarm_cli/version.rb +1 -1
  34. data/lib/swarm_memory/adapters/filesystem_adapter.rb +11 -34
  35. data/lib/swarm_memory/core/semantic_index.rb +10 -2
  36. data/lib/swarm_memory/core/storage.rb +7 -2
  37. data/lib/swarm_memory/dsl/memory_config.rb +37 -0
  38. data/lib/swarm_memory/integration/sdk_plugin.rb +201 -28
  39. data/lib/swarm_memory/optimization/defragmenter.rb +1 -1
  40. data/lib/swarm_memory/prompts/memory_researcher.md.erb +0 -1
  41. data/lib/swarm_memory/tools/load_skill.rb +0 -1
  42. data/lib/swarm_memory/tools/memory_edit.rb +2 -1
  43. data/lib/swarm_memory/tools/memory_read.rb +1 -1
  44. data/lib/swarm_memory/version.rb +1 -1
  45. data/lib/swarm_memory.rb +8 -6
  46. data/lib/swarm_sdk/agent/builder.rb +58 -0
  47. data/lib/swarm_sdk/agent/chat.rb +527 -1061
  48. data/lib/swarm_sdk/agent/{chat → chat_helpers}/context_tracker.rb +13 -88
  49. data/lib/swarm_sdk/agent/chat_helpers/event_emitter.rb +204 -0
  50. data/lib/swarm_sdk/agent/{chat → chat_helpers}/hook_integration.rb +108 -46
  51. data/lib/swarm_sdk/agent/chat_helpers/instrumentation.rb +78 -0
  52. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +267 -0
  53. data/lib/swarm_sdk/agent/{chat → chat_helpers}/logging_helpers.rb +3 -3
  54. data/lib/swarm_sdk/agent/chat_helpers/serialization.rb +83 -0
  55. data/lib/swarm_sdk/agent/{chat → chat_helpers}/system_reminder_injector.rb +11 -13
  56. data/lib/swarm_sdk/agent/chat_helpers/system_reminders.rb +79 -0
  57. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +146 -0
  58. data/lib/swarm_sdk/agent/context.rb +1 -2
  59. data/lib/swarm_sdk/agent/definition.rb +66 -154
  60. data/lib/swarm_sdk/agent/llm_instrumentation_middleware.rb +4 -2
  61. data/lib/swarm_sdk/agent/system_prompt_builder.rb +161 -0
  62. data/lib/swarm_sdk/agent_registry.rb +146 -0
  63. data/lib/swarm_sdk/builders/base_builder.rb +488 -0
  64. data/lib/swarm_sdk/concerns/cleanupable.rb +39 -0
  65. data/lib/swarm_sdk/concerns/snapshotable.rb +67 -0
  66. data/lib/swarm_sdk/concerns/validatable.rb +55 -0
  67. data/lib/swarm_sdk/config.rb +302 -0
  68. data/lib/swarm_sdk/configuration/parser.rb +373 -0
  69. data/lib/swarm_sdk/configuration/translator.rb +255 -0
  70. data/lib/swarm_sdk/configuration.rb +77 -546
  71. data/lib/swarm_sdk/context_compactor/token_counter.rb +2 -6
  72. data/lib/swarm_sdk/context_compactor.rb +6 -11
  73. data/lib/swarm_sdk/context_management/builder.rb +128 -0
  74. data/lib/swarm_sdk/context_management/context.rb +328 -0
  75. data/lib/swarm_sdk/custom_tool_registry.rb +226 -0
  76. data/lib/swarm_sdk/defaults.rb +196 -0
  77. data/lib/swarm_sdk/events_to_messages.rb +18 -0
  78. data/lib/swarm_sdk/hooks/adapter.rb +3 -3
  79. data/lib/swarm_sdk/hooks/shell_executor.rb +4 -2
  80. data/lib/swarm_sdk/log_collector.rb +179 -29
  81. data/lib/swarm_sdk/log_stream.rb +29 -0
  82. data/lib/swarm_sdk/models.json +4333 -1
  83. data/lib/swarm_sdk/models.rb +43 -2
  84. data/lib/swarm_sdk/node_context.rb +1 -1
  85. data/lib/swarm_sdk/observer/builder.rb +81 -0
  86. data/lib/swarm_sdk/observer/config.rb +45 -0
  87. data/lib/swarm_sdk/observer/manager.rb +236 -0
  88. data/lib/swarm_sdk/patterns/agent_observer.rb +160 -0
  89. data/lib/swarm_sdk/plugin.rb +95 -5
  90. data/lib/swarm_sdk/result.rb +52 -0
  91. data/lib/swarm_sdk/snapshot.rb +6 -6
  92. data/lib/swarm_sdk/snapshot_from_events.rb +13 -2
  93. data/lib/swarm_sdk/state_restorer.rb +136 -151
  94. data/lib/swarm_sdk/state_snapshot.rb +65 -100
  95. data/lib/swarm_sdk/swarm/agent_initializer.rb +181 -137
  96. data/lib/swarm_sdk/swarm/builder.rb +44 -578
  97. data/lib/swarm_sdk/swarm/executor.rb +213 -0
  98. data/lib/swarm_sdk/swarm/hook_triggers.rb +151 -0
  99. data/lib/swarm_sdk/swarm/logging_callbacks.rb +341 -0
  100. data/lib/swarm_sdk/swarm/mcp_configurator.rb +7 -4
  101. data/lib/swarm_sdk/swarm/tool_configurator.rb +58 -140
  102. data/lib/swarm_sdk/swarm.rb +203 -683
  103. data/lib/swarm_sdk/tools/bash.rb +14 -8
  104. data/lib/swarm_sdk/tools/delegate.rb +61 -43
  105. data/lib/swarm_sdk/tools/edit.rb +8 -13
  106. data/lib/swarm_sdk/tools/glob.rb +12 -4
  107. data/lib/swarm_sdk/tools/grep.rb +7 -0
  108. data/lib/swarm_sdk/tools/multi_edit.rb +15 -11
  109. data/lib/swarm_sdk/tools/path_resolver.rb +51 -2
  110. data/lib/swarm_sdk/tools/read.rb +16 -18
  111. data/lib/swarm_sdk/tools/registry.rb +122 -10
  112. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +9 -5
  113. data/lib/swarm_sdk/tools/stores/storage.rb +0 -6
  114. data/lib/swarm_sdk/tools/todo_write.rb +7 -0
  115. data/lib/swarm_sdk/tools/web_fetch.rb +20 -17
  116. data/lib/swarm_sdk/tools/write.rb +8 -13
  117. data/lib/swarm_sdk/version.rb +1 -1
  118. data/lib/swarm_sdk/{node → workflow}/agent_config.rb +1 -1
  119. data/lib/swarm_sdk/workflow/builder.rb +192 -0
  120. data/lib/swarm_sdk/workflow/executor.rb +497 -0
  121. data/lib/swarm_sdk/{node/builder.rb → workflow/node_builder.rb} +7 -5
  122. data/lib/swarm_sdk/{node → workflow}/transformer_executor.rb +5 -3
  123. data/lib/swarm_sdk/{node_orchestrator.rb → workflow.rb} +152 -456
  124. data/lib/swarm_sdk.rb +294 -108
  125. data/rubocop/cop/security/no_reflection_methods.rb +1 -1
  126. data/swarm_cli.gemspec +1 -1
  127. data/swarm_memory.gemspec +8 -3
  128. data/swarm_sdk.gemspec +6 -4
  129. data/team_full.yml +124 -320
  130. metadata +42 -14
  131. data/lib/swarm_memory/chat_extension.rb +0 -34
  132. data/lib/swarm_memory/tools/memory_multi_edit.rb +0 -281
  133. data/lib/swarm_sdk/providers/openai_with_responses.rb +0 -589
  134. /data/lib/swarm_memory/{errors.rb → error.rb} +0 -0
@@ -1,21 +1,50 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SwarmSDK
4
+ # Configuration facade that delegates to Parser and Translator
5
+ #
6
+ # This class maintains the public API while internally delegating to:
7
+ # - Configuration::Parser - YAML parsing, validation, and normalization
8
+ # - Configuration::Translator - Translation to Swarm/Workflow DSL builders
9
+ #
10
+ # ## Public API (unchanged)
11
+ # - Configuration.load_file(path) - Load from file
12
+ # - Configuration.new(yaml_content, base_dir:) - Load from string
13
+ # - config.load_and_validate - Parse and validate
14
+ # - config.to_swarm(allow_filesystem_tools:) - Convert to Swarm/Workflow
15
+ # - config.agent_names - Get list of agent names
16
+ # - config.connections_for(agent_name) - Get delegation targets
17
+ #
18
+ # ## Architecture
19
+ # The facade pattern keeps backward compatibility while separating concerns:
20
+ # - Parser handles all YAML parsing and validation logic
21
+ # - Translator handles all DSL builder translation logic
22
+ # - Configuration delegates to both, exposing parsed data via attr_readers
4
23
  class Configuration
5
- ENV_VAR_WITH_DEFAULT_PATTERN = /\$\{([^:}]+)(:=([^}]*))?\}/
6
-
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
24
+ attr_reader :config_type,
25
+ :swarm_name,
26
+ :swarm_id,
27
+ :lead_agent,
28
+ :start_node,
29
+ :agents,
30
+ :all_agents_config,
31
+ :swarm_hooks,
32
+ :all_agents_hooks,
33
+ :scratchpad_enabled,
34
+ :nodes,
35
+ :external_swarms
8
36
 
9
37
  class << self
10
38
  # Load configuration from YAML file
11
39
  #
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
40
  # @param path [String, Pathname] Path to YAML configuration file
41
+ # @param env_interpolation [Boolean, nil] Whether to interpolate environment variables.
42
+ # When nil, uses the global SwarmSDK.config.env_interpolation setting.
43
+ # When true, interpolates ${VAR} and ${VAR:=default} patterns.
44
+ # When false, skips interpolation entirely.
16
45
  # @return [Configuration] Validated configuration instance
17
46
  # @raise [ConfigurationError] If file not found or invalid
18
- def load_file(path)
47
+ def load_file(path, env_interpolation: nil)
19
48
  path = Pathname.new(path).expand_path
20
49
 
21
50
  unless path.exist?
@@ -25,7 +54,7 @@ module SwarmSDK
25
54
  yaml_content = File.read(path)
26
55
  base_dir = path.dirname
27
56
 
28
- new(yaml_content, base_dir: base_dir).tap(&:load_and_validate)
57
+ new(yaml_content, base_dir: base_dir, env_interpolation: env_interpolation).tap(&:load_and_validate)
29
58
  rescue Errno::ENOENT
30
59
  raise ConfigurationError, "Configuration file not found: #{path}"
31
60
  end
@@ -35,41 +64,35 @@ module SwarmSDK
35
64
  #
36
65
  # @param yaml_content [String] YAML configuration content
37
66
  # @param base_dir [String, Pathname] Base directory for resolving agent file paths (default: Dir.pwd)
38
- def initialize(yaml_content, base_dir: Dir.pwd)
67
+ # @param env_interpolation [Boolean, nil] Whether to interpolate environment variables.
68
+ # When nil, uses the global SwarmSDK.config.env_interpolation setting.
69
+ # When true, interpolates ${VAR} and ${VAR:=default} patterns.
70
+ # When false, skips interpolation entirely.
71
+ def initialize(yaml_content, base_dir: Dir.pwd, env_interpolation: nil)
39
72
  raise ArgumentError, "yaml_content cannot be nil" if yaml_content.nil?
40
73
  raise ArgumentError, "base_dir cannot be nil" if base_dir.nil?
41
74
 
42
75
  @yaml_content = yaml_content
43
76
  @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)
46
- @all_agents_config = {} # Settings applied to all agents
47
- @swarm_hooks = {} # Swarm-level hooks (swarm_start, swarm_stop)
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
77
+ @env_interpolation = env_interpolation
78
+ @parser = nil
79
+ @translator = nil
52
80
  end
53
81
 
82
+ # Parse and validate YAML configuration
83
+ #
84
+ # Delegates to Parser for all parsing logic, then syncs parsed data
85
+ # to instance variables for backward compatibility.
86
+ #
87
+ # @return [self]
54
88
  def load_and_validate
55
- @config = YAML.safe_load(@yaml_content, permitted_classes: [Symbol], aliases: true)
89
+ @parser = Parser.new(@yaml_content, base_dir: @base_dir, env_interpolation: @env_interpolation)
90
+ @parser.parse
56
91
 
57
- unless @config.is_a?(Hash)
58
- raise ConfigurationError, "Invalid YAML syntax: configuration must be a Hash"
59
- end
92
+ # Sync parsed data to instance variables for backward compatibility
93
+ sync_from_parser
60
94
 
61
- @config = Utils.symbolize_keys(@config)
62
- interpolate_env_vars!(@config)
63
- validate_version
64
- load_all_agents_config
65
- load_hooks_config
66
- validate_swarm
67
- load_agents
68
- load_nodes
69
- detect_circular_dependencies
70
95
  self
71
- rescue Psych::SyntaxError => e
72
- raise ConfigurationError, "Invalid YAML syntax: #{e.message}"
73
96
  end
74
97
 
75
98
  def agent_names
@@ -80,534 +103,42 @@ module SwarmSDK
80
103
  agent_config = @agents[agent_name]
81
104
  return [] unless agent_config
82
105
 
83
- # Extract delegates_to from hash and convert to symbols
84
106
  delegates = agent_config[:delegates_to] || []
85
107
  Array(delegates).map(&:to_sym)
86
108
  end
87
109
 
88
- # Convert configuration to Swarm or NodeOrchestrator using DSL
110
+ # Convert configuration to Swarm or Workflow using appropriate builder
89
111
  #
90
- # This method translates YAML configuration to Ruby DSL calls.
91
- # The DSL (Swarm::Builder) handles all validation, merging, and construction.
112
+ # Delegates to Translator for all DSL translation logic.
92
113
  #
93
114
  # @param allow_filesystem_tools [Boolean, nil] Whether to allow filesystem tools (nil uses global setting)
94
- # @return [Swarm, NodeOrchestrator] Configured swarm or orchestrator
115
+ # @return [Swarm, Workflow] Configured swarm or workflow
95
116
  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
119
- end
120
-
121
- # Translate all_agents config to DSL (if present)
122
- translate_all_agents(builder) if @all_agents_config.any?
117
+ raise ConfigurationError, "Configuration not loaded. Call load_and_validate first." unless @parser
123
118
 
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
119
+ @translator = Translator.new(@parser)
120
+ @translator.to_swarm(allow_filesystem_tools: allow_filesystem_tools)
138
121
  end
139
122
 
140
123
  private
141
124
 
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
-
158
- def interpolate_env_vars!(obj)
159
- case obj
160
- when String
161
- interpolate_env_string(obj)
162
- when Hash
163
- obj.transform_values! { |v| interpolate_env_vars!(v) }
164
- when Array
165
- obj.map! { |v| interpolate_env_vars!(v) }
166
- else
167
- obj
168
- end
169
- end
170
-
171
- def interpolate_env_string(str)
172
- str.gsub(ENV_VAR_WITH_DEFAULT_PATTERN) do |_match|
173
- env_var = Regexp.last_match(1)
174
- has_default = Regexp.last_match(2)
175
- default_value = Regexp.last_match(3)
176
-
177
- if ENV.key?(env_var)
178
- ENV[env_var]
179
- elsif has_default
180
- default_value || ""
181
- else
182
- raise ConfigurationError, "Environment variable '#{env_var}' is not set"
183
- end
184
- end
185
- end
186
-
187
- def validate_version
188
- version = @config[:version]
189
- raise ConfigurationError, "Missing 'version' field in configuration" unless version
190
- raise ConfigurationError, "SwarmSDK requires version: 2 configuration. Got version: #{version}" unless version == 2
191
- end
192
-
193
- def load_all_agents_config
194
- return unless @config[:swarm]
195
-
196
- @all_agents_config = @config[:swarm][:all_agents] || {}
197
-
198
- # Convert disable_default_tools array elements to symbols
199
- if @all_agents_config[:disable_default_tools].is_a?(Array)
200
- @all_agents_config[:disable_default_tools] = @all_agents_config[:disable_default_tools].map(&:to_sym)
201
- end
202
- end
203
-
204
- def load_hooks_config
205
- return unless @config[:swarm]
206
-
207
- # Load swarm-level hooks (only swarm_start, swarm_stop allowed)
208
- @swarm_hooks = Utils.symbolize_keys(@config[:swarm][:hooks] || {})
209
-
210
- # Load all_agents hooks (applied as swarm defaults)
211
- if @config[:swarm][:all_agents]
212
- @all_agents_hooks = Utils.symbolize_keys(@config[:swarm][:all_agents][:hooks] || {})
213
- end
214
- end
215
-
216
- def validate_swarm
217
- raise ConfigurationError, "Missing 'swarm' field in configuration" unless @config[:swarm]
218
-
219
- swarm = @config[:swarm]
220
- raise ConfigurationError, "Missing 'name' field in swarm configuration" unless swarm[:name]
221
- raise ConfigurationError, "Missing 'agents' field in swarm configuration" unless swarm[:agents]
222
- raise ConfigurationError, "Missing 'lead' field in swarm configuration" unless swarm[:lead]
223
- raise ConfigurationError, "No agents defined" if swarm[:agents].empty?
224
-
225
- @swarm_name = swarm[:name]
226
- @swarm_id = swarm[:id] # Optional - will auto-generate if missing
227
- @lead_agent = swarm[:lead].to_sym # Convert to symbol for consistency
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
266
- end
267
-
268
- def load_agents
269
- swarm_agents = @config[:swarm][:agents]
270
-
271
- swarm_agents.each do |name, agent_config|
272
- # Support three formats:
273
- # 1. String: assistant: "agents/assistant.md" (file path)
274
- # 2. Hash with agent_file: assistant: { agent_file: "..." }
275
- # 3. Hash with inline definition: assistant: { description: "...", model: "..." }
276
- # 4. nil: Invalid (will be caught when building swarm)
277
-
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)
282
- # Format 1: Direct file path as string
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
287
- else
288
- # Format 3: Inline definition
289
- agent_config || {}
290
- end
291
-
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"
297
- end
298
-
299
- @agents[name] = parsed_config
300
- end
301
-
302
- unless @agents.key?(@lead_agent)
303
- raise ConfigurationError, "Lead agent '#{@lead_agent}' not found in agents"
304
- end
305
- end
306
-
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
125
+ # Sync parsed data from Parser to instance variables
369
126
  #
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
406
- #
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
419
- #
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))
441
- else
442
- # Load from markdown only
443
- builder.agent(name, content)
444
- end
445
- else
446
- # Inline definition - translate to DSL
447
- builder.agent(name, &create_agent_config_block(config))
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?
485
-
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]
510
- end
511
- end
512
-
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
525
- end
526
-
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
550
-
551
- # Translate dependencies
552
- depends_on(*node_config[:dependencies]) if node_config[:dependencies]&.any?
553
-
554
- # Translate lead override
555
- lead(node_config[:lead].to_sym) if node_config[:lead]
556
-
557
- # Translate transformers
558
- if node_config[:input_command]
559
- input_command(node_config[:input_command], timeout: node_config[:input_timeout] || 60)
560
- end
561
-
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
567
- end
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
573
- def resolve_agent_file_path(file_path)
574
- return file_path if Pathname.new(file_path).absolute?
575
-
576
- @base_dir.join(file_path).to_s
577
- end
578
-
579
- def detect_circular_dependencies
580
- @agents.each_key do |agent_name|
581
- visited = Set.new
582
- path = []
583
- detect_cycle_from(agent_name, visited, path)
584
- end
585
- end
586
-
587
- def detect_cycle_from(agent_name, visited, path)
588
- return if visited.include?(agent_name)
589
-
590
- if path.include?(agent_name)
591
- cycle_start = path.index(agent_name)
592
- cycle = path[cycle_start..] + [agent_name]
593
- raise CircularDependencyError, "Circular dependency detected: #{cycle.join(" -> ")}"
594
- end
595
-
596
- path.push(agent_name)
597
- connections_for(agent_name).each do |connection|
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
-
603
- unless @agents.key?(connection_sym)
604
- raise ConfigurationError, "Agent '#{agent_name}' delegates to unknown target '#{connection}' (not a local agent or registered swarm)"
605
- end
606
-
607
- detect_cycle_from(connection_sym, visited, path)
608
- end
609
- path.pop
610
- visited.add(agent_name)
127
+ # This maintains backward compatibility with code that accesses
128
+ # @config_type, @agents, etc. directly via attr_readers.
129
+ def sync_from_parser
130
+ @config_type = @parser.config_type
131
+ @swarm_name = @parser.swarm_name
132
+ @swarm_id = @parser.swarm_id
133
+ @lead_agent = @parser.lead_agent
134
+ @start_node = @parser.start_node
135
+ @agents = @parser.agents
136
+ @all_agents_config = @parser.all_agents_config
137
+ @swarm_hooks = @parser.swarm_hooks
138
+ @all_agents_hooks = @parser.all_agents_hooks
139
+ @external_swarms = @parser.external_swarms
140
+ @nodes = @parser.nodes
141
+ @scratchpad_enabled = @parser.scratchpad_mode # NOTE: attr_reader says scratchpad_enabled
611
142
  end
612
143
  end
613
144
  end