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
@@ -1,17 +1,42 @@
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
16
41
  # @return [Configuration] Validated configuration instance
17
42
  # @raise [ConfigurationError] If file not found or invalid
@@ -41,35 +66,24 @@ module SwarmSDK
41
66
 
42
67
  @yaml_content = yaml_content
43
68
  @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
69
+ @parser = nil
70
+ @translator = nil
52
71
  end
53
72
 
73
+ # Parse and validate YAML configuration
74
+ #
75
+ # Delegates to Parser for all parsing logic, then syncs parsed data
76
+ # to instance variables for backward compatibility.
77
+ #
78
+ # @return [self]
54
79
  def load_and_validate
55
- @config = YAML.safe_load(@yaml_content, permitted_classes: [Symbol], aliases: true)
80
+ @parser = Parser.new(@yaml_content, base_dir: @base_dir)
81
+ @parser.parse
56
82
 
57
- unless @config.is_a?(Hash)
58
- raise ConfigurationError, "Invalid YAML syntax: configuration must be a Hash"
59
- end
83
+ # Sync parsed data to instance variables for backward compatibility
84
+ sync_from_parser
60
85
 
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
86
  self
71
- rescue Psych::SyntaxError => e
72
- raise ConfigurationError, "Invalid YAML syntax: #{e.message}"
73
87
  end
74
88
 
75
89
  def agent_names
@@ -80,534 +94,42 @@ module SwarmSDK
80
94
  agent_config = @agents[agent_name]
81
95
  return [] unless agent_config
82
96
 
83
- # Extract delegates_to from hash and convert to symbols
84
97
  delegates = agent_config[:delegates_to] || []
85
98
  Array(delegates).map(&:to_sym)
86
99
  end
87
100
 
88
- # Convert configuration to Swarm or NodeOrchestrator using DSL
101
+ # Convert configuration to Swarm or Workflow using appropriate builder
89
102
  #
90
- # This method translates YAML configuration to Ruby DSL calls.
91
- # The DSL (Swarm::Builder) handles all validation, merging, and construction.
103
+ # Delegates to Translator for all DSL translation logic.
92
104
  #
93
105
  # @param allow_filesystem_tools [Boolean, nil] Whether to allow filesystem tools (nil uses global setting)
94
- # @return [Swarm, NodeOrchestrator] Configured swarm or orchestrator
106
+ # @return [Swarm, Workflow] Configured swarm or workflow
95
107
  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?
108
+ raise ConfigurationError, "Configuration not loaded. Call load_and_validate first." unless @parser
123
109
 
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
110
+ @translator = Translator.new(@parser)
111
+ @translator.to_swarm(allow_filesystem_tools: allow_filesystem_tools)
138
112
  end
139
113
 
140
114
  private
141
115
 
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
116
+ # Sync parsed data from Parser to instance variables
369
117
  #
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)
118
+ # This maintains backward compatibility with code that accesses
119
+ # @config_type, @agents, etc. directly via attr_readers.
120
+ def sync_from_parser
121
+ @config_type = @parser.config_type
122
+ @swarm_name = @parser.swarm_name
123
+ @swarm_id = @parser.swarm_id
124
+ @lead_agent = @parser.lead_agent
125
+ @start_node = @parser.start_node
126
+ @agents = @parser.agents
127
+ @all_agents_config = @parser.all_agents_config
128
+ @swarm_hooks = @parser.swarm_hooks
129
+ @all_agents_hooks = @parser.all_agents_hooks
130
+ @external_swarms = @parser.external_swarms
131
+ @nodes = @parser.nodes
132
+ @scratchpad_enabled = @parser.scratchpad_mode # NOTE: attr_reader says scratchpad_enabled
611
133
  end
612
134
  end
613
135
  end
@@ -17,9 +17,9 @@ module SwarmSDK
17
17
  # total_tokens = TokenCounter.estimate_messages(messages)
18
18
  #
19
19
  class TokenCounter
20
- # Average characters per token for different content types
21
- CHARS_PER_TOKEN_PROSE = 4.0
22
- CHARS_PER_TOKEN_CODE = 3.5
20
+ # Backward compatibility aliases - use Defaults module for new code
21
+ CHARS_PER_TOKEN_PROSE = Defaults::TokenEstimation::CHARS_PER_TOKEN_PROSE
22
+ CHARS_PER_TOKEN_CODE = Defaults::TokenEstimation::CHARS_PER_TOKEN_CODE
23
23
 
24
24
  class << self
25
25
  # Estimate tokens for a single message