claude_swarm 1.0.10 → 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 (81) hide show
  1. checksums.yaml +4 -4
  2. data/{CHANGELOG.md → CHANGELOG.claude-swarm.md} +3 -0
  3. data/CLAUDE.md +0 -1
  4. data/decisions/2025-11-22-001-global-agent-registry.md +172 -0
  5. data/docs/v2/CHANGELOG.swarm_cli.md +12 -0
  6. data/docs/v2/CHANGELOG.swarm_memory.md +139 -0
  7. data/docs/v2/CHANGELOG.swarm_sdk.md +249 -1
  8. data/docs/v2/README.md +15 -5
  9. data/docs/v2/guides/complete-tutorial.md +93 -7
  10. data/docs/v2/guides/getting-started.md +3 -1
  11. data/docs/v2/guides/memory-adapters.md +41 -0
  12. data/docs/v2/guides/{migrating-to-2.3.md → migrating-to-2.x.md} +213 -8
  13. data/docs/v2/guides/plugins.md +52 -5
  14. data/docs/v2/guides/rails-integration.md +6 -0
  15. data/docs/v2/guides/swarm-memory.md +2 -13
  16. data/docs/v2/reference/cli.md +0 -1
  17. data/docs/v2/reference/configuration_reference.md +300 -0
  18. data/docs/v2/reference/event_payload_structures.md +26 -4
  19. data/docs/v2/reference/ruby-dsl.md +457 -4
  20. data/docs/v2/reference/swarm_memory_technical_details.md +7 -29
  21. data/docs/v2/reference/yaml.md +2 -2
  22. data/lib/claude_swarm/mcp_generator.rb +1 -1
  23. data/lib/claude_swarm/orchestrator.rb +8 -1
  24. data/lib/claude_swarm/version.rb +1 -1
  25. data/lib/swarm_cli/version.rb +1 -1
  26. data/lib/swarm_memory/core/semantic_index.rb +10 -2
  27. data/lib/swarm_memory/core/storage.rb +7 -2
  28. data/lib/swarm_memory/dsl/memory_config.rb +37 -0
  29. data/lib/swarm_memory/integration/sdk_plugin.rb +120 -27
  30. data/lib/swarm_memory/optimization/defragmenter.rb +1 -1
  31. data/lib/swarm_memory/prompts/memory_researcher.md.erb +0 -1
  32. data/lib/swarm_memory/tools/load_skill.rb +0 -1
  33. data/lib/swarm_memory/tools/memory_edit.rb +2 -1
  34. data/lib/swarm_memory/tools/memory_read.rb +1 -1
  35. data/lib/swarm_memory/version.rb +1 -1
  36. data/lib/swarm_memory.rb +7 -5
  37. data/lib/swarm_sdk/agent/chat.rb +1 -1
  38. data/lib/swarm_sdk/agent/chat_helpers/context_tracker.rb +4 -0
  39. data/lib/swarm_sdk/agent/chat_helpers/hook_integration.rb +1 -1
  40. data/lib/swarm_sdk/agent/chat_helpers/llm_configuration.rb +38 -4
  41. data/lib/swarm_sdk/agent/chat_helpers/logging_helpers.rb +2 -2
  42. data/lib/swarm_sdk/agent/chat_helpers/system_reminder_injector.rb +3 -5
  43. data/lib/swarm_sdk/agent/chat_helpers/token_tracking.rb +48 -0
  44. data/lib/swarm_sdk/agent/context.rb +1 -2
  45. data/lib/swarm_sdk/agent/definition.rb +3 -3
  46. data/lib/swarm_sdk/agent/system_prompt_builder.rb +1 -1
  47. data/lib/swarm_sdk/agent_registry.rb +146 -0
  48. data/lib/swarm_sdk/builders/base_builder.rb +91 -12
  49. data/lib/swarm_sdk/config.rb +302 -0
  50. data/lib/swarm_sdk/configuration/parser.rb +22 -2
  51. data/lib/swarm_sdk/configuration.rb +13 -4
  52. data/lib/swarm_sdk/context_compactor/token_counter.rb +2 -6
  53. data/lib/swarm_sdk/custom_tool_registry.rb +226 -0
  54. data/lib/swarm_sdk/hooks/adapter.rb +3 -3
  55. data/lib/swarm_sdk/hooks/shell_executor.rb +4 -3
  56. data/lib/swarm_sdk/models.json +4333 -1
  57. data/lib/swarm_sdk/models.rb +43 -2
  58. data/lib/swarm_sdk/plugin.rb +2 -2
  59. data/lib/swarm_sdk/result.rb +52 -0
  60. data/lib/swarm_sdk/swarm/agent_initializer.rb +1 -1
  61. data/lib/swarm_sdk/swarm/hook_triggers.rb +1 -0
  62. data/lib/swarm_sdk/swarm/logging_callbacks.rb +1 -0
  63. data/lib/swarm_sdk/swarm/tool_configurator.rb +18 -4
  64. data/lib/swarm_sdk/swarm.rb +76 -13
  65. data/lib/swarm_sdk/tools/bash.rb +7 -9
  66. data/lib/swarm_sdk/tools/glob.rb +5 -5
  67. data/lib/swarm_sdk/tools/read.rb +8 -8
  68. data/lib/swarm_sdk/tools/stores/scratchpad_storage.rb +4 -3
  69. data/lib/swarm_sdk/tools/web_fetch.rb +20 -18
  70. data/lib/swarm_sdk/version.rb +1 -1
  71. data/lib/swarm_sdk/workflow/builder.rb +49 -0
  72. data/lib/swarm_sdk/workflow/node_builder.rb +4 -2
  73. data/lib/swarm_sdk/workflow/transformer_executor.rb +4 -3
  74. data/lib/swarm_sdk.rb +261 -105
  75. data/swarm_cli.gemspec +1 -1
  76. data/swarm_memory.gemspec +8 -3
  77. data/swarm_sdk.gemspec +4 -4
  78. data/team_full.yml +104 -300
  79. metadata +9 -5
  80. data/lib/swarm_memory/tools/memory_multi_edit.rb +0 -281
  81. /data/lib/swarm_memory/{errors.rb → error.rb} +0 -0
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ # Global registry for reusable agent definitions
5
+ #
6
+ # AgentRegistry allows declaring agents in separate files that can be
7
+ # referenced by name in swarm definitions. This promotes code reuse and
8
+ # separation of concerns - agent definitions can live in dedicated files
9
+ # while swarm configurations compose them together.
10
+ #
11
+ # ## Usage
12
+ #
13
+ # Register agents globally (typically in separate files):
14
+ #
15
+ # # agents/backend.rb
16
+ # SwarmSDK.agent :backend do
17
+ # model "claude-sonnet-4"
18
+ # description "Backend API developer"
19
+ # system_prompt "You build REST APIs"
20
+ # tools :Read, :Edit, :Bash
21
+ # end
22
+ #
23
+ # Reference registered agents in swarm definitions:
24
+ #
25
+ # # swarm.rb
26
+ # SwarmSDK.build do
27
+ # name "Dev Team"
28
+ # lead :backend
29
+ #
30
+ # agent :backend # Pulls from registry
31
+ # end
32
+ #
33
+ # ## Override Support
34
+ #
35
+ # Registered agents can be extended with additional configuration:
36
+ #
37
+ # SwarmSDK.build do
38
+ # name "Dev Team"
39
+ # lead :backend
40
+ #
41
+ # agent :backend do
42
+ # # Registry config is applied first, then this block
43
+ # tools :CustomTool # Adds to tools from registry
44
+ # delegates_to :database
45
+ # end
46
+ # end
47
+ #
48
+ # @note This registry is not thread-safe. In multi-threaded environments,
49
+ # register all agents before spawning threads, or synchronize access
50
+ # externally. For typical fiber-based async usage (the default in SwarmSDK),
51
+ # this is not a concern.
52
+ #
53
+ class AgentRegistry
54
+ @agents = {}
55
+
56
+ class << self
57
+ # Register an agent definition block
58
+ #
59
+ # Stores a configuration block that will be executed when the agent
60
+ # is referenced in a swarm definition. The block receives an
61
+ # Agent::Builder context and can use all builder DSL methods.
62
+ #
63
+ # @param name [Symbol, String] Agent name (will be symbolized)
64
+ # @yield Agent configuration block using Agent::Builder DSL
65
+ # @return [void]
66
+ # @raise [ArgumentError] If no block is provided
67
+ # @raise [ArgumentError] If agent with same name is already registered
68
+ #
69
+ # @example Register a backend agent
70
+ # SwarmSDK::AgentRegistry.register(:backend) do
71
+ # model "claude-sonnet-4"
72
+ # description "Backend developer"
73
+ # tools :Read, :Edit, :Bash
74
+ # end
75
+ #
76
+ # @example Register with MCP servers
77
+ # SwarmSDK::AgentRegistry.register(:filesystem_agent) do
78
+ # model "gpt-4"
79
+ # description "File manager"
80
+ # mcp_server :fs, type: :stdio, command: "npx", args: ["-y", "@modelcontextprotocol/server-filesystem"]
81
+ # end
82
+ def register(name, &block)
83
+ raise ArgumentError, "Block required for agent registration" unless block_given?
84
+
85
+ sym_name = name.to_sym
86
+ if @agents.key?(sym_name)
87
+ raise ArgumentError,
88
+ "Agent '#{sym_name}' is already registered. " \
89
+ "Use SwarmSDK.clear_agent_registry! to reset, or choose a different name."
90
+ end
91
+
92
+ @agents[sym_name] = block
93
+ end
94
+
95
+ # Retrieve a registered agent block
96
+ #
97
+ # @param name [Symbol, String] Agent name
98
+ # @return [Proc, nil] The registration block or nil if not found
99
+ #
100
+ # @example
101
+ # block = SwarmSDK::AgentRegistry.get(:backend)
102
+ # builder.instance_eval(&block) if block
103
+ def get(name)
104
+ @agents[name.to_sym]
105
+ end
106
+
107
+ # Check if an agent is registered
108
+ #
109
+ # @param name [Symbol, String] Agent name
110
+ # @return [Boolean] true if agent is registered
111
+ #
112
+ # @example
113
+ # if SwarmSDK::AgentRegistry.registered?(:backend)
114
+ # puts "Backend agent is available"
115
+ # end
116
+ def registered?(name)
117
+ @agents.key?(name.to_sym)
118
+ end
119
+
120
+ # List all registered agent names
121
+ #
122
+ # @return [Array<Symbol>] Names of all registered agents
123
+ #
124
+ # @example
125
+ # SwarmSDK::AgentRegistry.names
126
+ # # => [:backend, :frontend, :database]
127
+ def names
128
+ @agents.keys
129
+ end
130
+
131
+ # Clear all registrations
132
+ #
133
+ # Primarily useful for testing to ensure clean state between tests.
134
+ #
135
+ # @return [void]
136
+ #
137
+ # @example In test setup/teardown
138
+ # def teardown
139
+ # SwarmSDK::AgentRegistry.clear
140
+ # end
141
+ def clear
142
+ @agents.clear
143
+ end
144
+ end
145
+ end
146
+ end
@@ -64,11 +64,14 @@ module SwarmSDK
64
64
  @swarm_registry_config = builder.registrations
65
65
  end
66
66
 
67
- # Define an agent with fluent API or load from markdown content
67
+ # Define an agent with fluent API, load from markdown, or reference registry
68
68
  #
69
- # Supports two forms:
70
- # 1. Inline DSL: agent :name do ... end
71
- # 2. Markdown content: agent :name, <<~MD ... MD
69
+ # Supports multiple forms:
70
+ # 1. Registry lookup: agent :name (pulls from global registry)
71
+ # 2. Registry + overrides: agent :name do ... end (when registered)
72
+ # 3. Inline DSL: agent :name do ... end (when not registered)
73
+ # 4. Markdown content: agent :name, <<~MD ... MD
74
+ # 5. Markdown + overrides: agent :name, <<~MD do ... end
72
75
  #
73
76
  # @example Inline DSL
74
77
  # agent :backend do
@@ -77,6 +80,15 @@ module SwarmSDK
77
80
  # tools :Read, :Write
78
81
  # end
79
82
  #
83
+ # @example Registry lookup (agent must be registered with SwarmSDK.agent)
84
+ # agent :backend # Pulls configuration from registry
85
+ #
86
+ # @example Registry + overrides
87
+ # agent :backend do
88
+ # # Base config from registry, then apply overrides
89
+ # tools :CustomTool # Adds to registry-defined tools
90
+ # end
91
+ #
80
92
  # @example Markdown content
81
93
  # agent :backend, <<~MD
82
94
  # ---
@@ -87,19 +99,38 @@ module SwarmSDK
87
99
  # You build APIs.
88
100
  # MD
89
101
  def agent(name, content = nil, &block)
102
+ name = name.to_sym
103
+
90
104
  # Case 1: agent :name, <<~MD do ... end (markdown + overrides)
91
105
  if content.is_a?(String) && block_given? && markdown_content?(content)
92
106
  load_agent_from_markdown_with_overrides(content, name, &block)
107
+
93
108
  # Case 2: agent :name, <<~MD (markdown only)
94
109
  elsif content.is_a?(String) && !block_given? && markdown_content?(content)
95
110
  load_agent_from_markdown(content, name)
96
- # Case 3: agent :name do ... end (inline DSL)
111
+
112
+ # Case 3: agent :name (registry lookup only - no content, no block)
113
+ elsif content.nil? && !block_given?
114
+ load_agent_from_registry(name)
115
+
116
+ # Case 4: agent :name do ... end (with registered agent - registry + overrides)
117
+ elsif content.nil? && block_given? && AgentRegistry.registered?(name)
118
+ load_agent_from_registry_with_overrides(name, &block)
119
+
120
+ # Case 5: agent :name do ... end (inline DSL - not registered)
97
121
  elsif block_given?
98
122
  builder = Agent::Builder.new(name)
99
123
  builder.instance_eval(&block)
100
124
  @agents[name] = builder
125
+
101
126
  else
102
- raise ArgumentError, "Invalid agent definition. Use: agent :name { ... } OR agent :name, <<~MD ... MD OR agent :name, <<~MD do ... end"
127
+ raise ArgumentError,
128
+ "Invalid agent definition for '#{name}'. Use:\n " \
129
+ "agent :#{name} { ... } # Inline DSL\n " \
130
+ "agent :#{name} # Registry lookup\n " \
131
+ "agent :#{name} { ... } # Registry + overrides (if registered)\n " \
132
+ "agent :#{name}, <<~MD ... MD # Markdown\n " \
133
+ "agent :#{name}, <<~MD do ... end # Markdown + overrides"
103
134
  end
104
135
  end
105
136
 
@@ -138,6 +169,54 @@ module SwarmSDK
138
169
  str.start_with?("---") || str.include?("\n---\n")
139
170
  end
140
171
 
172
+ # Load an agent from the global registry
173
+ #
174
+ # Retrieves the registered agent block and executes it in the context
175
+ # of a new Agent::Builder.
176
+ #
177
+ # @param name [Symbol] Agent name
178
+ # @return [void]
179
+ # @raise [ConfigurationError] If agent is not registered
180
+ #
181
+ # @example
182
+ # load_agent_from_registry(:backend)
183
+ def load_agent_from_registry(name)
184
+ registered_proc = AgentRegistry.get(name)
185
+ unless registered_proc
186
+ raise ConfigurationError,
187
+ "Agent '#{name}' not found in registry. " \
188
+ "Either define inline with `agent :#{name} do ... end` or " \
189
+ "register globally with `SwarmSDK.agent :#{name} do ... end`"
190
+ end
191
+
192
+ builder = Agent::Builder.new(name)
193
+ builder.instance_eval(&registered_proc)
194
+ @agents[name] = builder
195
+ end
196
+
197
+ # Load an agent from the registry with additional overrides
198
+ #
199
+ # Applies the registered configuration first, then executes the
200
+ # override block to customize the agent.
201
+ #
202
+ # @param name [Symbol] Agent name
203
+ # @yield Override block with additional configuration
204
+ # @return [void]
205
+ #
206
+ # @example
207
+ # load_agent_from_registry_with_overrides(:backend) do
208
+ # tools :CustomTool # Adds to registry-defined tools
209
+ # end
210
+ def load_agent_from_registry_with_overrides(name, &override_block)
211
+ registered_proc = AgentRegistry.get(name)
212
+ # Guaranteed to exist since we checked in the condition
213
+
214
+ builder = Agent::Builder.new(name)
215
+ builder.instance_eval(&registered_proc) # Base config from registry
216
+ builder.instance_eval(&override_block) # Apply overrides
217
+ @agents[name] = builder
218
+ end
219
+
141
220
  # Load an agent from markdown content
142
221
  #
143
222
  # Returns a hash of the agent config (not a Definition yet) so that
@@ -318,7 +397,7 @@ module SwarmSDK
318
397
  # @return [void]
319
398
  def validate_all_agents_filesystem_tools
320
399
  resolved_setting = if @allow_filesystem_tools.nil?
321
- SwarmSDK.settings.allow_filesystem_tools
400
+ SwarmSDK.config.allow_filesystem_tools
322
401
  else
323
402
  @allow_filesystem_tools
324
403
  end
@@ -333,10 +412,10 @@ module SwarmSDK
333
412
  return if forbidden.empty?
334
413
 
335
414
  raise ConfigurationError,
336
- "Filesystem tools are globally disabled (SwarmSDK.settings.allow_filesystem_tools = false) " \
415
+ "Filesystem tools are globally disabled (SwarmSDK.config.allow_filesystem_tools = false) " \
337
416
  "but all_agents configuration includes: #{forbidden.join(", ")}.\n\n" \
338
417
  "This is a system-wide security setting that cannot be overridden by swarm configuration.\n" \
339
- "To use filesystem tools, set SwarmSDK.settings.allow_filesystem_tools = true before loading the swarm."
418
+ "To use filesystem tools, set SwarmSDK.config.allow_filesystem_tools = true before loading the swarm."
340
419
  end
341
420
 
342
421
  # Validate individual agent filesystem tools
@@ -345,7 +424,7 @@ module SwarmSDK
345
424
  # @return [void]
346
425
  def validate_agent_filesystem_tools
347
426
  resolved_setting = if @allow_filesystem_tools.nil?
348
- SwarmSDK.settings.allow_filesystem_tools
427
+ SwarmSDK.config.allow_filesystem_tools
349
428
  else
350
429
  @allow_filesystem_tools
351
430
  end
@@ -373,10 +452,10 @@ module SwarmSDK
373
452
  next if forbidden.empty?
374
453
 
375
454
  raise ConfigurationError,
376
- "Filesystem tools are globally disabled (SwarmSDK.settings.allow_filesystem_tools = false) " \
455
+ "Filesystem tools are globally disabled (SwarmSDK.config.allow_filesystem_tools = false) " \
377
456
  "but agent '#{agent_name}' attempts to use: #{forbidden.join(", ")}.\n\n" \
378
457
  "This is a system-wide security setting that cannot be overridden by swarm configuration.\n" \
379
- "To use filesystem tools, set SwarmSDK.settings.allow_filesystem_tools = true before loading the swarm."
458
+ "To use filesystem tools, set SwarmSDK.config.allow_filesystem_tools = true before loading the swarm."
380
459
  end
381
460
  end
382
461
 
@@ -0,0 +1,302 @@
1
+ # frozen_string_literal: true
2
+
3
+ module SwarmSDK
4
+ # Centralized configuration for SwarmSDK
5
+ #
6
+ # Config provides a single entry point for all SwarmSDK configuration,
7
+ # including API keys (proxied to RubyLLM), defaults override, and
8
+ # WebFetch settings.
9
+ #
10
+ # ## Priority Order
11
+ #
12
+ # Configuration values are resolved in this order:
13
+ # 1. Explicit value (set via SwarmSDK.configure)
14
+ # 2. Environment variable
15
+ # 3. Module default (from SwarmSDK::Defaults)
16
+ #
17
+ # ## API Key Proxying
18
+ #
19
+ # API keys are automatically proxied to RubyLLM.config when set,
20
+ # ensuring RubyLLM always has the correct credentials.
21
+ #
22
+ # @example Basic configuration
23
+ # SwarmSDK.configure do |config|
24
+ # config.openai_api_key = "sk-..."
25
+ # config.default_model = "claude-sonnet-4"
26
+ # config.agent_request_timeout = 600
27
+ # end
28
+ #
29
+ # @example Testing setup
30
+ # def setup
31
+ # SwarmSDK.reset_config!
32
+ # end
33
+ class Config
34
+ # API keys that proxy to RubyLLM.config
35
+ # Maps SwarmSDK config key => [RubyLLM config key, ENV variable]
36
+ API_KEY_MAPPINGS = {
37
+ openai_api_key: [:openai_api_key, "OPENAI_API_KEY"],
38
+ openai_api_base: [:openai_api_base, "OPENAI_API_BASE"],
39
+ openai_organization_id: [:openai_organization_id, "OPENAI_ORG_ID"],
40
+ openai_project_id: [:openai_project_id, "OPENAI_PROJECT_ID"],
41
+ anthropic_api_key: [:anthropic_api_key, "ANTHROPIC_API_KEY"],
42
+ gemini_api_key: [:gemini_api_key, "GEMINI_API_KEY"],
43
+ gemini_api_base: [:gemini_api_base, "GEMINI_API_BASE"],
44
+ vertexai_project_id: [:vertexai_project_id, "GOOGLE_CLOUD_PROJECT"],
45
+ vertexai_location: [:vertexai_location, "GOOGLE_CLOUD_LOCATION"],
46
+ deepseek_api_key: [:deepseek_api_key, "DEEPSEEK_API_KEY"],
47
+ mistral_api_key: [:mistral_api_key, "MISTRAL_API_KEY"],
48
+ perplexity_api_key: [:perplexity_api_key, "PERPLEXITY_API_KEY"],
49
+ openrouter_api_key: [:openrouter_api_key, "OPENROUTER_API_KEY"],
50
+ bedrock_api_key: [:bedrock_api_key, "AWS_ACCESS_KEY_ID"],
51
+ bedrock_secret_key: [:bedrock_secret_key, "AWS_SECRET_ACCESS_KEY"],
52
+ bedrock_region: [:bedrock_region, "AWS_REGION"],
53
+ bedrock_session_token: [:bedrock_session_token, "AWS_SESSION_TOKEN"],
54
+ ollama_api_base: [:ollama_api_base, "OLLAMA_API_BASE"],
55
+ gpustack_api_base: [:gpustack_api_base, "GPUSTACK_API_BASE"],
56
+ gpustack_api_key: [:gpustack_api_key, "GPUSTACK_API_KEY"],
57
+ }.freeze
58
+
59
+ # SwarmSDK defaults that can be overridden
60
+ # Maps config key => [ENV variable, default proc]
61
+ DEFAULTS_MAPPINGS = {
62
+ default_model: ["SWARM_SDK_DEFAULT_MODEL", -> { Defaults::Agent::MODEL }],
63
+ default_provider: ["SWARM_SDK_DEFAULT_PROVIDER", -> { Defaults::Agent::PROVIDER }],
64
+ agent_request_timeout: ["SWARM_SDK_AGENT_REQUEST_TIMEOUT", -> { Defaults::Timeouts::AGENT_REQUEST_SECONDS }],
65
+ bash_command_timeout: ["SWARM_SDK_BASH_COMMAND_TIMEOUT", -> { Defaults::Timeouts::BASH_COMMAND_MS }],
66
+ bash_command_max_timeout: ["SWARM_SDK_BASH_COMMAND_MAX_TIMEOUT", -> { Defaults::Timeouts::BASH_COMMAND_MAX_MS }],
67
+ web_fetch_timeout: ["SWARM_SDK_WEB_FETCH_TIMEOUT", -> { Defaults::Timeouts::WEB_FETCH_SECONDS }],
68
+ hook_shell_timeout: ["SWARM_SDK_HOOK_SHELL_TIMEOUT", -> { Defaults::Timeouts::HOOK_SHELL_SECONDS }],
69
+ transformer_command_timeout: ["SWARM_SDK_TRANSFORMER_COMMAND_TIMEOUT", -> { Defaults::Timeouts::TRANSFORMER_COMMAND_SECONDS }],
70
+ global_concurrency_limit: ["SWARM_SDK_GLOBAL_CONCURRENCY_LIMIT", -> { Defaults::Concurrency::GLOBAL_LIMIT }],
71
+ local_concurrency_limit: ["SWARM_SDK_LOCAL_CONCURRENCY_LIMIT", -> { Defaults::Concurrency::LOCAL_LIMIT }],
72
+ output_character_limit: ["SWARM_SDK_OUTPUT_CHARACTER_LIMIT", -> { Defaults::Limits::OUTPUT_CHARACTERS }],
73
+ read_line_limit: ["SWARM_SDK_READ_LINE_LIMIT", -> { Defaults::Limits::READ_LINES }],
74
+ line_character_limit: ["SWARM_SDK_LINE_CHARACTER_LIMIT", -> { Defaults::Limits::LINE_CHARACTERS }],
75
+ web_fetch_character_limit: ["SWARM_SDK_WEB_FETCH_CHARACTER_LIMIT", -> { Defaults::Limits::WEB_FETCH_CHARACTERS }],
76
+ glob_result_limit: ["SWARM_SDK_GLOB_RESULT_LIMIT", -> { Defaults::Limits::GLOB_RESULTS }],
77
+ scratchpad_entry_size_limit: ["SWARM_SDK_SCRATCHPAD_ENTRY_SIZE_LIMIT", -> { Defaults::Storage::ENTRY_SIZE_BYTES }],
78
+ scratchpad_total_size_limit: ["SWARM_SDK_SCRATCHPAD_TOTAL_SIZE_LIMIT", -> { Defaults::Storage::TOTAL_SIZE_BYTES }],
79
+ context_compression_threshold: ["SWARM_SDK_CONTEXT_COMPRESSION_THRESHOLD", -> { Defaults::Context::COMPRESSION_THRESHOLD_PERCENT }],
80
+ todowrite_reminder_interval: ["SWARM_SDK_TODOWRITE_REMINDER_INTERVAL", -> { Defaults::Context::TODOWRITE_REMINDER_INTERVAL }],
81
+ chars_per_token_prose: ["SWARM_SDK_CHARS_PER_TOKEN_PROSE", -> { Defaults::TokenEstimation::CHARS_PER_TOKEN_PROSE }],
82
+ chars_per_token_code: ["SWARM_SDK_CHARS_PER_TOKEN_CODE", -> { Defaults::TokenEstimation::CHARS_PER_TOKEN_CODE }],
83
+ mcp_log_level: ["SWARM_SDK_MCP_LOG_LEVEL", -> { Defaults::Logging::MCP_LOG_LEVEL }],
84
+ }.freeze
85
+
86
+ # WebFetch and control settings
87
+ # Maps config key => [ENV variable, default value]
88
+ SETTINGS_MAPPINGS = {
89
+ webfetch_provider: ["SWARM_SDK_WEBFETCH_PROVIDER", nil],
90
+ webfetch_model: ["SWARM_SDK_WEBFETCH_MODEL", nil],
91
+ webfetch_base_url: ["SWARM_SDK_WEBFETCH_BASE_URL", nil],
92
+ webfetch_max_tokens: ["SWARM_SDK_WEBFETCH_MAX_TOKENS", 4096],
93
+ allow_filesystem_tools: ["SWARM_SDK_ALLOW_FILESYSTEM_TOOLS", true],
94
+ env_interpolation: ["SWARM_SDK_ENV_INTERPOLATION", true],
95
+ }.freeze
96
+
97
+ class << self
98
+ # Get the singleton Config instance
99
+ #
100
+ # @return [Config] The singleton instance
101
+ def instance
102
+ @instance ||= new
103
+ end
104
+
105
+ # Reset the Config instance
106
+ #
107
+ # Clears all configuration including explicit values and cached ENV values.
108
+ # Use in tests to ensure clean state.
109
+ #
110
+ # @return [void]
111
+ def reset!
112
+ @instance = nil
113
+ end
114
+ end
115
+
116
+ # Initialize a new Config instance
117
+ #
118
+ # @note Use Config.instance instead of new for the singleton pattern
119
+ def initialize
120
+ @explicit_values = {}
121
+ @env_values = {}
122
+ @env_loaded = false
123
+ @env_mutex = Mutex.new
124
+ end
125
+
126
+ # ========== API Key Accessors (with RubyLLM proxying) ==========
127
+
128
+ # @!method openai_api_key
129
+ # Get the OpenAI API key
130
+ # @return [String, nil] The API key
131
+ #
132
+ # @!method openai_api_key=(value)
133
+ # Set the OpenAI API key (proxied to RubyLLM)
134
+ # @param value [String] The API key
135
+
136
+ API_KEY_MAPPINGS.each_key do |config_key|
137
+ ruby_llm_key, _ = API_KEY_MAPPINGS[config_key]
138
+
139
+ # Getter
140
+ define_method(config_key) do
141
+ ensure_env_loaded!
142
+ @explicit_values[config_key] || @env_values[config_key]
143
+ end
144
+
145
+ # Setter with RubyLLM proxying
146
+ define_method("#{config_key}=") do |value|
147
+ @explicit_values[config_key] = value
148
+ RubyLLM.config.public_send("#{ruby_llm_key}=", value) if value
149
+ end
150
+ end
151
+
152
+ # ========== Defaults Accessors (with module constant fallback) ==========
153
+
154
+ # @!method default_model
155
+ # Get the default model
156
+ # @return [String] The default model (falls back to Defaults::Agent::MODEL)
157
+ #
158
+ # @!method default_model=(value)
159
+ # Set the default model
160
+ # @param value [String] The default model
161
+
162
+ DEFAULTS_MAPPINGS.each_key do |config_key|
163
+ _env_key, default_proc = DEFAULTS_MAPPINGS[config_key]
164
+
165
+ # Getter with default fallback
166
+ define_method(config_key) do
167
+ ensure_env_loaded!
168
+ @explicit_values[config_key] || @env_values[config_key] || default_proc.call
169
+ end
170
+
171
+ # Setter
172
+ define_method("#{config_key}=") do |value|
173
+ @explicit_values[config_key] = value
174
+ end
175
+ end
176
+
177
+ # ========== Settings Accessors (WebFetch and control) ==========
178
+
179
+ # @!method webfetch_provider
180
+ # Get the WebFetch LLM provider
181
+ # @return [String, nil] The provider
182
+ #
183
+ # @!method allow_filesystem_tools
184
+ # Get whether filesystem tools are allowed
185
+ # @return [Boolean] true if allowed
186
+
187
+ SETTINGS_MAPPINGS.each_key do |config_key|
188
+ _env_key, default_value = SETTINGS_MAPPINGS[config_key]
189
+
190
+ # Getter with default fallback
191
+ define_method(config_key) do
192
+ ensure_env_loaded!
193
+ if @explicit_values.key?(config_key)
194
+ @explicit_values[config_key]
195
+ elsif @env_values.key?(config_key)
196
+ @env_values[config_key]
197
+ else
198
+ default_value
199
+ end
200
+ end
201
+
202
+ # Setter
203
+ define_method("#{config_key}=") do |value|
204
+ @explicit_values[config_key] = value
205
+ end
206
+ end
207
+
208
+ # ========== Convenience Methods ==========
209
+
210
+ # Check if WebFetch LLM processing is enabled
211
+ #
212
+ # WebFetch uses LLM processing when both provider and model are configured.
213
+ #
214
+ # @return [Boolean] true if WebFetch LLM is configured
215
+ def webfetch_llm_enabled?
216
+ !webfetch_provider.nil? && !webfetch_model.nil?
217
+ end
218
+
219
+ private
220
+
221
+ # Ensure ENV values are loaded (lazy loading with double-check locking)
222
+ #
223
+ # Thread-safe lazy loading of ENV values. Only loads once per Config instance.
224
+ #
225
+ # @return [void]
226
+ def ensure_env_loaded!
227
+ return if @env_loaded
228
+
229
+ @env_mutex.synchronize do
230
+ return if @env_loaded
231
+
232
+ load_env_values!
233
+ @env_loaded = true
234
+ end
235
+ end
236
+
237
+ # Load environment variable values
238
+ #
239
+ # Loads API keys (with RubyLLM proxying), defaults, and settings from ENV.
240
+ # Only loads values that haven't been explicitly set.
241
+ #
242
+ # @return [void]
243
+ def load_env_values!
244
+ # Load API keys and proxy to RubyLLM
245
+ API_KEY_MAPPINGS.each do |config_key, (ruby_llm_key, env_key)|
246
+ next if @explicit_values.key?(config_key)
247
+ next unless ENV.key?(env_key)
248
+
249
+ value = ENV[env_key]
250
+ @env_values[config_key] = value
251
+
252
+ # Proxy to RubyLLM
253
+ RubyLLM.config.public_send("#{ruby_llm_key}=", value)
254
+ end
255
+
256
+ # Load defaults (no RubyLLM proxy)
257
+ DEFAULTS_MAPPINGS.each do |config_key, (env_key, _default_proc)|
258
+ next if @explicit_values.key?(config_key)
259
+ next unless ENV.key?(env_key)
260
+
261
+ @env_values[config_key] = parse_env_value(ENV[env_key], config_key)
262
+ end
263
+
264
+ # Load settings (no RubyLLM proxy)
265
+ SETTINGS_MAPPINGS.each do |config_key, (env_key, _default_value)|
266
+ next if @explicit_values.key?(config_key)
267
+ next unless ENV.key?(env_key)
268
+
269
+ @env_values[config_key] = parse_env_value(ENV[env_key], config_key)
270
+ end
271
+ end
272
+
273
+ # Parse environment variable value to appropriate type
274
+ #
275
+ # Converts string ENV values to integers, floats, or booleans based on
276
+ # the configuration key pattern.
277
+ #
278
+ # @param value [String] The ENV value string
279
+ # @param key [Symbol] The configuration key
280
+ # @return [Integer, Float, Boolean, String] The parsed value
281
+ def parse_env_value(value, key)
282
+ case key
283
+ when :allow_filesystem_tools, :env_interpolation
284
+ # Convert string to boolean
285
+ case value.to_s.downcase
286
+ when "true", "yes", "1", "on", "enabled"
287
+ true
288
+ when "false", "no", "0", "off", "disabled"
289
+ false
290
+ else
291
+ true # Default to true if unrecognized
292
+ end
293
+ when /_timeout$/, /_limit$/, /_interval$/, /_threshold$/, :mcp_log_level, :webfetch_max_tokens
294
+ value.to_i
295
+ when /^chars_per_token/
296
+ value.to_f
297
+ else
298
+ value
299
+ end
300
+ end
301
+ end
302
+ end
@@ -30,9 +30,18 @@ module SwarmSDK
30
30
  :nodes,
31
31
  :external_swarms
32
32
 
33
- def initialize(yaml_content, base_dir:)
33
+ # Initialize parser with YAML content and options
34
+ #
35
+ # @param yaml_content [String] YAML configuration content
36
+ # @param base_dir [String, Pathname] Base directory for resolving paths
37
+ # @param env_interpolation [Boolean, nil] Whether to interpolate environment variables.
38
+ # When nil, uses the global SwarmSDK.config.env_interpolation setting.
39
+ # When true, interpolates ${VAR} and ${VAR:=default} patterns.
40
+ # When false, skips interpolation entirely.
41
+ def initialize(yaml_content, base_dir:, env_interpolation: nil)
34
42
  @yaml_content = yaml_content
35
43
  @base_dir = Pathname.new(base_dir).expand_path
44
+ @env_interpolation = env_interpolation
36
45
  @config_type = nil
37
46
  @swarm_id = nil
38
47
  @swarm_name = nil
@@ -55,7 +64,7 @@ module SwarmSDK
55
64
  end
56
65
 
57
66
  @config = Utils.symbolize_keys(@config)
58
- interpolate_env_vars!(@config)
67
+ interpolate_env_vars!(@config) if env_interpolation_enabled?
59
68
 
60
69
  validate_version
61
70
  detect_and_validate_type
@@ -86,6 +95,17 @@ module SwarmSDK
86
95
 
87
96
  private
88
97
 
98
+ # Check if environment variable interpolation is enabled
99
+ #
100
+ # Uses the local setting if explicitly set, otherwise falls back to global config.
101
+ #
102
+ # @return [Boolean] true if interpolation should be performed
103
+ def env_interpolation_enabled?
104
+ return @env_interpolation unless @env_interpolation.nil?
105
+
106
+ SwarmSDK.config.env_interpolation
107
+ end
108
+
89
109
  def validate_version
90
110
  version = @config[:version]
91
111
  raise ConfigurationError, "Missing 'version' field in configuration" unless version