swarm_sdk 2.3.0 → 2.4.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f8643441321fcf3c24d637134d75697f61670ffa4129608c1c4d0e98571805a5
4
- data.tar.gz: cb105cd387671e16eea9ebf52fbcf31a11f9ec0d6a03b7f2e45cad7721434dab
3
+ metadata.gz: 9921bd8cfa2073d8a470b7c9f39ec925f07a4d4c3a38ea03fd1e5bc5e83fd2a7
4
+ data.tar.gz: e7a77f34453d3d3521eea2c3461563050b5057b340f54e7544ad3d9efecbda5a
5
5
  SHA512:
6
- metadata.gz: f613f1d8be2c5fd00ebaaa896b54bdeb34f6537b56638706c5655d8b0ceeb942c87702be5d07be748dd194ec90c48c5e7b24d180ca6bea58714f43cd55afaf86
7
- data.tar.gz: b608df1ef8b5368c4f1e84a057591103943df755706bdaba521fa56075805d6281d434968eadb4cb35edb6a4d9987e710e9b1450fde09c9ed23ed1bebe5af075
6
+ metadata.gz: 7a37d856b2989058fa3a62fb9847de0fe810d03140fb4fee10f88af2f5995c3a79cd2f6443e233403499619c8a9b99c825f64a68386d411a35eb92accc11a225
7
+ data.tar.gz: 42e1849a8b7d9660627b0b1477cf558829e0b7afd46f4087c1a5204a1dd0dd7ae76a09be2c963884fc61f926c3b8c93fd17331cc5d8d1fa5b025c5c44fc71740
@@ -122,7 +122,7 @@ module SwarmSDK
122
122
  max_concurrent_tools = definition[:max_concurrent_tools]
123
123
  base_url = definition[:base_url]
124
124
  api_version = definition[:api_version]
125
- timeout = definition[:timeout] || Defaults::Timeouts::AGENT_REQUEST_SECONDS
125
+ timeout = definition[:timeout] || SwarmSDK.config.agent_request_timeout
126
126
  assume_model_exists = definition[:assume_model_exists]
127
127
  system_prompt = definition[:system_prompt]
128
128
  parameters = definition[:parameters]
@@ -117,7 +117,7 @@ module SwarmSDK
117
117
 
118
118
  # Trigger automatic compression at 60% ONLY if no custom handler
119
119
  compression_triggered = false
120
- if threshold == Context::COMPRESSION_THRESHOLD && !has_custom_handler
120
+ if threshold == SwarmSDK.config.context_compression_threshold && !has_custom_handler
121
121
  compressed_count = apply_automatic_compression
122
122
  compression_triggered = compressed_count > 0
123
123
  end
@@ -47,7 +47,7 @@ module SwarmSDK
47
47
  #
48
48
  # @return [RubyLLM::Chat] Chat instance
49
49
  def instantiate_chat(model_id:, provider_name:, base_url:, timeout:, assume_model_exists:, chat_options:)
50
- if base_url || timeout != Defaults::Timeouts::AGENT_REQUEST_SECONDS
50
+ if base_url || timeout != SwarmSDK.config.agent_request_timeout
51
51
  instantiate_with_custom_context(
52
52
  model_id: model_id,
53
53
  provider_name: provider_name,
@@ -123,22 +123,51 @@ module SwarmSDK
123
123
  end
124
124
 
125
125
  # Configure provider-specific base URL
126
+ #
127
+ # @param config [RubyLLM::Config] RubyLLM configuration context
128
+ # @param provider [String] Provider name
129
+ # @param base_url [String] Custom base URL
130
+ # @raise [ConfigurationError] If API key is required but not configured
131
+ # @raise [ArgumentError] If provider doesn't support custom base_url
126
132
  def configure_provider_base_url(config, provider, base_url)
127
133
  case provider.to_s
128
134
  when "openai", "deepseek", "perplexity", "mistral", "openrouter"
129
135
  config.openai_api_base = base_url
130
- config.openai_api_key = ENV["OPENAI_API_KEY"] || "dummy-key-for-local"
136
+ api_key = SwarmSDK.config.openai_api_key
137
+
138
+ # For local endpoints, API key is optional
139
+ # For cloud endpoints, require API key
140
+ unless api_key || local_endpoint?(base_url)
141
+ raise ConfigurationError,
142
+ "OpenAI API key required for '#{provider}' with base_url '#{base_url}'. " \
143
+ "Configure with: SwarmSDK.configure { |c| c.openai_api_key = '...' }"
144
+ end
145
+
146
+ config.openai_api_key = api_key if api_key
131
147
  config.openai_use_system_role = true
132
148
  when "ollama"
133
149
  config.ollama_api_base = base_url
150
+ # Ollama doesn't need an API key
134
151
  when "gpustack"
135
152
  config.gpustack_api_base = base_url
136
- config.gpustack_api_key = ENV["GPUSTACK_API_KEY"] || "dummy-key"
153
+ api_key = SwarmSDK.config.gpustack_api_key
154
+ config.gpustack_api_key = api_key if api_key
137
155
  else
138
156
  raise ArgumentError, "Provider '#{provider}' doesn't support custom base_url."
139
157
  end
140
158
  end
141
159
 
160
+ # Check if a URL points to a local endpoint
161
+ #
162
+ # @param url [String] URL to check
163
+ # @return [Boolean] true if URL is a local endpoint
164
+ def local_endpoint?(url)
165
+ uri = URI.parse(url)
166
+ ["localhost", "127.0.0.1", "0.0.0.0"].include?(uri.host)
167
+ rescue URI::InvalidURIError
168
+ false
169
+ end
170
+
142
171
  # Fetch real model info for accurate context tracking
143
172
  #
144
173
  # @param model_id [String] Model ID to lookup
@@ -22,9 +22,6 @@ module SwarmSDK
22
22
  <system-reminder>The TodoWrite tool hasn't been used recently. If you're working on tasks that would benefit from tracking progress, consider using the TodoWrite tool to track progress. Also consider cleaning up the todo list if has become stale and no longer matches what you are working on. Only use it if it's relevant to the current work. This is just a gentle reminder - ignore if not applicable.</system-reminder>
23
23
  REMINDER
24
24
 
25
- # Backward compatibility alias - use Defaults module for new code
26
- TODOWRITE_REMINDER_INTERVAL = Defaults::Context::TODOWRITE_REMINDER_INTERVAL
27
-
28
25
  class << self
29
26
  # Check if this is the first user message in the conversation
30
27
  #
@@ -106,15 +103,16 @@ module SwarmSDK
106
103
  end
107
104
 
108
105
  # Check if enough messages have passed since last TodoWrite
106
+ reminder_interval = SwarmSDK.config.todowrite_reminder_interval
109
107
  if last_todo_index.nil? && last_todowrite_index.nil?
110
108
  # Never used TodoWrite - check if we've exceeded interval
111
- chat.message_count >= TODOWRITE_REMINDER_INTERVAL
109
+ chat.message_count >= reminder_interval
112
110
  elsif last_todo_index
113
111
  # Recently used - don't remind
114
112
  false
115
113
  elsif last_todowrite_index
116
114
  # Used before - check if interval has passed
117
- chat.message_count - last_todowrite_index >= TODOWRITE_REMINDER_INTERVAL
115
+ chat.message_count - last_todowrite_index >= reminder_interval
118
116
  else
119
117
  false
120
118
  end
@@ -30,8 +30,7 @@ module SwarmSDK
30
30
  # 60% triggers automatic compression, 80%/90% are informational warnings
31
31
  CONTEXT_WARNING_THRESHOLDS = [60, 80, 90].freeze
32
32
 
33
- # Backward compatibility alias - use Defaults module for new code
34
- COMPRESSION_THRESHOLD = Defaults::Context::COMPRESSION_THRESHOLD_PERCENT
33
+ # NOTE: Compression threshold now accessed via SwarmSDK.config.context_compression_threshold
35
34
 
36
35
  attr_reader :name, :delegation_tools, :metadata, :warning_thresholds_hit, :swarm_id, :parent_swarm_id
37
36
 
@@ -67,14 +67,14 @@ module SwarmSDK
67
67
  end
68
68
 
69
69
  @description = config[:description]
70
- @model = config[:model] || Defaults::Agent::MODEL
71
- @provider = config[:provider] || Defaults::Agent::PROVIDER
70
+ @model = config[:model] || SwarmSDK.config.default_model
71
+ @provider = config[:provider] || SwarmSDK.config.default_provider
72
72
  @base_url = config[:base_url]
73
73
  @api_version = config[:api_version]
74
74
  @context_window = config[:context_window] # Explicit context window override
75
75
  @parameters = config[:parameters] || {}
76
76
  @headers = Utils.stringify_keys(config[:headers] || {})
77
- @timeout = config[:timeout] || Defaults::Timeouts::AGENT_REQUEST_SECONDS
77
+ @timeout = config[:timeout] || SwarmSDK.config.agent_request_timeout
78
78
  @bypass_permissions = config[:bypass_permissions] || false
79
79
  @max_concurrent_tools = config[:max_concurrent_tools]
80
80
  # Always assume model exists - SwarmSDK validates models separately using models.json
@@ -318,7 +318,7 @@ module SwarmSDK
318
318
  # @return [void]
319
319
  def validate_all_agents_filesystem_tools
320
320
  resolved_setting = if @allow_filesystem_tools.nil?
321
- SwarmSDK.settings.allow_filesystem_tools
321
+ SwarmSDK.config.allow_filesystem_tools
322
322
  else
323
323
  @allow_filesystem_tools
324
324
  end
@@ -333,10 +333,10 @@ module SwarmSDK
333
333
  return if forbidden.empty?
334
334
 
335
335
  raise ConfigurationError,
336
- "Filesystem tools are globally disabled (SwarmSDK.settings.allow_filesystem_tools = false) " \
336
+ "Filesystem tools are globally disabled (SwarmSDK.config.allow_filesystem_tools = false) " \
337
337
  "but all_agents configuration includes: #{forbidden.join(", ")}.\n\n" \
338
338
  "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."
339
+ "To use filesystem tools, set SwarmSDK.config.allow_filesystem_tools = true before loading the swarm."
340
340
  end
341
341
 
342
342
  # Validate individual agent filesystem tools
@@ -345,7 +345,7 @@ module SwarmSDK
345
345
  # @return [void]
346
346
  def validate_agent_filesystem_tools
347
347
  resolved_setting = if @allow_filesystem_tools.nil?
348
- SwarmSDK.settings.allow_filesystem_tools
348
+ SwarmSDK.config.allow_filesystem_tools
349
349
  else
350
350
  @allow_filesystem_tools
351
351
  end
@@ -373,10 +373,10 @@ module SwarmSDK
373
373
  next if forbidden.empty?
374
374
 
375
375
  raise ConfigurationError,
376
- "Filesystem tools are globally disabled (SwarmSDK.settings.allow_filesystem_tools = false) " \
376
+ "Filesystem tools are globally disabled (SwarmSDK.config.allow_filesystem_tools = false) " \
377
377
  "but agent '#{agent_name}' attempts to use: #{forbidden.join(", ")}.\n\n" \
378
378
  "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."
379
+ "To use filesystem tools, set SwarmSDK.config.allow_filesystem_tools = true before loading the swarm."
380
380
  end
381
381
  end
382
382
 
@@ -0,0 +1,301 @@
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
+ }.freeze
95
+
96
+ class << self
97
+ # Get the singleton Config instance
98
+ #
99
+ # @return [Config] The singleton instance
100
+ def instance
101
+ @instance ||= new
102
+ end
103
+
104
+ # Reset the Config instance
105
+ #
106
+ # Clears all configuration including explicit values and cached ENV values.
107
+ # Use in tests to ensure clean state.
108
+ #
109
+ # @return [void]
110
+ def reset!
111
+ @instance = nil
112
+ end
113
+ end
114
+
115
+ # Initialize a new Config instance
116
+ #
117
+ # @note Use Config.instance instead of new for the singleton pattern
118
+ def initialize
119
+ @explicit_values = {}
120
+ @env_values = {}
121
+ @env_loaded = false
122
+ @env_mutex = Mutex.new
123
+ end
124
+
125
+ # ========== API Key Accessors (with RubyLLM proxying) ==========
126
+
127
+ # @!method openai_api_key
128
+ # Get the OpenAI API key
129
+ # @return [String, nil] The API key
130
+ #
131
+ # @!method openai_api_key=(value)
132
+ # Set the OpenAI API key (proxied to RubyLLM)
133
+ # @param value [String] The API key
134
+
135
+ API_KEY_MAPPINGS.each_key do |config_key|
136
+ ruby_llm_key, _ = API_KEY_MAPPINGS[config_key]
137
+
138
+ # Getter
139
+ define_method(config_key) do
140
+ ensure_env_loaded!
141
+ @explicit_values[config_key] || @env_values[config_key]
142
+ end
143
+
144
+ # Setter with RubyLLM proxying
145
+ define_method("#{config_key}=") do |value|
146
+ @explicit_values[config_key] = value
147
+ RubyLLM.config.public_send("#{ruby_llm_key}=", value) if value
148
+ end
149
+ end
150
+
151
+ # ========== Defaults Accessors (with module constant fallback) ==========
152
+
153
+ # @!method default_model
154
+ # Get the default model
155
+ # @return [String] The default model (falls back to Defaults::Agent::MODEL)
156
+ #
157
+ # @!method default_model=(value)
158
+ # Set the default model
159
+ # @param value [String] The default model
160
+
161
+ DEFAULTS_MAPPINGS.each_key do |config_key|
162
+ _env_key, default_proc = DEFAULTS_MAPPINGS[config_key]
163
+
164
+ # Getter with default fallback
165
+ define_method(config_key) do
166
+ ensure_env_loaded!
167
+ @explicit_values[config_key] || @env_values[config_key] || default_proc.call
168
+ end
169
+
170
+ # Setter
171
+ define_method("#{config_key}=") do |value|
172
+ @explicit_values[config_key] = value
173
+ end
174
+ end
175
+
176
+ # ========== Settings Accessors (WebFetch and control) ==========
177
+
178
+ # @!method webfetch_provider
179
+ # Get the WebFetch LLM provider
180
+ # @return [String, nil] The provider
181
+ #
182
+ # @!method allow_filesystem_tools
183
+ # Get whether filesystem tools are allowed
184
+ # @return [Boolean] true if allowed
185
+
186
+ SETTINGS_MAPPINGS.each_key do |config_key|
187
+ _env_key, default_value = SETTINGS_MAPPINGS[config_key]
188
+
189
+ # Getter with default fallback
190
+ define_method(config_key) do
191
+ ensure_env_loaded!
192
+ if @explicit_values.key?(config_key)
193
+ @explicit_values[config_key]
194
+ elsif @env_values.key?(config_key)
195
+ @env_values[config_key]
196
+ else
197
+ default_value
198
+ end
199
+ end
200
+
201
+ # Setter
202
+ define_method("#{config_key}=") do |value|
203
+ @explicit_values[config_key] = value
204
+ end
205
+ end
206
+
207
+ # ========== Convenience Methods ==========
208
+
209
+ # Check if WebFetch LLM processing is enabled
210
+ #
211
+ # WebFetch uses LLM processing when both provider and model are configured.
212
+ #
213
+ # @return [Boolean] true if WebFetch LLM is configured
214
+ def webfetch_llm_enabled?
215
+ !webfetch_provider.nil? && !webfetch_model.nil?
216
+ end
217
+
218
+ private
219
+
220
+ # Ensure ENV values are loaded (lazy loading with double-check locking)
221
+ #
222
+ # Thread-safe lazy loading of ENV values. Only loads once per Config instance.
223
+ #
224
+ # @return [void]
225
+ def ensure_env_loaded!
226
+ return if @env_loaded
227
+
228
+ @env_mutex.synchronize do
229
+ return if @env_loaded
230
+
231
+ load_env_values!
232
+ @env_loaded = true
233
+ end
234
+ end
235
+
236
+ # Load environment variable values
237
+ #
238
+ # Loads API keys (with RubyLLM proxying), defaults, and settings from ENV.
239
+ # Only loads values that haven't been explicitly set.
240
+ #
241
+ # @return [void]
242
+ def load_env_values!
243
+ # Load API keys and proxy to RubyLLM
244
+ API_KEY_MAPPINGS.each do |config_key, (ruby_llm_key, env_key)|
245
+ next if @explicit_values.key?(config_key)
246
+ next unless ENV.key?(env_key)
247
+
248
+ value = ENV[env_key]
249
+ @env_values[config_key] = value
250
+
251
+ # Proxy to RubyLLM
252
+ RubyLLM.config.public_send("#{ruby_llm_key}=", value)
253
+ end
254
+
255
+ # Load defaults (no RubyLLM proxy)
256
+ DEFAULTS_MAPPINGS.each do |config_key, (env_key, _default_proc)|
257
+ next if @explicit_values.key?(config_key)
258
+ next unless ENV.key?(env_key)
259
+
260
+ @env_values[config_key] = parse_env_value(ENV[env_key], config_key)
261
+ end
262
+
263
+ # Load settings (no RubyLLM proxy)
264
+ SETTINGS_MAPPINGS.each do |config_key, (env_key, _default_value)|
265
+ next if @explicit_values.key?(config_key)
266
+ next unless ENV.key?(env_key)
267
+
268
+ @env_values[config_key] = parse_env_value(ENV[env_key], config_key)
269
+ end
270
+ end
271
+
272
+ # Parse environment variable value to appropriate type
273
+ #
274
+ # Converts string ENV values to integers, floats, or booleans based on
275
+ # the configuration key pattern.
276
+ #
277
+ # @param value [String] The ENV value string
278
+ # @param key [Symbol] The configuration key
279
+ # @return [Integer, Float, Boolean, String] The parsed value
280
+ def parse_env_value(value, key)
281
+ case key
282
+ when :allow_filesystem_tools
283
+ # Convert string to boolean
284
+ case value.to_s.downcase
285
+ when "true", "yes", "1", "on", "enabled"
286
+ true
287
+ when "false", "no", "0", "off", "disabled"
288
+ false
289
+ else
290
+ true # Default to true if unrecognized
291
+ end
292
+ when /_timeout$/, /_limit$/, /_interval$/, /_threshold$/, :mcp_log_level, :webfetch_max_tokens
293
+ value.to_i
294
+ when /^chars_per_token/
295
+ value.to_f
296
+ else
297
+ value
298
+ end
299
+ end
300
+ end
301
+ end
@@ -17,10 +17,6 @@ module SwarmSDK
17
17
  # total_tokens = TokenCounter.estimate_messages(messages)
18
18
  #
19
19
  class TokenCounter
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
-
24
20
  class << self
25
21
  # Estimate tokens for a single message
26
22
  #
@@ -78,9 +74,9 @@ module SwarmSDK
78
74
 
79
75
  # Choose characters per token based on content type
80
76
  chars_per_token = if code_ratio > 0.1
81
- CHARS_PER_TOKEN_CODE # Code
77
+ SwarmSDK.config.chars_per_token_code # Code
82
78
  else
83
- CHARS_PER_TOKEN_PROSE # Prose
79
+ SwarmSDK.config.chars_per_token_prose # Prose
84
80
  end
85
81
 
86
82
  (text.length / chars_per_token).ceil
@@ -167,7 +167,7 @@ module SwarmSDK
167
167
  def create_hook_callback(hook_def, event_symbol, agent_name, swarm_name)
168
168
  # Support both string and symbol keys (YAML may be symbolized)
169
169
  command = hook_def[:command] || hook_def["command"]
170
- timeout = hook_def[:timeout] || hook_def["timeout"] || ShellExecutor::DEFAULT_TIMEOUT
170
+ timeout = hook_def[:timeout] || hook_def["timeout"] || SwarmSDK.config.hook_shell_timeout
171
171
 
172
172
  lambda do |context|
173
173
  input_json = build_input_json(context, event_symbol, agent_name)
@@ -191,7 +191,7 @@ module SwarmSDK
191
191
  def create_all_agents_hook_callback(hook_def, event_symbol, swarm_name)
192
192
  # Support both string and symbol keys (YAML may be symbolized)
193
193
  command = hook_def[:command] || hook_def["command"]
194
- timeout = hook_def[:timeout] || hook_def["timeout"] || ShellExecutor::DEFAULT_TIMEOUT
194
+ timeout = hook_def[:timeout] || hook_def["timeout"] || SwarmSDK.config.hook_shell_timeout
195
195
 
196
196
  lambda do |context|
197
197
  # Agent name comes from context
@@ -217,7 +217,7 @@ module SwarmSDK
217
217
  def create_swarm_hook_callback(hook_def, event_symbol, swarm_name)
218
218
  # Support both string and symbol keys (YAML may be symbolized)
219
219
  command = hook_def[:command] || hook_def["command"]
220
- timeout = hook_def[:timeout] || hook_def["timeout"] || ShellExecutor::DEFAULT_TIMEOUT
220
+ timeout = hook_def[:timeout] || hook_def["timeout"] || SwarmSDK.config.hook_shell_timeout
221
221
 
222
222
  lambda do |context|
223
223
  input_json = build_swarm_input_json(context, event_symbol, swarm_name)
@@ -47,8 +47,7 @@ module SwarmSDK
47
47
  # )
48
48
  # # => Result (continue or halt based on exit code)
49
49
  class ShellExecutor
50
- # Backward compatibility alias - use Defaults module for new code
51
- DEFAULT_TIMEOUT = Defaults::Timeouts::HOOK_SHELL_SECONDS
50
+ # NOTE: Timeout now accessed via SwarmSDK.config.hook_shell_timeout
52
51
 
53
52
  class << self
54
53
  # Execute a shell command hook
@@ -60,7 +59,9 @@ module SwarmSDK
60
59
  # @param swarm_name [String, nil] Swarm name for environment variables
61
60
  # @param event [Symbol] Event type for context-aware behavior
62
61
  # @return [Result] Result based on exit code (continue or halt)
63
- def execute(command:, input_json:, timeout: DEFAULT_TIMEOUT, agent_name: nil, swarm_name: nil, event: nil)
62
+ def execute(command:, input_json:, timeout: nil, agent_name: nil, swarm_name: nil, event: nil)
63
+ timeout ||= SwarmSDK.config.hook_shell_timeout
64
+
64
65
  # Build environment variables
65
66
  env = build_environment(agent_name: agent_name, swarm_name: swarm_name)
66
67